Skip to content

Commit cc61555

Browse files
authored
Merge branch 'h2oai:main' into main
2 parents ab7996d + fe7f7bd commit cc61555

File tree

13 files changed

+240
-16
lines changed

13 files changed

+240
-16
lines changed

Diff for: 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()

Diff for: 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

Diff for: 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

Diff for: 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:

Diff for: 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

Diff for: 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:

Diff for: 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.

Diff for: ui/src/progress_table_cell_type.test.tsx

+21-4
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ const
2626
{input: 0.8888, output: '88.88%'},
2727
{input: 0.88888, output: '88.89%'},
2828
{input: 0.88899, output: '88.90%'},
29-
{input: 0.88999, output: '89.00%'},]
29+
{input: 0.88999, output: '89.00%'},],
30+
progressFloatingPointValues = [
31+
{input: 0.14, output: '14%'},
32+
{input: 0.148, output: '14.8%'},
33+
{input: 0.1414, output: '14.14%'},
34+
{input: 0.141414, output: '14.14%'},
35+
{input: 0.29, output: '29%'},
36+
{input: 0.58, output: '58%'},
37+
{input: 0.592, output: '59.2%'},]
3038

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

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

4351
it('Renders data-test attr with decimal values', () => {
4452
const { queryByTestId, rerender } = render(<XProgressTableCellType model={progressCellProps} progress={progress} />)
45-
progressValues.map(progressValue => {
53+
progressValues.forEach(progressValue => {
4654
rerender(<XProgressTableCellType model={progressCellProps} progress={progressValue.input} />)
4755
expect(queryByTestId(name)).toBeInTheDocument()
4856
expect(queryByTestId(name)).toHaveTextContent(progressValue.output)
4957
})
50-
})
51-
})
58+
})
59+
60+
it('Handle potential floating-point decimal errors', () => {
61+
const { queryByTestId, rerender } = render(<XProgressTableCellType model={progressCellProps} progress={progress} />)
62+
progressFloatingPointValues.forEach(progressValue => {
63+
rerender(<XProgressTableCellType model={progressCellProps} progress={progressValue.input} />)
64+
expect(queryByTestId(name)).toBeInTheDocument()
65+
expect(queryByTestId(name)).toHaveTextContent(progressValue.output)
66+
})
67+
})
68+
})

Diff for: ui/src/progress_table_cell_type.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const XProgressTableCellType = ({ model: m, progress }: { model: Progress
5050
<div data-test={m.name} className={css.container}>
5151
<ProgressArc thickness={2} color={cssVar(m.color || '$red')} value={progress} />
5252
<Fluent.Stack horizontalAlign='center' verticalAlign='center' className={clas(css.percentContainer, 'wave-s12')}>
53-
<div className={css.percent}>{`${Number.isInteger(progress * 1000) ? (progress * 100) : (progress * 100).toFixed(2)}%`}</div>
53+
<div className={css.percent}>{`${Number.isInteger(progress * 1000) ? (progress * 1000)/10 : (progress * 100).toFixed(2)}%`}</div>
5454
</Fluent.Stack>
5555
</div >
5656
)

Diff for: 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

Diff for: ui/src/table.tsx

+24-4
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export interface Table {
158158
groups?: TableGroup[]
159159
/** Display a pagination control at the bottom of the table. Set this value using `ui.table_pagination()`. */
160160
pagination?: TablePagination
161-
/** The events to capture on this table when pagination is set. One of 'search' | 'sort' | 'filter' | 'download' | 'page_change' | 'reset' | 'select'. */
161+
/** 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'. */
162162
events?: S[]
163163
/** True to allow only one row to be selected at time. Mutually exclusive with `multiple` attr. */
164164
single?: B
@@ -402,7 +402,8 @@ const
402402
const sortIconName = sortCols && sortCols[props.column.key] ? 'Sort' + sortCols[props.column.key] : 'Sort'
403403

404404
return (
405-
<div style={{ display: 'flex', alignItems: 'center' }}>{props.column.name}
405+
<div style={{ display: 'flex', alignItems: 'center' }}>
406+
<span style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{props.column.name}</span>
406407
{c.sortable && (
407408
<Fluent.Icon iconName={sortIconName}
408409
className={css.sortingIcon}
@@ -497,14 +498,33 @@ const
497498
} />
498499
)
499500
}, []),
500-
onToggleCollapseAll = (isAllCollapsed: B) => expandedRefs.current = isAllCollapsed ? {} : null,
501+
onToggleCollapseAll = (isAllCollapsed: B) => {
502+
if (m.events?.includes('group_change')) {
503+
const changedGroups =
504+
isAllCollapsed && expandedRefs.current && Object.keys(expandedRefs.current).length > 0
505+
? Object.keys(expandedRefs.current)
506+
: groups?.map(group => group.name)
507+
wave.emit(m.name, 'group_change', changedGroups)
508+
}
509+
expandedRefs.current = isAllCollapsed ? {} : null
510+
},
501511
onToggleCollapse = ({ key, isCollapsed }: Fluent.IGroup) => {
512+
if (m.events?.includes('group_change')) {
513+
wave.emit(m.name, 'group_change', [key])
514+
}
502515
if (expandedRefs.current) {
503516
isCollapsed
504517
? expandedRefs.current[key] = false
505518
: delete expandedRefs.current[key]
506519
} else {
507-
expandedRefs.current = { [key]: false }
520+
if (groups){
521+
expandedRefs.current = groups?.reduce((acc, { name }) => {
522+
if (name != key){
523+
acc[name] = false
524+
}
525+
return acc
526+
}, {} as { [key: S]: B })
527+
}
508528
}
509529
},
510530
onRenderRow = (props?: Fluent.IDetailsRowProps) => props

Diff for: website/docs/examples/assets/table-events-group.png

26.3 KB
Loading

Diff for: 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)