Skip to content

Commit 5f83b36

Browse files
authored
feat: Emit event on table group open/collapse #2133 (#2402)
1 parent a40b7d7 commit 5f83b36

File tree

11 files changed

+216
-10
lines changed

11 files changed

+216
-10
lines changed

py/examples/table_events_group.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Table / Events / Group
2+
# Register the `group_change` #event to emit Wave event when group collapses or opens.
3+
# #table #events #groups
4+
# ---
5+
from h2o_wave import main, app, Q, ui
6+
7+
bobrows = [
8+
{"name":"row1", "cell":"Issue1"},
9+
{"name":"row2", "cell":"Issue2"},
10+
]
11+
johnrows = [
12+
{"name":"row3", "cell":"Issue3"},
13+
{"name":"row4", "cell":"Issue4"},
14+
{"name":"row5", "cell":"Issue5"},
15+
]
16+
17+
collapsed_states = {
18+
'Bob': True,
19+
'John': False
20+
}
21+
22+
@app('/demo')
23+
async def serve(q: Q):
24+
if q.events.issues_table and q.events.issues_table.group_change:
25+
# toggle the collapse states
26+
for group in q.events.issues_table.group_change:
27+
collapsed_states[group] = not collapsed_states[group]
28+
q.page['collapse'].content = f'{q.events.issues_table.group_change}'
29+
else:
30+
q.page['form'] = ui.form_card(box='1 1 4 5', items=[
31+
ui.table(
32+
name='issues_table',
33+
columns=[ui.table_column(name='text', label='Issues assigned to')],
34+
groups=[
35+
ui.table_group("Bob",
36+
rows=[ui.table_row(
37+
name=row["name"],
38+
cells=[row["cell"]])
39+
for row in bobrows],
40+
collapsed=collapsed_states["Bob"]
41+
),
42+
ui.table_group("John",
43+
rows=[ui.table_row(
44+
name=row["name"],
45+
cells=[row["cell"]])
46+
for row in johnrows],
47+
collapsed=collapsed_states["John"]
48+
),],
49+
height='400px',
50+
events=['group_change']
51+
)
52+
])
53+
q.page['collapse'] = ui.markdown_card(box='5 1 2 1', title='Group change info', content='')
54+
55+
q.client.initialized = True
56+
57+
await q.page.save()

py/examples/tour.conf

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ table_filter_backend.py
9393
table_download.py
9494
table_groupby.py
9595
table_groups.py
96+
table_events_group.py
9697
table_select_single.py
9798
table_select_multiple.py
9899
table_events_select.py

