Skip to content

Commit

Permalink
Merge branch 'h2oai:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
akwaed authored Oct 17, 2024
2 parents ab7996d + fe7f7bd commit cc61555
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 16 deletions.
57 changes: 57 additions & 0 deletions py/examples/table_events_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Table / Events / Group
# Register the `group_change` #event to emit Wave event when group collapses or opens.
# #table #events #groups
# ---
from h2o_wave import main, app, Q, ui

bobrows = [
{"name":"row1", "cell":"Issue1"},
{"name":"row2", "cell":"Issue2"},
]
johnrows = [
{"name":"row3", "cell":"Issue3"},
{"name":"row4", "cell":"Issue4"},
{"name":"row5", "cell":"Issue5"},
]

collapsed_states = {
'Bob': True,
'John': False
}

@app('/demo')
async def serve(q: Q):
if q.events.issues_table and q.events.issues_table.group_change:
# toggle the collapse states
for group in q.events.issues_table.group_change:
collapsed_states[group] = not collapsed_states[group]
q.page['collapse'].content = f'{q.events.issues_table.group_change}'
else:
q.page['form'] = ui.form_card(box='1 1 4 5', items=[
ui.table(
name='issues_table',
columns=[ui.table_column(name='text', label='Issues assigned to')],
groups=[
ui.table_group("Bob",
rows=[ui.table_row(
name=row["name"],
cells=[row["cell"]])
for row in bobrows],
collapsed=collapsed_states["Bob"]
),
ui.table_group("John",
rows=[ui.table_row(
name=row["name"],
cells=[row["cell"]])
for row in johnrows],
collapsed=collapsed_states["John"]
),],
height='400px',
events=['group_change']
)
])
q.page['collapse'] = ui.markdown_card(box='5 1 2 1', title='Group change info', content='')

q.client.initialized = True

