Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 11 additions & 5 deletions .changeset/quiet-footers-rise.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@
"@tailor-platform/app-shell": minor
---

Add bulk actions to `DataTable`. Pass `selectionActions` to `useDataTable` and, while rows are selected, `DataTable.Footer` becomes a bulk-action bar: the selection count, your actions, and a Clear button, with pagination kept alongside. Give an action `canApply` to scope it to the selected rows it can act on — the bar shows that count, disables the action at zero, and hands only those rows to `onClick`. Return the promise from `onClick` and the bar waits for it: actions are disabled with a spinner meanwhile, and the selection clears once it resolves (set `keepSelection` for actions such as an export). An action that confirms first can hand its later request back through the `run` helper — `run(deleteRows(rows))` from the dialog's confirm button — and get the same handling. Clear is disabled while a request is pending.
Add ID-based bulk actions to `DataTable`. Pass `selectionActions` to `useDataTable` and, while rows are selected, `DataTable.Footer` becomes a bulk-action bar: the selection count, your actions, and a Clear button, with pagination kept alongside. `onClick` receives a click-time copy of all selected ids, including ids whose rows have not been loaded. Consumers own eligibility checks, required fetching, confirmation UI, and execution; selection actions do not have row-based `canApply` or automatic eligible counts/disabled states.

Return the promise from `onClick` for pending-state tracking: actions and Clear are disabled with a spinner meanwhile. A confirmation dialog can hand a later request back through `helpers.run`. Neither success nor rejection changes selection, and there is no `keepSelection` option. Use controlled `rowSelection` to remove only ids actually changed from the current selection, preserving ineligible records and newer selections. The explicit `clearSelection` helper remains available for intentional clearing of every page.

```tsx
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const table = useDataTable({
columns,
data,
control,
rowSelection: { selectedIds, onChange: setSelectedIds },
selectionActions: [
{
id: "activate",
label: "Activate",
canApply: (vendor) => vendor.status === "inactive",
onClick: (vendors) => activate(vendors),
onClick: async (ids) => {
const activatedIds = new Set(await activate(ids));
setSelectedIds((current) => current.filter((id) => !activatedIds.has(id)));
},
},
],
});
```

`RowAction` and `SelectionAction` now share a base type, `DataTableAction` (`id`, `label`, `icon`, `variant`, `canApply`), so one definition can be spread into both `rowActions` and `selectionActions`. `RowAction` gains `canApply`; its `isDisabled`, which reads the other way round, is deprecated but still honoured.
`RowAction<TRow>` and the non-generic `SelectionAction` share the non-generic presentation base `DataTableAction` (`id`, `label`, `icon`, `variant`). `RowAction` gains row-based `canApply`; its `isDisabled`, which reads the other way round, is deprecated but still honoured. Bulk eligibility belongs in the consumer's ID-based workflow.

Selection now also remembers the rows it holds across pages (`selectedRows`), and a non-empty `selectionActions` enables selection on its own. The first three actions render as buttons and the rest collapse into a "More actions" menu. On a page-scrolling table the bar sticks to the bottom of the viewport until the table's end scrolls into view.
Selection now also remembers loaded selected rows across pages (`selectedRows`), a partial cache rather than the bulk-operation payload. A non-empty `selectionActions` enables selection on its own. The first three actions render as buttons and the rest collapse into a "More actions" menu. On a page-scrolling table the bar sticks to the bottom of the viewport until the table's end scrolls into view.

**Behavior change:** the header checkbox is now page-scoped in both directions. Checking it adds the current page's rows to the selection instead of replacing it, and unchecking it removes only the current page's rows instead of clearing every page. `clearSelection` still empties everything (and is now a no-op when nothing is selected), and the new `deselectAllRows` is the page-scoped counterpart of `selectAllRows`.