py/h2o_lightwave/h2o_lightwave/types.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3922,7 +3922,7 @@ def __init__(
39223922
self.pagination = pagination
39233923
"""Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`."""
39243924
self.events = events
3925-
"""The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'."""
3925+
"""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'."""
39263926
self.single = single
39273927
"""True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr."""
39283928
self.value = value

py/h2o_lightwave/h2o_lightwave/ui.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,7 @@ def table(
14781478
tooltip: An optional tooltip message displayed when a user clicks the help icon to the right of the component.
14791479
groups: Creates collapsible / expandable groups of data rows. Mutually exclusive with `rows` attr.
14801480
pagination: Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`.
1481-
events: The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'.
1481+
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'.
14821482
single: True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr.
14831483
value: The name of the selected row. If this parameter is set, single selection will be allowed (`single` is assumed to be `True`).
14841484
Returns:

py/h2o_wave/h2o_wave/types.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3922,7 +3922,7 @@ def __init__(
39223922
self.pagination = pagination
39233923
"""Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`."""
39243924
self.events = events
3925-
"""The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'."""
3925+
"""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'."""
39263926
self.single = single
39273927
"""True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr."""
39283928
self.value = value

py/h2o_wave/h2o_wave/ui.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,7 @@ def table(
14781478
tooltip: An optional tooltip message displayed when a user clicks the help icon to the right of the component.
14791479
groups: Creates collapsible / expandable groups of data rows. Mutually exclusive with `rows` attr.
14801480
pagination: Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`.
1481-
events: The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'.
1481+
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'.
14821482
single: True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr.
14831483
value: The name of the selected row. If this parameter is set, single selection will be allowed (`single` is assumed to be `True`).
14841484
Returns:

r/R/ui.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -1703,7 +1703,7 @@ ui_table_pagination <- function(
17031703
#' @param tooltip An optional tooltip message displayed when a user clicks the help icon to the right of the component.
17041704
#' @param groups Creates collapsible / expandable groups of data rows. Mutually exclusive with `rows` attr.
17051705
#' @param pagination Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`.
1706-
#' @param events The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'.
1706+
#' @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'.
17071707
#' @param single True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr.
17081708
#' @param value The name of the selected row. If this parameter is set, single selection will be allowed (`single` is assumed to be `True`).
17091709
#' @return A Table instance.

ui/src/table.test.tsx

+128
Original file line numberDiff line numberDiff line change
@@ -1676,6 +1676,58 @@ describe('Table.tsx', () => {
16761676
expect(container.querySelectorAll('.ms-GroupHeader-title')[0]).toHaveTextContent('1/20/1970, 4:58:47 AM(0)')
16771677
expect(container.querySelectorAll('.ms-GroupHeader-title')[1]).toHaveTextContent('6/22/2022, 8:47:51 PM(1)')
16781678
})
1679+
1680+
it('Collapses all group by list - fire event', () => {
1681+
const { container, getAllByText, getByTestId } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1682+
1683+
fireEvent.click(getByTestId('groupby'))
1684+
fireEvent.click(getAllByText('Col1')[1]!)
1685+
1686+
//open all groups
1687+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1688+
emitMock.mockClear()
1689+
1690+
//collapse all groups
1691+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1692+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', [cell21, cell11, cell31])
1693+
})
1694+
1695+
it('Expands all group by list - fire event', () => {
1696+
const { container, getAllByText, getByTestId } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1697+
1698+
fireEvent.click(getByTestId('groupby'))
1699+
fireEvent.click(getAllByText('Col1')[1]!)
1700+
1701+
//open all groups
1702+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1703+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', [cell21, cell11, cell31])
1704+
})
1705+
1706+
it('Collapses group by list - fire event', () => {
1707+
const { container, getAllByText, getByTestId } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1708+
1709+
fireEvent.click(getByTestId('groupby'))
1710+
fireEvent.click(getAllByText('Col1')[1]!)
1711+
1712+
//open all groups
1713+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1714+
emitMock.mockClear()
1715+
1716+
//collapse 1st group
1717+
fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
1718+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', [cell21])
1719+
})
1720+
1721+
it('Expands group by list - fire event', () => {
1722+
const { container, getAllByText, getByTestId } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1723+
1724+
fireEvent.click(getByTestId('groupby'))
1725+
fireEvent.click(getAllByText('Col1')[1]!)
1726+
1727+
//open 1st group
1728+
fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
1729+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', [cell21])
1730+
})
16791731
})
16801732

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

1833+
it('Collapses all groups - fire event', () => {
1834+
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1835+
1836+
//collapse all groups
1837+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1838+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA', 'GroupB'])
1839+
expect(emitMock).toHaveBeenCalledTimes(1)
1840+
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount)
1841+
})
1842+
1843+
it('Expands all groups - fire event', () => {
1844+
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1845+
1846+
//collapse all groups
1847+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1848+
emitMock.mockClear()
1849+
1850+
//open all groups
1851+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1852+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA', 'GroupB'])
1853+
expect(emitMock).toHaveBeenCalledTimes(1)
1854+
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + items)
1855+
})
1856+
1857+
it('Collapses group - fire event', () => {
1858+
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1859+
1860+
fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
1861+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA'])
1862+
expect(emitMock).toHaveBeenCalledTimes(1)
1863+
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + filteredItem)
1864+
})
1865+
1866+
it('Expands group - fire event', () => {
1867+
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1868+
1869+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1870+
emitMock.mockClear()
1871+
1872+
fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
1873+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA'])
1874+
expect(emitMock).toHaveBeenCalledTimes(1)
1875+
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + items - filteredItem)
1876+
})
1877+
1878+
it('Collapses all groups when some already collapsed - fire event', () => {
1879+
const { container, getAllByRole } = render(<XTable model={{ ...tableProps, events: ['group_change'] }} />)
1880+
1881+
//collapse all groups
1882+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1883+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA', 'GroupB'])
1884+
expect(emitMock).toHaveBeenCalledTimes(1)
1885+
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount)
1886+
emitMock.mockClear()
1887+
1888+
//open all groups
1889+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1890+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA', 'GroupB'])
1891+
expect(emitMock).toHaveBeenCalledTimes(1)
1892+
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + items)
1893+
emitMock.mockClear()
1894+
1895+
//collapse GroupA
1896+
fireEvent.click(container.querySelector('.ms-GroupHeader-expand')!)
1897+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupA'])
1898+
expect(emitMock).toHaveBeenCalledTimes(1)
1899+
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount + filteredItem)
1900+
emitMock.mockClear()
1901+
1902+
//collapse all groups
1903+
fireEvent.click(container.querySelector('.ms-DetailsHeader-collapseButton')!)
1904+
expect(emitMock).toHaveBeenCalledWith(tableProps.name, 'group_change', ['GroupB'])
1905+
expect(emitMock).toHaveBeenCalledTimes(1)
1906+
expect(getAllByRole('row')).toHaveLength(headerRow + groupHeaderRowsCount)
1907+
})
1908+
17811909
it('Checks if expanded state is preserved after sort', () => {
17821910
const { container, getAllByRole } = render(<XTable model={tableProps} />)
17831911

ui/src/table.tsx

+22-3
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export interface Table {
152152
groups?: TableGroup[]
153153
/** Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`. */
154154
pagination?: TablePagination
155-
/** The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'. */
155+
/** 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'. */
156156
events?: S[]
157157
/** True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr. */
158158
single?: B
@@ -491,14 +491,33 @@ const
491491
} />
492492
)
493493
}, []),
494-
onToggleCollapseAll = (isAllCollapsed: B) => expandedRefs.current = isAllCollapsed ? {} : null,
494+
onToggleCollapseAll = (isAllCollapsed: B) => {
495+
if (m.events?.includes('group_change')) {
496+
const changedGroups =
497+
isAllCollapsed && expandedRefs.current && Object.keys(expandedRefs.current).length > 0
498+
? Object.keys(expandedRefs.current)
499+
: groups?.map(group => group.name)
500+
wave.emit(m.name, 'group_change', changedGroups)
501+
}
502+
expandedRefs.current = isAllCollapsed ? {} : null
503+
},
495504
onToggleCollapse = ({ key, isCollapsed }: Fluent.IGroup) => {
505+
if (m.events?.includes('group_change')) {
506+
wave.emit(m.name, 'group_change', [key])
507+
}
496508
if (expandedRefs.current) {
497509
isCollapsed
498510
? expandedRefs.current[key] = false
499511
: delete expandedRefs.current[key]
500512
} else {
501-
expandedRefs.current = { [key]: false }
513+
if (groups){
514+
expandedRefs.current = groups?.reduce((acc, { name }) => {
515+
if (name != key){
516+
acc[name] = false
517+
}
518+
return acc
519+
}, {} as { [key: S]: B })
520+
}
502521
}
503522
},
504523
onRenderRow = (props?: Fluent.IDetailsRowProps) => props
Loading

website/widgets/form/table.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ q.page['example'] = ui.form_card(box='1 1 3 4', items=[
401401

402402
### With collapsed groups
403403

404-
Groups are shown in a collapsed state by default. With the `collapsed` attribute you can change this behavior.
404+
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.
405405

406406
```py
407407
q.page['example'] = ui.form_card(box='1 1 3 4', items=[
@@ -421,7 +421,8 @@ q.page['example'] = ui.form_card(box='1 1 3 4', items=[
421421
ui.table_row(name='row4', cells=['Task4', 'Low']),
422422
ui.table_row(name='row5', cells=['Task5', 'Very High'])
423423
])
424-
])
424+
],
425+
events=['group_change'])
425426
])
426427
```
427428

0 commit comments

Comments
 (0)