await q.page.save()
1 change: 1 addition & 0 deletions py/examples/tour.conf
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ table_filter_backend.py
table_download.py
table_groupby.py
table_groups.py
table_events_group.py
table_select_single.py
table_select_multiple.py
table_events_select.py
Expand Down
2 changes: 1 addition & 1 deletion py/h2o_lightwave/h2o_lightwave/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3922,7 +3922,7 @@ def __init__(
self.pagination = pagination
"""Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`."""
self.events = events
"""The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'."""
"""The events to capture on this table. When pagination is set, one of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset'. These events are available regardless of pagination: 'select' | 'group_change'."""
self.single = single
"""True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr."""
self.value = value
Expand Down
2 changes: 1 addition & 1 deletion py/h2o_lightwave/h2o_lightwave/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1478,7 +1478,7 @@ def table(
tooltip: An optional tooltip message displayed when a user clicks the help icon to the right of the component.
groups: Creates collapsible / expandable groups of data rows. Mutually exclusive with `rows` attr.
pagination: Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`.
events: The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'.
events: The events to capture on this table. When pagination is set, one of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset'. These events are available regardless of pagination: 'select' | 'group_change'.
single: True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr.
value: The name of the selected row. If this parameter is set, single selection will be allowed (`single` is assumed to be `True`).
Returns:
Expand Down
2 changes: 1 addition & 1 deletion py/h2o_wave/h2o_wave/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3922,7 +3922,7 @@ def __init__(
self.pagination = pagination
"""Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`."""
self.events = events
"""The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'."""
"""The events to capture on this table. When pagination is set, one of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset'. These events are available regardless of pagination: 'select' | 'group_change'."""
self.single = single
"""True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr."""
self.value = value
Expand Down
2 changes: 1 addition & 1 deletion py/h2o_wave/h2o_wave/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1478,7 +1478,7 @@ def table(
tooltip: An optional tooltip message displayed when a user clicks the help icon to the right of the component.
groups: Creates collapsible / expandable groups of data rows. Mutually exclusive with `rows` attr.
pagination: Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`.
events: The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'.
events: The events to capture on this table. When pagination is set, one of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset'. These events are available regardless of pagination: 'select' | 'group_change'.
single: True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr.
value: The name of the selected row. If this parameter is set, single selection will be allowed (`single` is assumed to be `True`).
Returns:
Expand Down
2 changes: 1 addition & 1 deletion r/R/ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -1703,7 +1703,7 @@ ui_table_pagination <- function(
#' @param tooltip An optional tooltip message displayed when a user clicks the help icon to the right of the component.
#' @param groups Creates collapsible / expandable groups of data rows. Mutually exclusive with `rows` attr.
#' @param pagination Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`.
#' @param events The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'.
#' @param events The events to capture on this table. When pagination is set, one of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset'. These events are available regardless of pagination: 'select' | 'group_change'.
#' @param single True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr.
#' @param value The name of the selected row. If this parameter is set, single selection will be allowed (`single` is assumed to be `True`).
#' @return A Table instance.
Expand Down
25 changes: 21 additions & 4 deletions ui/src/progress_table_cell_type.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ const
{input: 0.8888, output: '88.88%'},
{input: 0.88888, output: '88.89%'},
{input: 0.88899, output: '88.90%'},
{input: 0.88999, output: '89.00%'},]
{input: 0.88999, output: '89.00%'},],
progressFloatingPointValues = [
{input: 0.14, output: '14%'},
{input: 0.148, output: '14.8%'},
{input: 0.1414, output: '14.14%'},
{input: 0.141414, output: '14.14%'},
{input: 0.29, output: '29%'},
{input: 0.58, output: '58%'},
{input: 0.592, output: '59.2%'},]

describe('ProgressTableCellType.tsx', () => {

Expand All @@ -42,10 +50,19 @@ describe('ProgressTableCellType.tsx', () => {

it('Renders data-test attr with decimal values', () => {
const { queryByTestId, rerender } = render(<XProgressTableCellType model={progressCellProps} progress={progress} />)
progressValues.map(progressValue => {
progressValues.forEach(progressValue => {
rerender(<XProgressTableCellType model={progressCellProps} progress={progressValue.input} />)
expect(queryByTestId(name)).toBeInTheDocument()
expect(queryByTestId(name)).toHaveTextContent(progressValue.output)
})
})
})
})

it('Handle potential floating-point decimal errors', () => {
const { queryByTestId, rerender } = render(<XProgressTableCellType model={progressCellProps} progress={progress} />)
progressFloatingPointValues.forEach(progressValue => {
rerender(<XProgressTableCellType model={progressCellProps} progress={progressValue.input} />)
expect(queryByTestId(name)).toBeInTheDocument()
expect(queryByTestId(name)).toHaveTextContent(progressValue.output)
})
})
})
2 changes: 1 addition & 1 deletion ui/src/progress_table_cell_type.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const XProgressTableCellType = ({ model: m, progress }: { model: Progress
<div data-test={m.name} className={css.container}>
<ProgressArc thickness={2} color={cssVar(m.color || '$red')} value={progress} />
<Fluent.Stack horizontalAlign='center' verticalAlign='center' className={clas(css.percentContainer, 'wave-s12')}>
<div className={css.percent}>{`${Number.isInteger(progress * 1000) ? (progress * 100) : (progress * 100).toFixed(2)}%`}</div>
<div className={css.percent}>{`${Number.isInteger(progress * 1000) ? (progress * 1000)/10 : (progress * 100).toFixed(2)}%`}</div>
</Fluent.Stack>
</div >
)
128 changes: 128 additions & 0 deletions ui/src/table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,58 @@ describe('Table.tsx', () => {
expect(container.querySelectorAll('.ms-GroupHeader-title')[0]).toHaveTextContent('1/20/1970, 4:58:47 AM(0)')
expect(container.querySelectorAll('.ms-GroupHeader-title')[1]).toHaveTextContent('6/22/2022, 8:47:51 PM(1)')
})

it('Collapses all group by list - fire event', () => {
const { container, getAllByText, getByTestId } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

fireEvent.click(getByTestId('groupby'))
fireEvent.click(getAllByText('Col1')[1]!)

//open all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
emitMock.mockClear()

//collapse all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', [cell21, cell11, cell31])
})

it('Expands all group by list - fire event', () => {
const { container, getAllByText, getByTestId } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

fireEvent.click(getByTestId('groupby'))
fireEvent.click(getAllByText('Col1')[1]!)

//open all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', [cell21, cell11, cell31])
})

it('Collapses group by list - fire event', () => {
const { container, getAllByText, getByTestId } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

fireEvent.click(getByTestId('groupby'))
fireEvent.click(getAllByText('Col1')[1]!)

//open all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
emitMock.mockClear()

//collapse 1st group
fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', [cell21])
})

it('Expands group by list - fire event', () => {
const { container, getAllByText, getByTestId } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

fireEvent.click(getByTestId('groupby'))
fireEvent.click(getAllByText('Col1')[1]!)

//open 1st group
fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', [cell21])
})
})