Expand Down
17 changes: 17 additions & 0 deletions decisions/data-table-selection-footer-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
> Status: **Decided — built into DataTable as `selectionActions` (Option A below). Implemented by PR #496.**
> Scope: where multi-select bulk actions render, and the `useDataTable` API that declares them. Ticket: tailor-inc/platform-planning#1738.

## Operation contract update after #604

The footer placement and interaction design below remain unchanged. The original
row-based bulk-operation contract is superseded by the
[follow-up proposal on #604](https://github.com/tailor-platform/app-shell/pull/604#issuecomment-6160412655):

- `SelectionAction.onClick` receives a click-time copy of every selected ID,
including IDs whose rows have never loaded.
- Selection actions have no `canApply`, automatic eligible counts, or
`keepSelection`. Row actions retain their row-based `canApply` predicate.
- Neither returned promises nor `helpers.run` clear selection. They only track
pending work; consumers own eligibility, confirmation, execution, and selection
updates based on actual results. `clearSelection` remains an explicit command.
- `selectedRows` remains a partial cache, not a complete operation target.

The original rationale is preserved below as historical context.

## Context

Multi-select already worked: `onSelectionChange` added the checkbox column, and the footer read "N of M row(s) selected". AppShell had no settled answer for **where the bulk actions go** once rows are selected. The `interaction/multi-select` pattern prescribed a floating bottom bar built on raw `Table.Root` with native checkboxes, which predated DataTable's own selection.
Expand Down
16 changes: 8 additions & 8 deletions docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,10 @@
"withURLCollectionState"
],
"hashes": {
"typeSurface": "d8d9fe5ef1459ad6",
"outline": "30e9c33a9fddd652",
"typeSurface": "0e0e081a20ed4f14",
"outline": "598fad95e7fba24c",
"snapshot": null,
"outputMd": "a0ec09ea656fcc7a",
"outputMd": "c0177dbc181bf641",
"examples": null
}
},
Expand Down Expand Up @@ -948,10 +948,10 @@
"symbols": [],
"hashes": {
"typeSurface": null,
"outline": "8d704967c8c882fd",
"outline": "a3d8ae4dca5c6eeb",
"snapshot": null,
"outputMd": "770b33ada6b9317f",
"examples": "04c7dc51378c04c4"
"outputMd": "d43dad72f2584803",
"examples": "0ecdf5402f6e073c"
}
},
"interaction-toast": {
Expand Down Expand Up @@ -1711,7 +1711,7 @@
"packages/core/skills/app-shell-patterns/references/components/combobox.md": "dfb775c7c4307409",
"packages/core/skills/app-shell-patterns/references/components/command-palette.md": "7e5bc675a593791a",
"packages/core/skills/app-shell-patterns/references/components/csv-importer.md": "53d3c795ca21b9dc",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "a0ec09ea656fcc7a",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "c0177dbc181bf641",
"packages/core/skills/app-shell-patterns/references/components/date-picker.md": "bc69f14829004f9d",
"packages/core/skills/app-shell-patterns/references/components/default-header.md": "375e55f4b12fa2be",
"packages/core/skills/app-shell-patterns/references/components/default-sidebar.md": "3422388a0cc78571",
Expand Down Expand Up @@ -1771,7 +1771,7 @@
"packages/core/skills/app-shell-patterns/references/patterns/form-single-page.md": "10a0f4d91a120752",
"packages/core/skills/app-shell-patterns/references/patterns/form-wizard.md": "851afb4672da6673",
"packages/core/skills/app-shell-patterns/references/patterns/interaction-confirm.md": "2db70f423c6fdf97",
"packages/core/skills/app-shell-patterns/references/patterns/interaction-multi-select.md": "770b33ada6b9317f",
"packages/core/skills/app-shell-patterns/references/patterns/interaction-multi-select.md": "d43dad72f2584803",
"packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md": "f68bdab855730f48",
"packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md": "916baa3cd2b9cbf1",
"packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "717ef4e7ae835aff",
Expand Down
Loading
Loading