Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emit event on table group open/collapse #2133 #2402

Merged
merged 2 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions py/examples/table_events_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Table / Events / Group
# Register the `group_change` #event to emit Wave event when group collapses or opens.
# #table #events #groups #background_tasks
# ---
import asyncio
import concurrent.futures
import time
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"},
]
issue_cnt = 5

collapsed_states = {
'Bob': True,
'John': False
}
stop = False
new_issues_label = 'Add Issues'

def add_issues_function(q: Q, loop: asyncio.AbstractEventLoop):
global stop
stop = False
future = None
while not stop:
time.sleep(2)
if not future or future.done():
future = asyncio.ensure_future(update_issues(q), loop=loop)

async def update_issues(q: Q):
global issue_cnt
issue_cnt += 1
if (issue_cnt % 2) == 0:
bobrows.append({"name":"row"+str(issue_cnt), "cell":"Issue"+str(issue_cnt)})
else:
johnrows.append({"name":"row"+str(issue_cnt), "cell":"Issue"+str(issue_cnt)})
update_table_groups(q)
await q.page.save()


@app('/demo')
async def serve(q: Q):
mturoci marked this conversation as resolved.
Show resolved Hide resolved
global issue_cnt, new_issues_label, stop
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}'
elif q.args.add_issues:
if new_issues_label == 'Add Issues':
new_issues_label = 'Stop Adding'
q.page['add_issues'].add_issues.label = new_issues_label
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as pool:
await q.exec(pool, add_issues_function, q, loop)
else:
stop = True
new_issues_label = 'Add Issues'
q.page['add_issues'].add_issues.label = new_issues_label
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['add_issues'] = ui.form_card(box='5 1 2 1', items=[ui.button(name='add_issues', label=new_issues_label)])
q.page['collapse'] = ui.markdown_card(box='5 2 2 1', title='Group change info', content='')

q.client.initialized = True

await q.page.save()

def update_table_groups(q: Q):
q.page['form'].issues_table.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"]
),
]
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', () => {
mturoci marked this conversation as resolved.
Show resolved Hide resolved
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
31 changes: 28 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,39 @@ const
} />
)
}, []),
onToggleCollapseAll = (isAllCollapsed: B) => expandedRefs.current = isAllCollapsed ? {} : null,
onToggleCollapseAll = (isAllCollapsed: B) => {
if (m.events?.includes('group_change')) {
let changingGroups
if (isAllCollapsed){
if (expandedRefs.current && Object.keys(expandedRefs.current).length > 0){
changingGroups = Object.keys(expandedRefs.current)
} else {
changingGroups = groups?.map(group => group.name)
}
} else {
changingGroups = groups?.map(group => group.name)
}
mturoci marked this conversation as resolved.
Show resolved Hide resolved
wave.emit(m.name, 'group_change', changingGroups)
}
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 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.
mturoci marked this conversation as resolved.
Show resolved Hide resolved

```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