describe('Groups', () => {
Expand Down Expand Up @@ -1778,6 +1830,82 @@ describe('Table.tsx', () => {
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + items - filteredItem)
})

it('Collapses all groups - fire event', () => {
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

//collapse all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA', 'GroupB'])
expect(emitMock).toHaveBeenCalledTimes(1)
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount)
})

it('Expands all groups - fire event', () => {
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

//collapse all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
emitMock.mockClear()

//open all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA', 'GroupB'])
expect(emitMock).toHaveBeenCalledTimes(1)
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + items)
})

it('Collapses group - fire event', () => {
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA'])
expect(emitMock).toHaveBeenCalledTimes(1)
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + filteredItem)
})

it('Expands group - fire event', () => {
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
emitMock.mockClear()

fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA'])
expect(emitMock).toHaveBeenCalledTimes(1)
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + items - filteredItem)
})

it('Collapses all groups when some already collapsed - fire event', () => {
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)

//collapse all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA', 'GroupB'])
expect(emitMock).toHaveBeenCalledTimes(1)
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount)
emitMock.mockClear()

//open all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA', 'GroupB'])
expect(emitMock).toHaveBeenCalledTimes(1)
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + items)
emitMock.mockClear()

//collapse GroupA
fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA'])
expect(emitMock).toHaveBeenCalledTimes(1)
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + filteredItem)
emitMock.mockClear()

//collapse all groups
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupB'])
expect(emitMock).toHaveBeenCalledTimes(1)
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount)
})

it('Checks if expanded state is preserved after sort', () => {
const { container, getAllByRole } = render(<XTable model={tableProps} />)

Expand Down
28 changes: 24 additions & 4 deletions ui/src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export interface Table {
groups?: TableGroup[]
/** Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`. */
pagination?: TablePagination
/** The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'. */
/** The events to capture on this table. When pagination is set, one of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset'. These events are available regardless of pagination: 'select' | 'group_change'. */
events?: S[]
/** True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr. */
single?: B
Expand Down Expand Up @@ -402,7 +402,8 @@ const
const sortIconName = sortCols && sortCols[props.column.key] ? 'Sort' + sortCols[props.column.key] : 'Sort'

return (
<div style={{ display: 'flex', alignItems: 'center' }}>{props.column.name}
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{props.column.name}</span>
{c.sortable && (
<Fluent.Icon iconName={sortIconName}
className={css.sortingIcon}
Expand Down Expand Up @@ -497,14 +498,33 @@ const
} />
)
}, []),
onToggleCollapseAll = (isAllCollapsed: B) => expandedRefs.current = isAllCollapsed ? {} : null,
onToggleCollapseAll = (isAllCollapsed: B) => {
if (m.events?.includes('group_change')) {
const changedGroups =
isAllCollapsed && expandedRefs.current && Object.keys(expandedRefs.current).length > 0
? Object.keys(expandedRefs.current)
: groups?.map(group => group.name)
wave.emit(m.name, 'group_change', changedGroups)
}
expandedRefs.current = isAllCollapsed ? {} : null
},
onToggleCollapse = ({ key, isCollapsed }: Fluent.IGroup) => {
if (m.events?.includes('group_change')) {
wave.emit(m.name, 'group_change', [key])
}
if (expandedRefs.current) {
isCollapsed
? expandedRefs.current[key] = false
: delete expandedRefs.current[key]
} else {
expandedRefs.current = { [key]: false }
if (groups){
expandedRefs.current = groups?.reduce((acc, { name }) => {
if (name != key){
acc[name] = false
}
return acc
}, {} as { [key: S]: B })
}
}
},
onRenderRow = (props?: Fluent.IDetailsRowProps) => props
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions website/widgets/form/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ q.page['example'] = ui.form_card(box='1 1 3 4', items=[

### With collapsed groups

Groups are shown in a collapsed state by default. With the `collapsed` attribute you can change this behavior.
Groups are shown in a collapsed state by default. With the `collapsed` attribute you can change this behavior. You can also keep track of the collapsed states by registering a `'group_change'` [event](/docs/examples/table-events-group) (populated in `q.events`). This is useful when needing to refresh the table and persist collapsed states.

```py
q.page['example'] = ui.form_card(box='1 1 3 4', items=[
Expand All @@ -421,7 +421,8 @@ q.page['example'] = ui.form_card(box='1 1 3 4', items=[
ui.table_row(name='row4', cells=['Task4', 'Low']),
ui.table_row(name='row5', cells=['Task5', 'Very High'])
])
])
],
events=['group_change'])
])
```

Expand Down

0 comments on commit cc61555

Please sign in to comment.