Skip to content

Commit

Permalink
feat: Emit event on table group open/collapse #2133 (#2402)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbranley authored Oct 2, 2024
1 parent a40b7d7 commit 5f83b36
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 10 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
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
25 changes: 22 additions & 3 deletions ui/src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,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 @@ -491,14 +491,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 5f83b36

Please sign in to comment.