Skip to content

Commit a15d7e8

Browse files
committed
Emit event on table group open/collapse h2oai#2133
1 parent 641f8b2 commit a15d7e8

File tree

11 files changed

+279
-10
lines changed

11 files changed

+279
-10
lines changed

py/examples/table_events_group.py

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Table / Events / Group
2+
# Register the `group_change` #event to emit Wave event when group collapses or opens.
3+
# #table #events #groups #background_tasks
4+
# ---
5+
import asyncio
6+
import concurrent.futures
7+
import time
8+
from h2o_wave import main, app, Q, ui
9+
10+
bobrows = [
11+
{"name":"row1", "cell":"Issue1"},
12+
{"name":"row2", "cell":"Issue2"},
13+
]
14+
johnrows = [
15+
{"name":"row3", "cell":"Issue3"},
16+
{"name":"row4", "cell":"Issue4"},
17+
{"name":"row5", "cell":"Issue5"},
18+
]
19+
issue_cnt = 5
20+
21+
collapsed_states = {
22+
'Bob': True,
23+
'John': False
24+
}
25+
stop = False
26+
new_issues_label = 'Add Issues'
27+
28+
def add_issues_function(q: Q, loop: asyncio.AbstractEventLoop):
29+
global stop
30+
stop = False
31+
future = None
32+
while not stop:
33+
time.sleep(2)
34+
if not future or future.done():
35+
future = asyncio.ensure_future(update_issues(q), loop=loop)
36+
37+
async def update_issues(q: Q):
38+
global issue_cnt
39+
issue_cnt += 1
40+
if (issue_cnt % 2) == 0:
41+
bobrows.append({"name":"row"+str(issue_cnt), "cell":"Issue"+str(issue_cnt)})
42+
else:
43+
johnrows.append({"name":"row"+str(issue_cnt), "cell":"Issue"+str(issue_cnt)})
44+
update_table_groups(q)
45+
await q.page.save()
46+
47+
48+
@app('/demo')
49+
async def serve(q: Q):
50+
global issue_cnt, new_issues_label, stop
51+
if q.events.issues_table and q.events.issues_table.group_change:
52+
# toggle the collapse states
53+
for group in q.events.issues_table.group_change:
54+
collapsed_states[group] = not collapsed_states[group]
55+
q.page['collapse'].content = f'{q.events.issues_table.group_change}'
56+
elif q.args.add_issues:
57+
if new_issues_label == 'Add Issues':
58+
new_issues_label = 'Stop Adding'
59+
q.page['add_issues'].add_issues.label = new_issues_label
60+
loop = asyncio.get_event_loop()
61+
with concurrent.futures.ThreadPoolExecutor() as pool:
62+
await q.exec(pool, add_issues_function, q, loop)
63+
else:
64+
stop = True
65+
new_issues_label = 'Add Issues'
66+
q.page['add_issues'].add_issues.label = new_issues_label
67+
else:
68+
q.page['form'] = ui.form_card(box='1 1 4 5', items=[
69+
ui.table(
70+
name='issues_table',
71+
columns=[ui.table_column(name='text', label='Issues assigned to')],
72+
groups=[
73+
ui.table_group("Bob",
74+
rows=[ui.table_row(
75+
name=row["name"],
76+
cells=[row["cell"]])
77+
for row in bobrows],
78+
collapsed=collapsed_states["Bob"]
79+
),
80+
ui.table_group("John",
81+
rows=[ui.table_row(
82+
name=row["name"],
83+
cells=[row["cell"]])
84+
for row in johnrows],
85+
collapsed=collapsed_states["John"]
86+
),],
87+
height='400px',
88+
events=['group_change']
89+
)
90+
])
91+
q.page['add_issues'] = ui.form_card(box='5 1 2 1', items=[ui.button(name='add_issues', label=new_issues_label)])
92+
q.page['collapse'] = ui.markdown_card(box='5 2 2 1', title='Group change info', content='')
93+
94+
q.client.initialized = True
95+
96+
await q.page.save()
97+
98+
def update_table_groups(q: Q):
99+
q.page['form'].issues_table.groups=[
100+
ui.table_group("Bob",
101+
rows=[ui.table_row(
102+
name=row["name"],
103+
cells=[row["cell"]])
104+
for row in bobrows],
105+
collapsed=collapsed_states["Bob"]
106+
),
107+
ui.table_group("John",
108+
rows=[ui.table_row(
109+
name=row["name"],
110+
cells=[row["cell"]])
111+
for row in johnrows],
112+
collapsed=collapsed_states["John"]
113+
),
114+
]

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

+28-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,39 @@ const
491491
} />
492492
)
493493
}, []),
494-
onToggleCollapseAll = (isAllCollapsed: B) => expandedRefs.current = isAllCollapsed ? {} : null,
494+
onToggleCollapseAll = (isAllCollapsed: B) => {
495+
if (m.events?.includes('group_change')) {
496+
let changingGroups
497+
if (isAllCollapsed){
498+
if (expandedRefs.current && Object.keys(expandedRefs.current).length > 0){
499+
changingGroups = Object.keys(expandedRefs.current)
500+
} else {
501+
changingGroups = groups?.map(group => group.name)
502+
}
503+
} else {
504+
changingGroups = groups?.map(group => group.name)
505+
}
506+
wave.emit(m.name, 'group_change', changingGroups)
507+
}
508+
expandedRefs.current = isAllCollapsed ? {} : null
509+
},
495510
onToggleCollapse = ({ key, isCollapsed }: Fluent.IGroup) => {
511+
if (m.events?.includes('group_change')) {
512+
wave.emit(m.name, 'group_change', [key])
513+
}
496514
if (expandedRefs.current) {
497515
isCollapsed
498516
? expandedRefs.current[key] = false
499517
: delete expandedRefs.current[key]
500518
} else {
501-
expandedRefs.current = { [key]: false }
519+
if (groups){
520+
expandedRefs.current = groups?.reduce((acc, { name }) => {
521+
if (name != key){
522+
acc[name] = false
523+
}
524+
return acc
525+
}, {} as { [key: S]: B })
526+
}
502527
}
503528
},
504529
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 also can 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.
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)