From f1cfb7fb68db6a581b6d3b1593f22425d99fe85c Mon Sep 17 00:00:00 2001 From: vpolessky Date: Thu, 3 Sep 2026 18:44:29 +0200 Subject: [PATCH 1/3] chore(design-system): add cleanup sb table [AR-78174] --- .../__snapshots__/ds-table.docs.snap | 7248 +++++++++++++++++ .../ds-table-filters-panel.browser.test.tsx | 439 + .../stories/ds-table-active-row.stories.tsx | 132 +- .../ds-table-column-filters.stories.tsx | 10 +- .../stories/ds-table-columns.stories.tsx | 28 +- .../stories/ds-table-expansion.stories.tsx | 94 +- .../stories/ds-table-loading.stories.tsx | 18 +- .../stories/ds-table-row-actions.stories.tsx | 2 +- .../stories/ds-table-search.stories.tsx | 43 +- .../stories/ds-table-selection.stories.tsx | 113 +- .../stories/ds-table-virtualized.stories.tsx | 315 +- .../stories/ds-table.stories.module.scss | 287 +- .../stories/filters-panel.stories.tsx | 195 +- .../storybook/docs-snippets.docs.test.ts | 1 + 14 files changed, 8148 insertions(+), 777 deletions(-) create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/ds-table-filters-panel.browser.test.tsx diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table.docs.snap new file mode 100644 index 000000000..70e956d46 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table.docs.snap @@ -0,0 +1,7248 @@ +# DsTable docs snippets + +## Default + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const Default = () => } + onRowClick={fn()} + data={defaultData} />; + +## With Controls + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + controls={RED version V2Import REDExport REDActions} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const WithControls = () => } + onRowClick={fn()} + data={defaultData} + controls={( + + RED version V2 + + + + Import RED + + + Export RED + + Actions + + )} />; + +## Empty State + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const EmptyState = () => } + onRowClick={fn()} + data={[]} />; + +## No Border + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const NoBorder = () => } + onRowClick={fn()} + data={defaultData} />; + +## Horizontal Scroll + +### Show code + {}, + header: 'First Name', + size: 250 + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name', + size: 250 + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age (years)', + size: 200 + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Number of Visits', + size: 250 + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Relationship Status', + size: 250 + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress', + size: 250 + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const HorizontalScroll = () => } + onRowClick={fn()} + data={defaultData} />; + +# DsTable docs snippets + +## Active Row + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const ActiveRow = () => } + onRowClick={fn()} + activeRowId="3" />; + +## Active Row with Drawer + +### Show code +{ + name: 'Active Row with Drawer', + args: { + data: defaultData.slice(0, 10) + }, + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [selectedPerson, setSelectedPerson] = useState(null); + const activeRowId = selectedPerson?.id; + const isDrawerOpen = !!activeRowId; + const handleRowClick = (person: Person) => { + const isSameRow = activeRowId === person.id; + setSelectedPerson(isSameRow ? null : person); + }; + return <> + + + { + if (!open) { + setSelectedPerson(null); + } + }} columns={4} position="end"> + {selectedPerson &&
+ + + Person Details + setSelectedPerson(null)} /> + + + + + + Full Name + + + {selectedPerson.firstName} {selectedPerson.lastName} + + + + + + Age + + {selectedPerson.age} years old + + + + + Visits + + {selectedPerson.visits} visits + + + + + Status + + + {selectedPerson.status.charAt(0).toUpperCase() + selectedPerson.status.slice(1)} + + + + + + Profile Progress + + + + + +
} +
+ ; + } +} + +### MCP manifest +const WithDrawerAndActiveRow = () => { + const [selectedPerson, setSelectedPerson] = useState(null); + + const activeRowId = selectedPerson?.id; + const isDrawerOpen = !!activeRowId; + + const handleRowClick = (person: Person) => { + const isSameRow = activeRowId === person.id; + + setSelectedPerson(isSameRow ? null : person); + }; + + return ( + <> + } + activeRowId={activeRowId} + onRowClick={handleRowClick} /> + { + if (!open) { + setSelectedPerson(null); + } + }} + columns={4} + position="end"> + {selectedPerson && ( +
+ + + Person Details + setSelectedPerson(null)} + /> + + + + + + Full Name + + + {selectedPerson.firstName} {selectedPerson.lastName} + + + + + + Age + + {selectedPerson.age} years old + + + + + Visits + + {selectedPerson.visits} visits + + + + + Status + + + {selectedPerson.status.charAt(0).toUpperCase() + selectedPerson.status.slice(1)} + + + + + + Profile Progress + + + + + +
+ )} +
+ + ); +}; + +# DsTable docs snippets + +## Default + +### Show code + {}, + header: 'First Name', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + } + ], + header: 'Identity', + id: 'identity', + meta: { + group: { + collapsible: true, + defaultCollapsed: false + } + } + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ], + header: 'Activity', + id: 'activity', + meta: { + group: { + collapsible: true + } + } + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + selectable + stickyHeader +/> + +### MCP manifest +const Default = () => } />; + +## Initially Collapsed + +### Show code + {}, + header: 'First Name', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + } + ], + header: 'Identity', + id: 'identity', + meta: { + group: { + collapsible: true, + defaultCollapsed: true + } + } + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ], + header: 'Activity', + id: 'activity', + meta: { + group: { + collapsible: true + } + } + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + selectable + stickyHeader +/> + +### MCP manifest +const InitiallyCollapsed = () => } />; + +## Controlled + +### Show code + {}, + header: 'First Name', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + } + ], + header: 'Identity', + id: 'identity', + meta: { + group: { + collapsible: true, + defaultCollapsed: false + } + } + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ], + header: 'Activity', + id: 'activity', + meta: { + group: { + collapsible: true + } + } + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onCollapsedColumnGroupsChange={() => {}} + selectable + stickyHeader +/> + +### MCP manifest +const Controlled = () => { + const [collapsed, setCollapsed] = useState(['activity']); + + return ( + } + collapsedColumnGroups={collapsed} + onCollapsedColumnGroupsChange={setCollapsed} /> + ); +}; + +## With Controls + +### Show code + {}, + header: 'First Name', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + } + ], + header: 'Identity', + id: 'identity', + meta: { + group: { + collapsible: true, + defaultCollapsed: false + } + } + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ], + header: 'Activity', + id: 'activity', + meta: { + group: { + collapsible: true + } + } + } + ]} + controls={RED version V2Import REDExport REDActions} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + selectable + stickyHeader +/> + +### MCP manifest +const WithControls = () => } + controls={( + + RED version V2 + + + + Import RED + + + Export RED + + Actions + + )} />; + +# DsTable docs snippets + +## Progress as Infographic + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const WithProgressInfographic = () => { + if ('accessorKey' in col && col.accessorKey === 'progress') { + return { + ...col, + header: 'Profile Progress', + cell: (info) => , + }; + } else if ('accessorKey' in col && col.accessorKey === 'status') { + return { + ...col, + header: 'Status', + cell: (info) => ( + + {info.getValue() as string} + + ), + }; + } + return col; + })} + data={defaultData} + stickyHeader + bordered + fullWidth + expandable={false} + emptyState={} + onRowClick={fn()} />; + +## Column Hiding + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const columnsToToggle = [{ + id: 'age', + label: 'Age' + }, { + id: 'visits', + label: 'Visits' + }, { + id: 'status', + label: 'Status' + }, { + id: 'progress', + label: 'Profile Progress' + }]; + const [columnVisibility, setColumnVisibility] = useState({ + age: true, + visits: true, + status: true, + progress: true + }); + const toggleColumn = (columnId: string) => { + setColumnVisibility(prev => ({ + ...prev, + [columnId]: !prev[columnId] + })); + }; + return + + {columnsToToggle.map(column => toggleColumn(column.id)} />)} + + + + ; + } +} + +### MCP manifest +const ColumnHiding = () => { + const columnsToToggle = [ + { id: 'age', label: 'Age' }, + { id: 'visits', label: 'Visits' }, + { id: 'status', label: 'Status' }, + { id: 'progress', label: 'Profile Progress' }, + ]; + const [columnVisibility, setColumnVisibility] = useState({ + age: true, + visits: true, + status: true, + progress: true, + }); + + const toggleColumn = (columnId: string) => { + setColumnVisibility((prev) => ({ + ...prev, + [columnId]: !prev[columnId], + })); + }; + + return ( + + + {columnsToToggle.map((column) => ( + toggleColumn(column.id)} + /> + ))} + + } + onRowClick={fn()} + columnVisibility={columnVisibility} + onColumnVisibilityChange={setColumnVisibility} /> + + ); +}; + +# DsTable docs snippets + +## Editable + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data, setData] = useState(defaultData); + const statusOptions = [{ + label: 'Single', + value: 'single' + }, { + label: 'Relationship', + value: 'relationship' + }, { + label: 'Complicated', + value: 'complicated' + }]; + const statusLabels: Record = { + single: 'Single', + relationship: 'Relationship', + complicated: 'Complicated' + }; + const progressPresets = [25, 50, 75, 100]; + const ProgressEditor = ({ + cellContext + }: { + cellContext: CellContext; + }) => { + const { + value, + setValue, + error + } = useCellEditor({ + cellContext + }); + return +
+ {progressPresets.map(preset => setValue(preset)} />)} +
+ +
; + }; + const columns: ColumnDef[] = [{ + accessorKey: 'id', + header: 'ID', + size: 60, + cell: info => {info.getValue() as string} + }, { + accessorKey: 'firstName', + header: 'First Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'lastName', + header: 'Last Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'age', + header: 'Age', + size: 100, + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'visits', + header: 'Visits', + size: 100, + cell: info => info.getValue(), + editCell: (info: CellContext) => , + editDisabled: (info: CellContext) => { + if (info.row.original.status === 'complicated') { + return { + reason: 'Visits are locked while the status is “Complicated”.' + }; + } + if (info.row.original.age >= 40) { + return true; + } + return false; + } + }, { + accessorKey: 'status', + header: 'Status', + size: 160, + cell: info => statusLabels[info.getValue() as Status], + editCell: (info: CellContext) => + }, { + accessorKey: 'progress', + header: 'Profile Progress', + cell: info => {`${String(info.getValue())}%`}, + editCell: (info: CellContext) => + }]; + return { + setData(rows => rows.map(person => person.id === row.id ? { + ...person, + [columnId]: value + } : person)); + }} />; + } +} + +### MCP manifest +const Editable = () => { + const [data, setData] = useState(defaultData); + + const statusOptions = [ + { label: 'Single', value: 'single' }, + { label: 'Relationship', value: 'relationship' }, + { label: 'Complicated', value: 'complicated' }, + ]; + + const statusLabels: Record = { + single: 'Single', + relationship: 'Relationship', + complicated: 'Complicated', + }; + + const progressPresets = [25, 50, 75, 100]; + + const ProgressEditor = ({ cellContext }: { cellContext: CellContext }) => { + const { value, setValue, error } = useCellEditor({ cellContext }); + + return ( + +
+ {progressPresets.map((preset) => ( + setValue(preset)} + /> + ))} +
+ +
+ ); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + size: 60, + cell: (info) => {info.getValue() as string}, + }, + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'age', + header: 'Age', + size: 100, + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'visits', + header: 'Visits', + size: 100, + cell: (info) => info.getValue(), + editCell: (info: CellContext) => , + editDisabled: (info: CellContext) => { + if (info.row.original.status === 'complicated') { + return { reason: 'Visits are locked while the status is “Complicated”.' }; + } + if (info.row.original.age >= 40) { + return true; + } + return false; + }, + }, + { + accessorKey: 'status', + header: 'Status', + size: 160, + cell: (info) => statusLabels[info.getValue() as Status], + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + cell: (info) => {`${String(info.getValue())}%`}, + editCell: (info: CellContext) => , + }, + ]; + + return ( + } + data={data} + columns={columns} + selectable + onRowClick={fn()} + primaryRowActions={[{ icon: 'delete_outline', label: 'Delete', onClick: fn() }]} + secondaryRowActions={[{ icon: 'info', label: 'Details', onClick: fn() }]} + onCellEdit={(row, columnId, value) => { + setData((rows) => + rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), + ); + }} /> + ); +}; + +## Live Validation + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data, setData] = useState(defaultData); + const personSchema = z.object({ + firstName: z.string().trim().min(1, 'First name is required').max(50, 'Max 50 characters'), + lastName: z.string().trim().min(1, 'Last name is required').max(50, 'Max 50 characters') + }); + const validateField = (columnId: string, value: unknown): string | null => { + const shape: Record = personSchema.shape; + const fieldSchema = shape[columnId]; + if (!fieldSchema) { + return null; + } + const result = fieldSchema.safeParse(value); + return result.success ? null : result.error.issues[0]?.message ?? null; + }; + const columns: ColumnDef[] = [{ + accessorKey: 'id', + header: 'ID', + size: 60, + cell: info => {info.getValue() as string} + }, { + accessorKey: 'firstName', + header: 'First Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'lastName', + header: 'Last Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }]; + return validateField(columnId, value)} onCellEdit={(row, columnId, value) => { + setData(rows => rows.map(person => person.id === row.id ? { + ...person, + [columnId]: value + } : person)); + }} />; + } +} + +### MCP manifest +const LiveValidation = () => { + const [data, setData] = useState(defaultData); + + const personSchema = z.object({ + firstName: z.string().trim().min(1, 'First name is required').max(50, 'Max 50 characters'), + lastName: z.string().trim().min(1, 'Last name is required').max(50, 'Max 50 characters'), + }); + + const validateField = (columnId: string, value: unknown): string | null => { + const shape: Record = personSchema.shape; + const fieldSchema = shape[columnId]; + if (!fieldSchema) { + return null; + } + const result = fieldSchema.safeParse(value); + return result.success ? null : (result.error.issues[0]?.message ?? null); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + size: 60, + cell: (info) => {info.getValue() as string}, + }, + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + ]; + + return ( + } + data={data} + columns={columns} + onCellValidate={(_row, columnId, value) => validateField(columnId, value)} + onCellEdit={(row, columnId, value) => { + setData((rows) => + rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), + ); + }} /> + ); +}; + +## Validate on Async Save + +### Show code +{ + name: 'Validate on Async Save', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data, setData] = useState(defaultData); + const saveFirstName = (value: string, signal: AbortSignal): Promise => new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + const trimmed = value.trim(); + if (trimmed.length === 0) { + resolve('First name is required'); + return; + } + if (trimmed.toLowerCase() === 'taken') { + resolve('This name is already taken'); + return; + } + resolve(null); + }, 900); + signal.addEventListener('abort', () => { + clearTimeout(timeout); + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + const columns: ColumnDef[] = [{ + accessorKey: 'id', + header: 'ID', + size: 60, + cell: info => {info.getValue() as string} + }, { + accessorKey: 'firstName', + header: 'First Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'lastName', + header: 'Last Name', + cell: info => info.getValue() + }]; + return { + const error = await saveFirstName(value as string, signal); + if (error !== null) { + return error; + } + setData(rows => rows.map(person => person.id === row.id ? { + ...person, + [columnId]: value + } : person)); + }} />; + } +} + +### MCP manifest +const ValidateOnAsyncSave = () => { + const [data, setData] = useState(defaultData); + + const saveFirstName = (value: string, signal: AbortSignal): Promise => + new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + const trimmed = value.trim(); + if (trimmed.length === 0) { + resolve('First name is required'); + return; + } + if (trimmed.toLowerCase() === 'taken') { + resolve('This name is already taken'); + return; + } + resolve(null); + }, 900); + + signal.addEventListener('abort', () => { + clearTimeout(timeout); + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + size: 60, + cell: (info) => {info.getValue() as string}, + }, + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + }, + ]; + + return ( + } + data={data} + columns={columns} + onCellEdit={async (row, columnId, value, signal) => { + const error = await saveFirstName(value as string, signal); + if (error !== null) { + return error; + } + setData((rows) => + rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), + ); + }} /> + ); +}; + +# DsTable docs snippets + +## Expandable + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + } + ]} + emptyState={} + expandable={() => {}} + fullWidth + onRowClick={() => {}} + renderExpandedRow={() => {}} + stickyHeader +/> + +### MCP manifest +const Expandable = () => row.firstName !== 'Tanner'} + emptyState={} + onRowClick={fn()} + renderExpandedRow={(row) => ( + + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + + + + )} />; + +## Custom Expander Column Width + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + } + ]} + emptyState={} + expandable + expandableColumnWidth={48} + fullWidth + onRowClick={() => {}} + renderExpandedRow={() => {}} + stickyHeader +/> + +### MCP manifest +const CustomExpanderColumnWidth = () => } + onRowClick={fn()} + expandableColumnWidth={48} + renderExpandedRow={(row) => ( + + Details for {row.firstName} + + )} />; + +## Programmatic Expansion + +### Show code +{ + args: { + data: defaultData.slice(0, 5), + expandable: row => row.firstName !== 'Tanner', + renderExpandedRow: row => + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + }, + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const tableRef = useRef>(null); + const [expandedRows, setExpandedRows] = useState([]); + const expandRow = (rowId: string) => { + tableRef.current?.expandRow(rowId); + setExpandedRows(prev => prev.includes(rowId) ? prev : [...prev, rowId]); + }; + const expandAllRows = () => { + tableRef.current?.expandAllRows(); + const expandableRowIds = defaultData.slice(0, 5).filter(row => row.firstName !== 'Tanner').map(row => row.id); + setExpandedRows(expandableRowIds); + }; + const collapseAllRows = () => { + tableRef.current?.collapseAllRows(); + setExpandedRows([]); + }; + const expandFirstThreeRows = () => { + const firstThreeIds = ['2', '3', '4']; + tableRef.current?.expandRows(firstThreeIds); + setExpandedRows(firstThreeIds); + }; + return + + Expanded rows: {expandedRows.length > 0 ? expandedRows.join(', ') : 'None'} + + + + expandRow('2')}> + Expand Kevin + + expandRow('3')}> + Expand John + + expandRow('4')}> + Expand Jane + + + Expand All + + + Collapse All + + + Expand First 3 Expandable + + + + + ; + } +} + +### MCP manifest +const ProgrammaticExpansion = () => { + const tableRef = useRef>(null); + const [expandedRows, setExpandedRows] = useState([]); + + const expandRow = (rowId: string) => { + tableRef.current?.expandRow(rowId); + setExpandedRows((prev) => (prev.includes(rowId) ? prev : [...prev, rowId])); + }; + + const expandAllRows = () => { + tableRef.current?.expandAllRows(); + const expandableRowIds = defaultData + .slice(0, 5) + .filter((row) => row.firstName !== 'Tanner') + .map((row) => row.id); + setExpandedRows(expandableRowIds); + }; + + const collapseAllRows = () => { + tableRef.current?.collapseAllRows(); + setExpandedRows([]); + }; + + const expandFirstThreeRows = () => { + const firstThreeIds = ['2', '3', '4']; + tableRef.current?.expandRows(firstThreeIds); + setExpandedRows(firstThreeIds); + }; + + return ( + + Expanded rows: {expandedRows.length > 0 ? expandedRows.join(', ') : 'None'} + + + expandRow('2')}>Expand Kevin + + expandRow('3')}>Expand John + + expandRow('4')}>Expand Jane + + Expand All + + Collapse All + + Expand First 3 Expandable + + + row.firstName !== 'Tanner'} + emptyState={} + onRowClick={fn()} + renderExpandedRow={(row) => ( + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + )} + ref={tableRef} /> + + ); +}; + +# DsTable docs snippets + +## Per-Column — Popover + +### Show code +import type { ColumnDef } from '@tanstack/react-table'; +import { + DsTable, + DsTagFilter, + createCheckboxFilterAdapter, + useTableFilters, +} from '@drivenets/design-system'; + +type DeviceRow = { + id: string; + name: string; + type: 'PP-LGX' | 'ME10' | 'OLT'; + site: string; + status: 'active' | 'warning' | 'failed'; +}; + +const typeOptions = [ + { value: 'PP-LGX' as const, label: 'PP-LGX' }, + { value: 'ME10' as const, label: 'ME10' }, + { value: 'OLT' as const, label: 'OLT' }, +]; + +const siteOptions = [ + { value: 'NYC-DC1', label: 'NYC-DC1' }, + { value: 'LON-DC2', label: 'LON-DC2' }, + { value: 'SFO-DC3', label: 'SFO-DC3' }, +]; + +const typeFilter = createCheckboxFilterAdapter({ + id: 'type', + label: 'Type', + items: typeOptions, + searchable: true, + selectAll: true, +}); + +const siteFilter = createCheckboxFilterAdapter({ + id: 'site', + label: 'Site', + items: siteOptions, + searchable: true, + selectAll: true, +}); + +const columns: ColumnDef[] = [ + { id: 'name', accessorKey: 'name', header: 'Name' }, + { id: 'type', accessorKey: 'type', header: 'Type', meta: { filter: { adapterId: 'type' } } }, + { id: 'site', accessorKey: 'site', header: 'Site', meta: { filter: { adapterId: 'site' } } }, + { id: 'status', accessorKey: 'status', header: 'Status' }, +]; + +function DevicesTable({ rows }: { rows: DeviceRow[] }) { + const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ + filterAdapters: [typeFilter, siteFilter], + baseColumns: columns, + }); + + return ( + <> + {filterChips.length > 0 && ( + + )} + + + ); +} + +### MCP manifest +const ColumnFilters = function Render() { + const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ + filterAdapters: adapters, + baseColumns, + }); + + return ( + + {filterChips.length > 0 && ( + + )} + + + + ); +}; + +## Per-Column — Controlled + +### Show code +import { useState } from 'react'; +import type { ColumnDef } from '@tanstack/react-table'; +import { + DsTable, + DsTagFilter, + type CheckboxFilterItem, + type FilterState, + createCheckboxFilterAdapter, + useTableFilters, +} from '@drivenets/design-system'; + +type DeviceRow = { + id: string; + name: string; + type: 'PP-LGX' | 'ME10' | 'OLT'; + site: string; +}; + +const typeFilter = createCheckboxFilterAdapter({ + id: 'type', + label: 'Type', + items: [ + { value: 'PP-LGX', label: 'PP-LGX' }, + { value: 'ME10', label: 'ME10' }, + { value: 'OLT', label: 'OLT' }, + ], + searchable: true, + selectAll: true, +}); + +const columns: ColumnDef[] = [ + { id: 'name', accessorKey: 'name', header: 'Name' }, + { id: 'type', accessorKey: 'type', header: 'Type', meta: { filter: { adapterId: 'type' } } }, + { id: 'site', accessorKey: 'site', header: 'Site' }, +]; + +function ControlledDevicesTable({ rows }: { rows: DeviceRow[] }) { + // Source of truth lives outside the hook (URL, server, parent store, etc.). + const [appliedFilters, setAppliedFilters] = useState>({ + type: [{ value: 'PP-LGX', label: 'PP-LGX' }], + }); + + const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ + filterAdapters: [typeFilter], + baseColumns: columns, + appliedFilters, + onFiltersChange: setAppliedFilters, + }); + + return ( + <> + {filterChips.length > 0 && ( + + )} + + + ); +} + +### MCP manifest +const ControlledColumnFilters = function Render() { + const [appliedFilters, setAppliedFilters] = useState>({ + type: [{ value: 'PP-LGX', label: 'PP-LGX' }], + }); + + const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ + filterAdapters: adapters, + baseColumns, + appliedFilters, + onFiltersChange: setAppliedFilters, + }); + + return ( + + {filterChips.length > 0 && ( + + )} + + + + ); +}; + +# DsTable docs snippets + +## Loading + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + loading + stickyHeader +/> + +### MCP manifest +const Loading = () => } + loading />; + +## Custom Loading Cell + +### Show code + {}, + header: 'First Name', + loadingCell: () => {} + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + loading + stickyHeader +/> + +### MCP manifest +const CustomLoadingCell = () => } + loading />; + +# DsTable docs snippets + +## Default + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + resizableColumns + stickyHeader +/> + +### MCP manifest +const Default = () => } />; + +## Fixed And Fill + +### Show code + {}, + header: 'First Name', + size: 200 + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name', + size: 200 + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age', + size: 120 + }, + { + accessorKey: 'visits', + cell: () => {}, + enableResizing: false, + header: 'Visits', + size: 120 + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + resizableColumns + stickyHeader +/> + +### MCP manifest +const FixedAndFill = () => info.getValue(), size: 200 }, + { accessorKey: 'lastName', header: 'Last Name', cell: (info) => info.getValue(), size: 200 }, + { accessorKey: 'age', header: 'Age', cell: (info) => info.getValue(), size: 120 }, + { + accessorKey: 'visits', + header: 'Visits', + cell: (info) => info.getValue(), + enableResizing: false, + size: 120, + }, + { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, + ]} + resizableColumns + stickyHeader + bordered + fullWidth + emptyState={} />; + +## Grouped Columns + +### Show code + {}, + header: 'First Name', + maxSize: 280, + minSize: 80 + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + } + ], + header: 'Identity', + id: 'identity' + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Progress' + } + ], + header: 'Activity', + id: 'activity' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + resizableColumns + stickyHeader +/> + +### MCP manifest +const GroupedColumns = () => info.getValue(), + minSize: 80, + maxSize: 280, + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + }, + ], + }, + { + id: 'activity', + header: 'Activity', + columns: [ + { accessorKey: 'visits', header: 'Visits', cell: (info) => info.getValue() }, + { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, + { + accessorKey: 'progress', + header: 'Progress', + cell: (info) => `${String(info.getValue())}%`, + }, + ], + }, + ]} + resizableColumns + stickyHeader + bordered + fullWidth + emptyState={} />; + +## Min And Max Size + +### Show code + {}, + header: 'First Name', + maxSize: 280, + minSize: 80, + size: 200 + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name', + minSize: 120, + size: 200 + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age', + maxSize: 180, + size: 120 + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + size: 120 + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + resizableColumns + stickyHeader +/> + +### MCP manifest +const MinAndMaxSize = () => info.getValue(), + size: 200, + minSize: 80, + maxSize: 280, + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + size: 200, + minSize: 120, + }, + { + accessorKey: 'age', + header: 'Age', + cell: (info) => info.getValue(), + size: 120, + maxSize: 180, + }, + { accessorKey: 'visits', header: 'Visits', cell: (info) => info.getValue(), size: 120 }, + { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, + ]} + resizableColumns + stickyHeader + bordered + fullWidth + emptyState={} />; + +## Persisted widths + +### Show code +{ + name: 'Persisted widths', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const STORAGE_KEY = 'storybook.ds-table.resizable.persisted-widths'; + const [tableKey, setTableKey] = useState(0); + let persistedWidths: Record; + try { + persistedWidths = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record; + } catch { + persistedWidths = {}; + } + return + + Persisted widths + + Persist the map from onColumnSizingChange, then pass it back as the columnSizing prop on the next + mount to restore widths — no need to stamp columnDef.size. Columns absent from the map are + measured automatically. Resize a column, refresh the story, and the widths come back. Reset stored + widths remounts from the default layout. + + + + + { + localStorage.removeItem(STORAGE_KEY); + setTableKey(key => key + 1); + }}> + Reset stored widths + + + + { + localStorage.setItem(STORAGE_KEY, JSON.stringify(columnSizing)); + }} /> + ; + } +} + +### MCP manifest +const PersistedWidths = () => { + const STORAGE_KEY = 'storybook.ds-table.resizable.persisted-widths'; + const [tableKey, setTableKey] = useState(0); + + let persistedWidths: Record; + try { + persistedWidths = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record; + } catch { + persistedWidths = {}; + } + + return ( + + + Persisted widths + Persist the map from onColumnSizingChange, then pass it back as the columnSizing prop on the next + mount to restore widths — no need to stamp columnDef.size. Columns absent from the map are + measured automatically. Resize a column, refresh the story, and the widths come back. Reset stored + widths remounts from the default layout. + + + + { + localStorage.removeItem(STORAGE_KEY); + setTableKey((key) => key + 1); + }}>Reset stored widths + + + } + key={tableKey} + columnSizing={persistedWidths} + onColumnSizingChange={(columnSizing) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(columnSizing)); + }} /> + + ); +}; + +# DsTable docs snippets + +## Reorderable + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + } + ]} + emptyState={} + fullWidth + onOrderChange={() => {}} + onRowClick={() => {}} + reorderable + stickyHeader +/> + +### MCP manifest +const Reorderable = () => } + onRowClick={fn()} + reorderable + onOrderChange={fn()} />; + +## Custom Reorder Column Width + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + } + ]} + emptyState={} + fullWidth + onOrderChange={() => {}} + onRowClick={() => {}} + reorderable + reorderableColumnWidth={80} + stickyHeader +/> + +### MCP manifest +const CustomReorderColumnWidth = () => } + onRowClick={fn()} + reorderable + reorderableColumnWidth={80} + onOrderChange={fn()} />; + +## With Row Actions + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: () => {} + }, + { + disabled: () => {}, + icon: 'open_in_new', + label: 'Open in New Window', + onClick: () => {} + } + ]} + secondaryRowActions={[ + { + className: '_destructiveAction_12uvr_138', + disabled: () => {}, + icon: 'delete_outline', + label: 'Delete', + onClick: () => {}, + tooltip: 'Delete this row' + }, + { + icon: 'info', + label: 'Details', + onClick: () => {}, + tooltip: 'Show details' + }, + { + icon: 'call', + label: () => {}, + onClick: () => {} + } + ]} + stickyHeader +/> + +### MCP manifest +const WithRowActions = () => } + onRowClick={fn()} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: editClickHandler, + }, + { + icon: 'open_in_new', + label: 'Open in New Window', + disabled: (data) => data.firstName === 'Tanner', + onClick: openInNewWindowClickHandler, + }, + ]} + secondaryRowActions={[ + { + icon: 'delete_outline', + label: 'Delete', + tooltip: 'Delete this row', + disabled: (data) => data.status === 'single', + className: styles.destructiveAction, + onClick: fn(), + }, + { + icon: 'info', + label: 'Details', + tooltip: 'Show details', + onClick: fn(), + }, + { + icon: 'call', + label: (row) => `Call ${row.firstName}`, + onClick: fn(), + }, + ]} />; + +## With Conditionally Hidden Actions + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: () => {} + }, + { + hidden: () => {}, + icon: 'open_in_new', + label: 'Open in New Window', + onClick: () => {} + } + ]} + secondaryRowActions={[ + { + hidden: () => {}, + icon: 'check_circle', + label: 'Approve', + onClick: () => {} + }, + { + hidden: () => {}, + icon: 'inventory_2', + label: 'Archive', + onClick: () => {} + }, + { + className: '_destructiveAction_12uvr_138', + hidden: () => {}, + icon: 'delete_outline', + label: 'Delete', + onClick: () => {} + }, + { + icon: 'info', + label: 'Details', + onClick: () => {} + } + ]} + stickyHeader +/> + +### MCP manifest +const WithConditionallyHiddenActions = () => } + onRowClick={fn()} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: fn(), + }, + { + icon: 'open_in_new', + label: 'Open in New Window', + // hidden on 'complicated' rows (e.g. cannot open a record in a bad state) + hidden: (data) => data.status === 'complicated', + onClick: fn(), + }, + ]} + secondaryRowActions={[ + { + icon: 'check_circle', + label: 'Approve', + // only shown on 'single' rows (pending approval) + hidden: (data) => data.status !== 'single', + onClick: fn(), + }, + { + icon: 'inventory_2', + label: 'Archive', + // only shown on 'relationship' rows (live records) + hidden: (data) => data.status !== 'relationship', + onClick: fn(), + }, + { + icon: 'delete_outline', + label: 'Delete', + // hidden on 'relationship' rows (cannot delete live records) + hidden: (data) => data.status === 'relationship', + className: styles.destructiveAction, + onClick: fn(), + }, + { + icon: 'info', + label: 'Details', + onClick: fn(), + }, + ]} />; + +## With Conditionally Disabled Actions + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: () => {} + }, + { + disabled: () => {}, + icon: 'open_in_new', + label: 'Open in New Window', + onClick: () => {} + } + ]} + secondaryRowActions={[ + { + className: '_destructiveAction_12uvr_138', + disabled: () => {}, + icon: 'delete_outline', + label: 'Delete', + onClick: () => {}, + tooltip: 'Delete this row' + }, + { + icon: 'info', + label: 'Details', + onClick: () => {}, + tooltip: 'Show details' + } + ]} + stickyHeader +/> + +### MCP manifest +const WithConditionallyDisabledActions = () => } + onRowClick={fn()} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: fn(), + }, + { + icon: 'open_in_new', + label: 'Open in New Window', + // disabled on Tanner's row (item stays visible but greyed out) + disabled: (data) => data.firstName === 'Tanner', + onClick: fn(), + }, + ]} + secondaryRowActions={[ + { + icon: 'delete_outline', + label: 'Delete', + tooltip: 'Delete this row', + // disabled on 'single' rows (destructive action guarded) + disabled: (data) => data.status === 'single', + className: styles.destructiveAction, + onClick: fn(), + }, + { + icon: 'info', + label: 'Details', + tooltip: 'Show details', + onClick: fn(), + }, + ]} />; + +## With Bulk Actions + +### Show code + {} + }, + { + icon: 'folder_open', + label: 'Folder', + onClick: () => {} + }, + { + icon: 'delete_outline', + label: 'Delete', + onClick: () => {} + } + ]} + bordered + columns={[ + { + accessorKey: 'firstName', + cell: () => {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + selectable + stickyHeader +/> + +### MCP manifest +const WithBulkActions = () => } + onRowClick={fn()} + selectable + actions={[ + { + icon: 'alarm', + label: 'Notify', + onClick: fn(), + }, + { + icon: 'folder_open', + label: 'Folder', + onClick: fn(), + }, + { + icon: 'delete_outline', + label: 'Delete', + onClick: fn(), + }, + ]} />; + +# DsTable docs snippets + +## Selectable + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + onSelectionChange={() => {}} + selectable + stickyHeader +/> + +### MCP manifest +const Selectable = () => } + onRowClick={fn()} + selectable + onSelectionChange={fn()} />; + +## Custom Select Column Width + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + onSelectionChange={() => {}} + selectable + selectableColumnWidth={48} + stickyHeader +/> + +### MCP manifest +const CustomSelectColumnWidth = () => } + onRowClick={fn()} + selectable + selectableColumnWidth={48} + onSelectionChange={fn()} />; + +## Programmatic Row Selection + +### Show code +{ + args: { + selectable: true, + showSelectAllCheckbox: false, + stickyHeader: true, + onSelectionChange: fn() + }, + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const tableRef = useRef>(null); + const [selectedRows, setSelectedRows] = useState([]); + const handleSelectionChange = (selection: Record) => { + setSelectedRows(Object.keys(selection)); + }; + return + + Selected rows: {selectedRows.length > 0 ? selectedRows.join(', ') : 'None'} + + + + tableRef.current?.selectRow('1')}> + Select Row 1 + + tableRef.current?.selectRow('2')}> + Select Row 2 + + tableRef.current?.selectRow('3')}> + Select Row 3 + + tableRef.current?.selectAllRows()}> + Select All + + tableRef.current?.deselectAllRows()}> + Deselect All + + tableRef.current?.selectRows(['1', '2', '3'])}> + Select First 3 Rows + + + + + ; + } +} + +### MCP manifest +const ProgrammaticRowSelection = () => { + const tableRef = useRef>(null); + const [selectedRows, setSelectedRows] = useState([]); + + const handleSelectionChange = (selection: Record) => { + setSelectedRows(Object.keys(selection)); + }; + + return ( + + Selected rows: {selectedRows.length > 0 ? selectedRows.join(', ') : 'None'} + + + tableRef.current?.selectRow('1')}>Select Row 1 + + tableRef.current?.selectRow('2')}>Select Row 2 + + tableRef.current?.selectRow('3')}>Select Row 3 + + tableRef.current?.selectAllRows()}>Select All + + tableRef.current?.deselectAllRows()}>Deselect All + + tableRef.current?.selectRows(['1', '2', '3'])}>Select First 3 Rows + + + } + onRowClick={fn()} + selectable + showSelectAllCheckbox={false} + ref={tableRef} + onSelectionChange={handleSelectionChange} /> + + ); +}; + +## Max N Selections + +### Show code +{ + name: 'Max N Selections', + args: { + showSelectAllCheckbox: false, + onSelectionChange: fn() + }, + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [rowSelection, setRowSelection] = useState>({}); + const maxSelections = 2; + const selectedCount = Object.keys(rowSelection).filter(id => rowSelection[id]).length; + const handleSelectionChange = (selection: Record) => { + setRowSelection(selection); + args.onSelectionChange?.(selection); + }; + return + + Selected: {selectedCount} / {maxSelections} + + + rowSelection[rowData.id] || selectedCount < maxSelections} /> + ; + } +} + +### MCP manifest +const MaxSelectionLimit = () => { + const [rowSelection, setRowSelection] = useState>({}); + const maxSelections = 2; + + const selectedCount = Object.keys(rowSelection).filter((id) => rowSelection[id]).length; + + const handleSelectionChange = (selection: Record) => { + setRowSelection(selection); + args.onSelectionChange?.(selection); + }; + + return ( + + Selected: {selectedCount}/ {maxSelections} + + } + onRowClick={fn()} + showSelectAllCheckbox={false} + onSelectionChange={handleSelectionChange} + selectable={(rowData) => rowSelection[rowData.id] || selectedCount < maxSelections} /> + + ); +}; + +# DsTable docs snippets + +## Empty State + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader + virtualized +/> + +### MCP manifest +const EmptyState = () => } + onRowClick={fn()} + virtualized />; + +## Virtualized Selectable Table + +### Show code +{ + name: 'Virtualized Selectable Table', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const pageSize = 10; + const [sorting, setSorting] = useState([]); + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + isLoading + } = useInfiniteQuery({ + queryKey: ['people', sorting], + queryFn: async ({ + pageParam + }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData + }, queryClient); + const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); + const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; + const hasMore = flatData.length < totalRows; + return
+ + {isLoading &&
+ + + + Loading data... + + +
} +
; + }, + args: { + selectable: true, + columns: columns.map(col => { + if ('accessorKey' in col && col.accessorKey === 'age') { + return { + ...col, + size: 100 + }; + } + return col; + }), + onScroll: fn() + } +} + +### MCP manifest +const VirtualizedSelectable = () => { + const pageSize = 10; + const [sorting, setSorting] = useState([]); + + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + isLoading, + } = useInfiniteQuery( + { + queryKey: ['people', sorting], + queryFn: async ({ pageParam }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData, + }, + queryClient, + ); + + const flatData = useMemo( + () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], + [infiniteQueryData], + ); + + const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; + const hasMore = flatData.length < totalRows; + + return ( +
+ { + if ('accessorKey' in col && col.accessorKey === 'age') { + return { + ...col, + size: 100, + }; + } + return col; + })} + stickyHeader + bordered + fullWidth + expandable={false} + emptyState={} + onRowClick={fn()} + selectable + onScroll={fn()} + data={flatData} + onSortingChange={setSorting} + virtualized={true} + infiniteScroll={{ + hasMore, + isLoadingMore: isFetching, + onLoadMore: fetchNextPage, + }} /> + {isLoading && ( +
+ + + + Loading data... + + +
+ )} +
+ ); +}; + +## Virtualized Expandable Table + +### Show code +{ + name: 'Virtualized Expandable Table', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const pageSize = 10; + const [sorting, setSorting] = useState([]); + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + isLoading + } = useInfiniteQuery({ + queryKey: ['people-expandable', sorting], + queryFn: async ({ + pageParam + }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData + }, queryClient); + const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); + const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; + const hasMore = flatData.length < totalRows; + return
+ + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + + + } /> + {isLoading &&
+ + + + Loading data... + + +
} +
; + }, + args: { + columns: columns.map(col => { + if ('accessorKey' in col && col.accessorKey === 'age') { + return { + ...col, + size: 100 + }; + } + return col; + }), + onScroll: fn() + } +} + +### MCP manifest +const VirtualizedExpandable = () => { + const pageSize = 10; + const [sorting, setSorting] = useState([]); + + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + isLoading, + } = useInfiniteQuery( + { + queryKey: ['people-expandable', sorting], + queryFn: async ({ pageParam }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData, + }, + queryClient, + ); + + const flatData = useMemo( + () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], + [infiniteQueryData], + ); + + const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; + const hasMore = flatData.length < totalRows; + + return ( +
+ { + if ('accessorKey' in col && col.accessorKey === 'age') { + return { + ...col, + size: 100, + }; + } + return col; + })} + stickyHeader + bordered + fullWidth + emptyState={} + onRowClick={fn()} + onScroll={fn()} + data={flatData} + onSortingChange={setSorting} + virtualized={true} + expandable={true} + infiniteScroll={{ + hasMore, + isLoadingMore: isFetching, + onLoadMore: fetchNextPage, + }} + renderExpandedRow={(row) => ( + + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + + + + )} /> + {isLoading && ( +
+ + + + Loading data... + + +
+ )} +
+ ); +}; + +## Virtualized Infinite Scroll + +### Show code +{ + name: 'Virtualized Infinite Scroll', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const pageSize = 5; + const totalRows = 60; + const [sorting, setSorting] = useState([]); + const { + data: infiniteQueryData, + fetchNextPage, + isFetching + } = useInfiniteQuery({ + queryKey: ['people-autofill', sorting], + queryFn: async ({ + pageParam + }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting, totalRows); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData + }, queryClient); + const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); + const fetchedTotal = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? totalRows; + const hasMore = flatData.length < fetchedTotal; + return
+ +
; + } +} + +### MCP manifest +const InfiniteScroll = () => { + const pageSize = 5; + const totalRows = 60; + const [sorting, setSorting] = useState([]); + + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + } = useInfiniteQuery( + { + queryKey: ['people-autofill', sorting], + queryFn: async ({ pageParam }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting, totalRows); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData, + }, + queryClient, + ); + + const flatData = useMemo( + () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], + [infiniteQueryData], + ); + + const fetchedTotal = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? totalRows; + const hasMore = flatData.length < fetchedTotal; + + return ( +
+ } + onRowClick={fn()} + data={flatData} + onSortingChange={setSorting} + virtualized={true} + infiniteScroll={{ + hasMore, + isLoadingMore: isFetching, + onLoadMore: fetchNextPage, + }} /> +
+ ); +}; + +## Virtualized Editable Table + +### Show code +{ + name: 'Virtualized Editable Table', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data, setData] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + return
+ { + setData(rows => updateRow(rows, row.id, columnId as keyof Person, value as never)); + }} /> +
; + } +} + +### MCP manifest +const VirtualizedEditable = () => { + const [data, setData] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + + return ( +
+ } + onRowClick={fn()} + data={data} + columns={editableColumns} + virtualized + onCellEdit={(row: Person, columnId, value) => { + setData((rows) => updateRow(rows, row.id, columnId as keyof Person, value as never)); + }} /> +
+ ); +}; + +## Virtualized With Controls + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + return ; + }, + args: { + controls: + + RED version V2 + + + + Import RED + + + Export RED + + Actions + + + } +} + +### MCP manifest +const VirtualizedWithControls = () => { + const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + + return ( + } + onRowClick={fn()} + controls={( + + RED version V2 + + + + Import RED + + + Export RED + + Actions + + )} + data={data} + virtualized /> + ); +}; + +## Virtualized Resizable Columns + +### Show code +{ + name: 'Virtualized Resizable Columns', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + return
+ +
; + }, + args: { + virtualized: true, + resizableColumns: true + } +} + +### MCP manifest +const VirtualizedResizable = () => { + const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + + return ( +
+ } + onRowClick={fn()} + virtualized + resizableColumns + data={data} /> +
+ ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/ds-table-filters-panel.browser.test.tsx b/packages/design-system/src/components/ds-table/__tests__/ds-table-filters-panel.browser.test.tsx new file mode 100644 index 000000000..60ba3bd5f --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/ds-table-filters-panel.browser.test.tsx @@ -0,0 +1,439 @@ +import { useState } from 'react'; +import { describe, expect, it } from 'vitest'; +import { page } from 'vitest/browser'; +import type { ColumnDef } from '@tanstack/react-table'; +import DsTable from '../ds-table'; +import { DsButtonV3 } from '../../ds-button-v3'; +import { DsModal } from '../../ds-modal'; +import { DsVerticalTabs } from '../../ds-vertical-tabs'; +import { DsTypography } from '../../ds-typography'; +import { DsTagFilter } from '../../ds-tag-filter'; +import { useTableFilters } from '../filters'; +import { type Workflow, workflowFilters } from '../stories/filters-panel/workflow-filters.config'; + +/** + * The story `render` is 100+ lines of modal + vertical-tabs layout. These tests + * reproduce a minimal-but-faithful harness so the behavioral assertions ported + * from `filters-panel.stories.tsx` (`FiltersPanel.play` / `Controlled.play`) + * live in a dedicated browser test instead of a Storybook `play` function. + * + * The columns and 12-row dataset are NOT exported from the story module, so an + * equivalent dataset is replicated here (statuses span active/running/etc.) to + * keep the row-count assertions meaningful. + */ + +const sampleUsers = [ + { name: 'Marry Levin', colorIndex: 0 }, + { name: 'Emery Frank', colorIndex: 1 }, + { name: 'Ryan Franco', colorIndex: 2 }, + { name: 'Roger Dias', colorIndex: 0 }, + { name: 'Lindsey Westerner', colorIndex: 1 }, + { name: 'Neil Sims', colorIndex: 2 }, +] as const; + +const columns: ColumnDef[] = [ + { id: 'status', accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, + { id: 'name', accessorKey: 'name', header: 'Name', cell: (info) => info.getValue() }, + { + id: 'runningCompleted', + accessorKey: 'runningCompleted', + header: 'Running/completed', + cell: (info) => { + const value = info.getValue() as { running: number; completed: number }; + return `${String(value.running)}/${String(value.completed)}`; + }, + }, + { id: 'category', accessorKey: 'category', header: 'Category', cell: (info) => info.getValue() }, + { id: 'version', accessorKey: 'version', header: 'Version', cell: (info) => info.getValue() }, + { id: 'lastEdited', accessorKey: 'lastEdited', header: 'Last edited' }, +]; + +const defaultData: Workflow[] = [ + { + id: '1', + name: 'Scheduled Config Backup', + status: 'active', + runningCompleted: { running: 3, completed: 41 }, + category: 'Network Built', + version: '000.0003', + lastEdited: { + editor: sampleUsers[0].name, + timestamp: '2025-11-26T16:47:00', + colorIndex: sampleUsers[0].colorIndex, + }, + }, + { + id: '2', + name: 'Network Provisioning', + status: 'running', + runningCompleted: { running: 8, completed: 14 }, + category: 'Network Built', + version: '000.0002', + lastEdited: { + editor: sampleUsers[1].name, + timestamp: '2025-11-26T15:32:00', + colorIndex: sampleUsers[1].colorIndex, + }, + }, + { + id: '3', + name: 'Service Provisioning', + status: 'inactive', + runningCompleted: { running: 0, completed: 243 }, + category: 'Network Built', + version: '000.0033', + lastEdited: { + editor: sampleUsers[2].name, + timestamp: '2025-11-25T11:15:00', + colorIndex: sampleUsers[2].colorIndex, + }, + }, + { + id: '4', + name: 'Assign IPv4 Address', + status: 'active', + runningCompleted: { running: 14, completed: 123 }, + category: 'Network Built', + version: '000.0001', + lastEdited: { + editor: sampleUsers[3].name, + timestamp: '2025-11-24T14:20:00', + colorIndex: sampleUsers[3].colorIndex, + }, + }, + { + id: '5', + name: 'Shutdown Decommissioned Device', + status: 'active', + runningCompleted: { running: 45, completed: 45 }, + category: 'Optical Optimization', + version: '000.0022', + lastEdited: { + editor: sampleUsers[4].name, + timestamp: '2025-11-23T13:05:00', + colorIndex: sampleUsers[4].colorIndex, + }, + }, + { + id: '6', + name: 'Optical Power Level Calibration', + status: 'draft', + runningCompleted: { running: 99, completed: 23 }, + category: 'Optical Optimization', + version: '000.0001', + lastEdited: { + editor: sampleUsers[5].name, + timestamp: '2025-11-20T09:30:00', + colorIndex: sampleUsers[5].colorIndex, + }, + }, + { + id: '7', + name: 'Deploy Layer 2 VPN Instance', + status: 'pending', + runningCompleted: { running: 49, completed: 100 }, + category: 'Optical Optimization', + version: '000.0012', + lastEdited: { + editor: sampleUsers[0].name, + timestamp: '2025-11-18T12:45:00', + colorIndex: sampleUsers[0].colorIndex, + }, + }, + { + id: '8', + name: 'Initiate Scheduled Firmware Upgrade', + status: 'active', + runningCompleted: { running: 25, completed: 75 }, + category: 'Service Provisioning', + version: '000.0010', + lastEdited: { + editor: sampleUsers[1].name, + timestamp: '2025-11-15T17:10:00', + colorIndex: sampleUsers[1].colorIndex, + }, + }, + { + id: '9', + name: 'Enable High Availability Mode', + status: 'running', + runningCompleted: { running: 77, completed: 88 }, + category: 'Service Provisioning', + version: '000.0001', + lastEdited: { + editor: sampleUsers[2].name, + timestamp: '2025-11-10T10:22:00', + colorIndex: sampleUsers[2].colorIndex, + }, + }, + { + id: '10', + name: 'Audit Access Control Policies', + status: 'active', + runningCompleted: { running: 65, completed: 200 }, + category: 'Service Provisioning', + version: '000.0001', + lastEdited: { + editor: sampleUsers[3].name, + timestamp: '2025-11-05T15:15:00', + colorIndex: sampleUsers[3].colorIndex, + }, + }, + { + id: '11', + name: 'Synchronize NTP Across Network Nodes', + status: 'warning', + runningCompleted: { running: 49, completed: 142 }, + category: 'Service Provisioning', + version: '000.0001', + lastEdited: { + editor: sampleUsers[4].name, + timestamp: '2025-10-28T08:40:00', + colorIndex: sampleUsers[4].colorIndex, + }, + }, + { + id: '12', + name: 'Validate Optical Link Integrity', + status: 'failed', + runningCompleted: { running: 90, completed: 300 }, + category: 'Network Built', + version: '000.0001', + lastEdited: { + editor: sampleUsers[5].name, + timestamp: '2025-10-15T16:47:00', + colorIndex: sampleUsers[5].colorIndex, + }, + }, +]; + +/** + * Minimal filter-panel harness mirroring the story: filter button, generated + * chips, table with enhanced columns, and a modal with a vertical-tabs nav + + * content + footer wired to `useTableFilters`. In `controlled` mode it holds + * `appliedFilters` externally and renders a `
` snapshot of that state.
+ */
+function FiltersPanelHarness({ controlled = false }: { controlled?: boolean }) {
+	const [appliedFilters, setAppliedFilters] = useState>({});
+
+	const { columnFilters, filterChips, filterNavItems, enhancedColumns, handlers, renderFilterContent } =
+		useTableFilters({
+			filterAdapters: workflowFilters,
+			baseColumns: columns,
+			...(controlled ? { appliedFilters, onFiltersChange: setAppliedFilters } : {}),
+		});
+
+	const [isOpen, setIsOpen] = useState(false);
+	const [selectedFilterId, setSelectedFilterId] = useState(filterNavItems[0]?.id ?? '');
+
+	const handleValueChange = (value: string | null) => {
+		if (value) {
+			setSelectedFilterId(value);
+		}
+	};
+
+	const handleApply = () => {
+		handlers.applyFilters();
+		setIsOpen(false);
+	};
+
+	const handleClearAll = () => {
+		handlers.clearAll();
+		setIsOpen(false);
+	};
+
+	return (
+		
+ {controlled &&
{JSON.stringify(appliedFilters, null, 2)}
} + + setIsOpen(true)} + /> + + {filterChips.length > 0 && ( + + )} + + No data available
} + /> + + + + Filters + + + + + + + {filterNavItems.map((item) => ( + + {item.label} + + ))} + + {filterNavItems.map((item) => ( + + {renderFilterContent(item)} + + ))} + + + + + + Clear all + + + + Apply + + + + + + ); +} + +// Body rows only (drop the header row), matching the sibling column-filters test. +const getDataRows = () => page.getByRole('row').all().slice(1); + +const filterButton = () => page.getByRole('button', { name: 'Filter', exact: true }); +const applyButton = () => page.getByRole('button', { name: 'Apply' }); +const tagFilterClearAll = () => page.getByRole('button', { name: 'Clear all filters' }); + +/** Open the modal, pick the Status tab, and check Active + Running. */ +async function selectActiveAndRunning() { + await filterButton().click(); + await page.getByRole('tab', { name: /status/i }).click(); + await page.getByRole('checkbox', { name: /^active$/i }).click(); + await page.getByRole('checkbox', { name: /^running$/i }).click(); +} + +describe('DsTable — filters panel', () => { + it('opens the modal and exposes the filter category tabs', async () => { + await page.render(); + + await filterButton().click(); + + await expect.element(page.getByRole('tab', { name: /status/i })).toBeVisible(); + await expect.element(page.getByRole('tab', { name: /running\/completed/i })).toBeVisible(); + await expect.element(page.getByRole('tab', { name: /last edited/i })).toBeVisible(); + }); + + it('checks the selected Status options and preserves them after reopening', async () => { + await page.render(); + + await selectActiveAndRunning(); + + await expect.element(page.getByRole('checkbox', { name: /^active$/i })).toBeChecked(); + await expect.element(page.getByRole('checkbox', { name: /^running$/i })).toBeChecked(); + + await applyButton().click(); + + // Reopen — the previously applied Status filters remain checked. + await filterButton().click(); + await page.getByRole('tab', { name: /status/i }).click(); + + await expect.element(page.getByRole('checkbox', { name: /^active$/i })).toBeChecked(); + await expect.element(page.getByRole('checkbox', { name: /^running$/i })).toBeChecked(); + }); + + it('applies status, range, editor, and time filters to produce chips and filter rows', async () => { + await page.render(); + + expect(getDataRows()).toHaveLength(12); + + // Status: Active + Running + await selectActiveAndRunning(); + + // Running/Completed: first two spin buttons are the Running field's From/To. + await page.getByRole('tab', { name: /running\/completed/i }).click(); + const spinButtons = page.getByRole('spinbutton'); + await spinButtons.nth(0).fill('0'); + await spinButtons.nth(1).fill('50'); + + // Last edited: an editor + a preset time range + await page.getByRole('tab', { name: /last edited/i }).click(); + await page.getByRole('checkbox', { name: /marry levin/i }).click(); + await page.getByRole('radio', { name: /last 3 months/i }).click(); + + await expect.element(page.getByRole('checkbox', { name: /marry levin/i })).toBeChecked(); + await expect.element(page.getByRole('radio', { name: /last 3 months/i })).toBeChecked(); + + await applyButton().click(); + + // Chips are queried by role so the aria-hidden measurement portal copies + // (rendered to document.body by DsTagFilter) are excluded. + await expect.element(page.getByRole('button', { name: /status: active/i })).toBeVisible(); + await expect.element(page.getByRole('button', { name: /status: running/i })).toBeVisible(); + await expect.element(page.getByRole('button', { name: /running.*0.*50/i })).toBeVisible(); + await expect.element(page.getByRole('button', { name: /editor: marry levin/i })).toBeVisible(); + await expect.element(page.getByRole('button', { name: /last 3 months/i })).toBeVisible(); + + expect(getDataRows().length).toBeLessThan(12); + }); + + it('removes a single chip via its Delete tag button', async () => { + await page.render(); + + await selectActiveAndRunning(); + await applyButton().click(); + + await expect.element(page.getByRole('button', { name: /^status: active$/i })).toBeVisible(); + + // The chip is itself role="button" (aria-label), so its nested Delete tag + // button is presentational to role queries — reach it by its label. The + // delete button only reveals (opacity/visibility) on hover, so hover first. + const activeChip = page.getByRole('button', { name: /^status: active$/i }); + await activeChip.hover(); + await activeChip.getByLabelText(/delete tag/i).click(); + + await expect.element(page.getByRole('button', { name: /^status: active$/i })).not.toBeInTheDocument(); + // The other status chip is untouched. + await expect.element(page.getByRole('button', { name: /^status: running$/i })).toBeVisible(); + }); + + it('clears all chips and restores every row via Clear all', async () => { + await page.render(); + + await selectActiveAndRunning(); + await applyButton().click(); + + await expect.element(page.getByRole('button', { name: /status: active/i })).toBeVisible(); + expect(getDataRows().length).toBeLessThan(12); + + await tagFilterClearAll().click(); + + await expect.element(page.getByRole('button', { name: /status:/i })).not.toBeInTheDocument(); + expect(getDataRows()).toHaveLength(12); + }); + + it('reflects applied filter state externally and resets it on Clear all (controlled)', async () => { + await page.render(); + + // External state starts empty. + await expect.element(page.getByText('{}', { exact: true })).toBeVisible(); + + await filterButton().click(); + await page.getByRole('tab', { name: /status/i }).click(); + await page.getByRole('checkbox', { name: /^active$/i }).click(); + await applyButton().click(); + + // External state now carries the status filter, and the chip appears. + await expect.element(page.getByText(/"status"/)).toBeVisible(); + await expect.element(page.getByRole('button', { name: /status: active/i })).toBeVisible(); + + await tagFilterClearAll().click(); + + await expect.element(page.getByText('{}', { exact: true })).toBeVisible(); + await expect.element(page.getByRole('button', { name: /status: active/i })).not.toBeInTheDocument(); + }); +}); diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-active-row.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-active-row.stories.tsx index 0e4aaa9b5..461940bb4 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-active-row.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-active-row.stories.tsx @@ -1,8 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { useState } from 'react'; -import classnames from 'classnames'; -import { DsIcon } from '../../ds-icon'; +import { fn } from 'storybook/test'; import { DsDrawer } from '../../ds-drawer'; +import { DsStack } from '../../ds-stack'; +import { DsButtonV3 } from '../../ds-button-v3'; +import { DsTypography } from '../../ds-typography'; import DsTable from '../ds-table'; import styles from './ds-table.stories.module.scss'; import { columns, defaultData, type Person } from './common/story-data'; @@ -23,7 +25,7 @@ const meta: Meta> = { fullWidth: true, expandable: false, emptyState: , - onRowClick: (row) => console.log('Row clicked:', row), + onRowClick: fn(), }, decorators: [fullHeightDecorator], }; @@ -31,11 +33,32 @@ const meta: Meta> = { export default meta; type Story = StoryObj>; +/** + * Pass `activeRowId` to highlight a single row independently of selection. The + * highlight persists until you change or clear the id — useful for marking the + * record a side panel or detail view is currently showing. + */ +export const ActiveRow: Story = { + args: { + data: defaultData.slice(0, 10), + activeRowId: '3', + }, +}; + +/** + * Pass `activeRowId` to keep a row highlighted independently of selection — + * ideal for a master/detail layout where clicking a row opens a drawer. Track + * the clicked record in state, derive `activeRowId` from it, and clear it when + * the drawer closes. Clicking the active row again toggles the drawer shut. + */ export const WithDrawerAndActiveRow: Story = { name: 'Active Row with Drawer', args: { data: defaultData.slice(0, 10), }, + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const [selectedPerson, setSelectedPerson] = useState(null); @@ -49,15 +72,7 @@ export const WithDrawerAndActiveRow: Story = { }; return ( -
-
-

Active Row with Drawer Demo

-

- Click on any row to open a drawer with detailed information. The clicked row will remain - highlighted to indicate which record the drawer is displaying. -

-
- + <> {selectedPerson && (
-
-

Person Details

- -
+ + + Person Details + setSelectedPerson(null)} + /> + -
-
- Full Name -

- {selectedPerson.firstName} {selectedPerson.lastName} -

-
+ + + + Full Name + + + {selectedPerson.firstName} {selectedPerson.lastName} + + -
- Age -

{selectedPerson.age} years old

-
+ + + Age + + {selectedPerson.age} years old + -
- Visits -

{selectedPerson.visits} visits

-
+ + + Visits + + {selectedPerson.visits} visits + -
- Status -

- {selectedPerson.status} -

-
+ + + Status + + + {selectedPerson.status.charAt(0).toUpperCase() + selectedPerson.status.slice(1)} + + -
- Profile Progress -
+ + + Profile Progress + -
-
- -
-

- Note: The row in the table remains highlighted while this drawer is open, - helping you keep track of which record you're viewing. -

-
-
+
+ +
)}
-
+ ); }, }; diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-column-filters.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-column-filters.stories.tsx index e82d413e8..c2e120165 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-column-filters.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-column-filters.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import type { ColumnDef } from '@tanstack/react-table'; import { useState } from 'react'; import DsTable from '../ds-table'; +import { DsStack } from '../../ds-stack'; import { DsTagFilter } from '../../ds-tag-filter'; import { type CheckboxFilterItem, @@ -10,7 +11,6 @@ import { useTableFilters, } from '../filters'; import { fullHeightDecorator } from './common/story-decorators'; -import styles from './ds-table.stories.module.scss'; type DeviceRow = { id: string; @@ -308,7 +308,7 @@ draft and closes it (same as pressing Escape). }); return ( -
+ {filterChips.length > 0 && ( -
+ ); }, }; @@ -357,7 +357,7 @@ external state, leaving other filters untouched. }); return ( -
+ {filterChips.length > 0 && ( -
+ ); }, }; diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-columns.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-columns.stories.tsx index 7a40c9efe..fb843e631 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-columns.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-columns.stories.tsx @@ -1,8 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { useState } from 'react'; +import { fn } from 'storybook/test'; import type { VisibilityState } from '@tanstack/react-table'; import classnames from 'classnames'; import { DsCheckbox } from '../../ds-checkbox'; +import { DsStack } from '../../ds-stack'; import DsTable from '../ds-table'; import styles from './ds-table.stories.module.scss'; import { columns, defaultData, type Person, type Status } from './common/story-data'; @@ -23,7 +25,7 @@ const meta: Meta> = { fullWidth: true, expandable: false, emptyState: , - onRowClick: (row) => console.log('Row clicked:', row), + onRowClick: fn(), }, decorators: [fullHeightDecorator], }; @@ -60,7 +62,15 @@ export const WithProgressInfographic: Story = { }, }; +/** + * Show or hide columns dynamically via the controlled `columnVisibility` / + * `onColumnVisibilityChange` props — useful for customizable table views or + * responsive layouts. Toggle a checkbox to add or remove the matching column. + */ export const ColumnHiding: Story = { + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const columnsToToggle = [ { id: 'age', label: 'Age' }, @@ -83,16 +93,8 @@ export const ColumnHiding: Story = { }; return ( -
-
-

Column Hiding Demo

-

- Use the checkboxes below to show or hide specific columns dynamically. This is useful for - customizable table views or responsive layouts. -

-
- -
+ + {columnsToToggle.map((column) => ( toggleColumn(column.id)} /> ))} -
+ -
+ ); }, }; diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-expansion.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-expansion.stories.tsx index 454816a97..d989c4782 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-expansion.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-expansion.stories.tsx @@ -3,7 +3,9 @@ import { fn } from 'storybook/test'; import { useRef, useState } from 'react'; import DsTable from '../ds-table'; import type { DsTableApi } from '../ds-table.types'; -import styles from './ds-table.stories.module.scss'; +import { DsStack } from '../../ds-stack'; +import { DsButtonV3 } from '../../ds-button-v3'; +import { DsTypography } from '../../ds-typography'; import { columns, defaultData, type Person } from './common/story-data'; import { fullHeightDecorator } from './common/story-decorators'; import { TableEmptyState } from './components'; @@ -35,15 +37,15 @@ export const Expandable: Story = { data: defaultData.slice(0, 5), expandable: (row) => row.firstName !== 'Tanner', renderExpandedRow: (row) => ( - <> -
-

Expanded Details for {row.firstName}

-

ID: {row.id}

-

+ + + Expanded Details for {row.firstName} + ID: {row.id} + Full Name: {row.firstName} {row.lastName} -

-

Status: {row.status}

-
+ + Status: {row.status} + - + ), }, }; @@ -76,25 +78,37 @@ export const CustomExpanderColumnWidth: Story = { data: defaultData.slice(0, 5), expandable: true, expandableColumnWidth: 48, - renderExpandedRow: (row) =>
Details for {row.firstName}
, + renderExpandedRow: (row) => ( + + Details for {row.firstName} + + ), }, }; +/** + * Drive expansion imperatively through the table ref. `expandRow`, `expandRows`, + * `expandAllRows`, and `collapseAllRows` on `DsTableApi` let a parent expand or + * collapse rows from outside the table. + */ export const ProgrammaticExpansion: Story = { args: { data: defaultData.slice(0, 5), expandable: (row) => row.firstName !== 'Tanner', renderExpandedRow: (row) => ( -
-

Expanded Details for {row.firstName}

-

ID: {row.id}

-

+ + Expanded Details for {row.firstName} + ID: {row.id} + Full Name: {row.firstName} {row.lastName} -

-

Status: {row.status}

-
+ + Status: {row.status} + ), }, + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const tableRef = useRef>(null); const [expandedRows, setExpandedRows] = useState([]); @@ -125,40 +139,34 @@ export const ProgrammaticExpansion: Story = { }; return ( -
-
-

Programmatic Row Expansion Demo

-

- Use the buttons below to programmatically control row expansion using TanStack Table v8 APIs. -

-

- Expanded rows: {expandedRows.length > 0 ? expandedRows.join(', ') : 'None'} -

-
+ + + Expanded rows: {expandedRows.length > 0 ? expandedRows.join(', ') : 'None'} + -
- - - - - - -
+ +
-
+ ); }, }; diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-loading.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-loading.stories.tsx index 42cb51bd2..8197c6cd7 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-loading.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-loading.stories.tsx @@ -17,7 +17,7 @@ const meta: Meta> = { layout: 'fullscreen', }, args: { - columns: loadingColumns, + columns, data: defaultData, stickyHeader: true, bordered: true, @@ -30,8 +30,24 @@ const meta: Meta> = { export default meta; type Story = StoryObj>; +/** + * Set `loading` to render skeleton rows. Each cell falls back to a default + * skeleton bar sized to its column. + */ export const Loading: Story = { args: { loading: true, }, }; + +/** + * Provide a per-column `loadingCell` to control the skeleton shown while + * `loading` is true — here the first column renders a circular skeleton instead + * of the default bar. + */ +export const CustomLoadingCell: Story = { + args: { + loading: true, + columns: loadingColumns, + }, +}; diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-row-actions.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-row-actions.stories.tsx index 2057656a2..3a22b287f 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-row-actions.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-row-actions.stories.tsx @@ -20,7 +20,7 @@ const meta: Meta> = { fullWidth: true, expandable: false, emptyState: , - onRowClick: (row) => console.log('Row clicked:', row), + onRowClick: fn(), }, decorators: [fullHeightDecorator], }; diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-search.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-search.stories.tsx index ed91e45fe..43e40536e 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-search.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-search.stories.tsx @@ -1,10 +1,12 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; import { useMemo, useState } from 'react'; import type { ColumnDef, ColumnFiltersState } from '@tanstack/react-table'; -import type { IconType } from '../../ds-icon'; +import { DsIcon, type IconType } from '../../ds-icon'; import { DsSmartTabs } from '../../ds-smart-tabs'; +import { DsStack } from '../../ds-stack'; +import { DsTextInput } from '../../ds-text-input'; import DsTable from '../ds-table'; -import styles from './ds-table.stories.module.scss'; import { columns, defaultData, type Person, type Status } from './common/story-data'; import { fullHeightDecorator } from './common/story-decorators'; import { StatusItem, TableEmptyState } from './components'; @@ -23,7 +25,7 @@ const meta: Meta> = { fullWidth: true, expandable: false, emptyState: , - onRowClick: (row) => console.log('Row clicked:', row), + onRowClick: fn(), }, decorators: [fullHeightDecorator], }; @@ -31,8 +33,16 @@ const meta: Meta> = { export default meta; type Story = StoryObj>; +/** + * Global search across every column, owned by the consumer. Keep the query in + * state, derive the filtered rows, and pass them to `data` — the table stays a + * pure presentational view of whatever rows you hand it. + */ export const AdvancedSearch: Story = { name: 'Search — Global Input', + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const [globalFilter, setGlobalFilter] = useState(''); @@ -49,24 +59,31 @@ export const AdvancedSearch: Story = { }, [globalFilter, args.data]); return ( -
-
- + + setGlobalFilter(e.target.value)} + onValueChange={setGlobalFilter} placeholder="Search all columns..." - style={{ padding: '0.5rem', width: '300px' }} + slots={{ startAdornment: }} /> -
+ -
+ ); }, }; +/** + * Drive a column filter from a tab bar. `DsSmartTabs` owns the active tab; each + * tab maps to a `columnFilters` entry (or clears it for "All"). The table is + * controlled via `columnFilters` / `onColumnFiltersChange`. + */ export const TabFilters: Story = { name: 'Tabs — Status Filter', + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const [columnFilters, setColumnFilters] = useState([]); const [activeTab, setActiveTab] = useState('all'); @@ -107,7 +124,7 @@ export const TabFilters: Story = { ); return ( -
+ -
+ ); }, }; diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-selection.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-selection.stories.tsx index 97fffb3ed..4563cafcd 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-selection.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-selection.stories.tsx @@ -3,7 +3,9 @@ import { fn } from 'storybook/test'; import { useRef, useState } from 'react'; import DsTable from '../ds-table'; import type { DsTableApi } from '../ds-table.types'; -import styles from './ds-table.stories.module.scss'; +import { DsStack } from '../../ds-stack'; +import { DsButtonV3 } from '../../ds-button-v3'; +import { DsTypography } from '../../ds-typography'; import { columns, defaultData, type Person } from './common/story-data'; import { fullHeightDecorator } from './common/story-decorators'; import { TableEmptyState } from './components'; @@ -22,7 +24,7 @@ const meta: Meta> = { fullWidth: true, expandable: false, emptyState: , - onRowClick: (row) => console.log('Row clicked:', row), + onRowClick: fn(), }, decorators: [fullHeightDecorator], }; @@ -49,83 +51,81 @@ export const CustomSelectColumnWidth: Story = { }, }; +/** + * Drive selection imperatively through the table ref. `selectRow`, `selectRows`, + * `selectAllRows`, and `deselectAllRows` on `DsTableApi` let a parent control + * selection from outside the table (toolbars, keyboard shortcuts, bulk flows). + */ export const ProgrammaticRowSelection: Story = { args: { selectable: true, showSelectAllCheckbox: false, stickyHeader: true, - onSelectionChange: (selectedRows) => console.log('Selected rows:', selectedRows), + onSelectionChange: fn(), + }, + parameters: { + docs: { source: { type: 'code' } }, }, render: function Render(args) { const tableRef = useRef>(null); const [selectedRows, setSelectedRows] = useState([]); - const selectRow = (rowId: string) => { - tableRef.current?.selectRow(rowId); - }; - - const selectAllRows = () => { - tableRef.current?.selectAllRows(); - }; - - const deselectAllRows = () => { - tableRef.current?.deselectAllRows(); - }; - - const selectSpecificRows = () => { - tableRef.current?.selectRows(['1', '2', '3']); - }; - const handleSelectionChange = (selection: Record) => { - const selectedIds = Object.keys(selection); - setSelectedRows(selectedIds); + setSelectedRows(Object.keys(selection)); }; return ( -
-
-

Programmatic Row Selection Demo

-

- Use the buttons below to programmatically control row selection using TanStack Table v8 APIs. -

-

- Selected rows: {selectedRows.length > 0 ? selectedRows.join(', ') : 'None'} -

-
+ + + Selected rows: {selectedRows.length > 0 ? selectedRows.join(', ') : 'None'} + -
- - - - - - -
+ +
-
+ ); }, }; +/** + * Cap how many rows can be selected at once. Track selection in state and make + * `selectable` a predicate: a row stays selectable only if it is already + * selected or the count is under the limit, so the remaining checkboxes disable + * once the cap is reached. + */ export const MaxSelectionLimit: Story = { name: 'Max N Selections', args: { showSelectAllCheckbox: false, onSelectionChange: fn(), }, + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const [rowSelection, setRowSelection] = useState>({}); const maxSelections = 2; @@ -138,26 +138,17 @@ export const MaxSelectionLimit: Story = { }; return ( -
-
-

Max Selection Limit Demo

-

- You can select at most {maxSelections} rows. Once the limit is reached, checkboxes for other rows - are disabled. -

-

- Selected: {selectedCount} / {maxSelections} -

-
+ + + Selected: {selectedCount} / {maxSelections} + { - return rowSelection[rowData.id] || selectedCount < maxSelections; - }} + selectable={(rowData) => rowSelection[rowData.id] || selectedCount < maxSelections} /> -
+ ); }, }; diff --git a/packages/design-system/src/components/ds-table/stories/ds-table-virtualized.stories.tsx b/packages/design-system/src/components/ds-table/stories/ds-table-virtualized.stories.tsx index fb4989629..5162cdaf1 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table-virtualized.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/ds-table-virtualized.stories.tsx @@ -12,6 +12,7 @@ import { import { DsSpinner } from '../../ds-spinner'; import { DsStack } from '../../ds-stack'; import { DsButtonV3 } from '../../ds-button-v3'; +import { DsTypography } from '../../ds-typography'; import { generatePersonData, simulateApiCall } from './common/story-data-generator'; import styles from './ds-table.stories.module.scss'; import editableStyles from './ds-table-editable.stories.module.scss'; @@ -33,12 +34,12 @@ const meta: Meta> = { fullWidth: true, expandable: false, emptyState: , - onRowClick: (row) => console.log('Row clicked:', row), + onRowClick: fn(), }, decorators: [ (Story) => fullHeightDecorator(() => ( -
+
)), @@ -69,8 +70,17 @@ export const EmptyState: Story = { }, }; +/** + * Row virtualization with selection, backed by TanStack Query infinite scroll. + * Keep the flattened pages in `data`, wire `infiniteScroll` to the query's + * `fetchNextPage` / loading flags, and the table renders only visible rows — + * performant even for very large datasets. + */ export const VirtualizedSelectable: Story = { name: 'Virtualized Selectable Table', + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const pageSize = 10; const [sorting, setSorting] = useState([]); @@ -103,39 +113,28 @@ export const VirtualizedSelectable: Story = { const hasMore = flatData.length < totalRows; return ( -
-
-

Virtualized Table Demo

-

- This table uses infinite query to fetch data as you scroll, making it performant even with large - datasets. Try scrolling to see the data loading! -

-

- ({flatData.length} of {totalRows} rows fetched) -

-
- -
- - {isLoading && ( -
-
- - Loading data... -
-
- )} -
+
+ + {isLoading && ( +
+ + + + Loading data... + + +
+ )}
); }, @@ -154,8 +153,15 @@ export const VirtualizedSelectable: Story = { }, }; +/** + * Combine row virtualization with expandable rows. `renderExpandedRow` supplies + * the detail content for each expanded row while the body stays virtualized. + */ export const VirtualizedExpandable: Story = { name: 'Virtualized Expandable Table', + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const pageSize = 10; const [sorting, setSorting] = useState([]); @@ -188,70 +194,59 @@ export const VirtualizedExpandable: Story = { const hasMore = flatData.length < totalRows; return ( -
-
-

Virtualized Table with Expandable Rows

-

- This table combines virtualization for large datasets with expandable rows. Click the chevron to - expand rows and see additional details. -

-

- ({flatData.length} of {totalRows} rows fetched) -

-
- -
- ( - <> -
-

Expanded Details for {row.firstName}

-

ID: {row.id}

-

- Full Name: {row.firstName} {row.lastName} -

-

Status: {row.status}

-
- - - - )} - /> - {isLoading && ( -
-
- - Loading data... -
-
+
+ ( + + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + + + )} -
+ /> + {isLoading && ( +
+ + + + Loading data... + + +
+ )}
); }, @@ -269,8 +264,17 @@ export const VirtualizedExpandable: Story = { }, }; +/** + * When the first page returns too few rows to fill the viewport, `autoFill` + * (on by default) keeps requesting pages until the content becomes scrollable, + * so infinite scroll can take over. Wire `infiniteScroll` to your query's + * `fetchNextPage` and loading flags. + */ export const InfiniteScroll: Story = { name: 'Virtualized Infinite Scroll', + parameters: { + docs: { source: { type: 'code' } }, + }, render: function Render(args) { const pageSize = 5; const totalRows = 60; @@ -303,32 +307,18 @@ export const InfiniteScroll: Story = { const hasMore = flatData.length < fetchedTotal; return ( -
-
-

Virtualized Infinite Scroll

-

- The first page only returns {pageSize} rows - too few to fill the viewport. With{' '} - autoFill: true (the default), the Table keeps requesting pages until the content - becomes scrollable. -

-

- ({flatData.length} of {fetchedTotal} rows fetched) -

-
- -
- -
+
+
); }, @@ -414,54 +404,42 @@ const editableColumns: ColumnDef[] = [ }, ]; +/** + * Inline editing works with row virtualization for large datasets. Scroll + * through thousands of rows and double-click any editable cell to edit in place; + * `onCellEdit` reports the row, column, and new value to persist. + */ export const VirtualizedEditable: Story = { name: 'Virtualized Editable Table', parameters: { - docs: { - description: { - story: - 'Editable cells work with row virtualization for large datasets. Scroll through thousands of rows and double-click any editable cell to edit in place.', - }, - }, + docs: { source: { type: 'code' } }, }, render: function Render(args) { const [data, setData] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); return ( -
-
-

Virtualized Editable Table

-

- Inline editing with row virtualization. Scroll through {VIRTUALIZED_ROW_COUNT.toLocaleString()}{' '} - rows and double-click a cell to edit in place. -

-

({data.length.toLocaleString()} rows loaded)

-
- -
- { - setData((rows) => updateRow(rows, row.id, columnId as keyof Person, value as never)); - }} - /> -
+
+ { + setData((rows) => updateRow(rows, row.id, columnId as keyof Person, value as never)); + }} + />
); }, }; +/** + * The pinned `controls` bar stays fixed above the header while the virtualized + * body scrolls through a large dataset. + */ export const VirtualizedWithControls: Story = { parameters: { - docs: { - description: { - story: - 'The pinned controls bar stays fixed above the header while the virtualized body scrolls through a large dataset.', - }, - }, + docs: { source: { type: 'code' } }, }, render: function Render(args) { const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); @@ -496,31 +474,14 @@ export const VirtualizedWithControls: Story = { export const VirtualizedResizable: Story = { name: 'Virtualized Resizable Columns', parameters: { - docs: { - source: { type: 'code' }, - description: { - story: - 'Column resize works with row virtualization. Scroll through a large dataset and drag a header edge to resize; widths apply to virtualized rows. Double-click a handle to restore the snapshotted width.', - }, - }, + docs: { source: { type: 'code' } }, }, render: function Render(args) { const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); return ( -
-
-

Virtualized Resizable Columns

-

- Row virtualization with column resize. Scroll through {VIRTUALIZED_ROW_COUNT.toLocaleString()}{' '} - rows and drag a header edge to resize. -

-

({data.length.toLocaleString()} rows loaded)

-
- -
- -
+
+
); }, diff --git a/packages/design-system/src/components/ds-table/stories/ds-table.stories.module.scss b/packages/design-system/src/components/ds-table/stories/ds-table.stories.module.scss index cd737b129..b5c26cb57 100644 --- a/packages/design-system/src/components/ds-table/stories/ds-table.stories.module.scss +++ b/packages/design-system/src/components/ds-table/stories/ds-table.stories.module.scss @@ -2,21 +2,25 @@ .storyPadding { height: 100%; - padding: 1rem; + padding: var(--standard); } -.expandedRowDetails { - padding: 10px; - margin-bottom: 20px; - background-color: #f9f9f9; - border-left: 3px solid lightblue; +.virtualizedStoryHeight { + height: 700px; + min-height: 100%; } +.horizontalScrollWrapper { + width: 700px; + height: 320px; +} + +// Custom status chip cell renderer (see Columns → Progress as Infographic) .statusCell { - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: 600; + padding: var(--3xs) var(--xs); + border-radius: var(--3xs); + font-size: var(--body-font-size-sm); + font-weight: var(--font-weight-semi-bold); color: var(--color-dap-gray-050); &--single { @@ -32,17 +36,32 @@ } } -.tableFilterContainer { - display: flex; - flex-direction: column; - height: 100%; +// Virtualized stories: the table body needs a bounded, positioned host so the +// loading scrim can overlay it while rows are fetched. +.virtualizedTableWrapper { + position: relative; flex: 1; - margin: 20px; - gap: 20px; } -.toolbar { - margin-left: auto; +.loadingOverlay { + position: absolute; + inset: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + background-color: rgb(255 255 255 / 80%); + border-radius: var(--3xs); +} + +// Active-row drawer content padding (Drawer owns no inner padding). +.drawerContent { + padding: var(--lg); +} + +// Filters modal layout (two-column: nav list + content). +.filterModal { + height: 600px; } .filterHeader { @@ -52,18 +71,12 @@ padding: var(--md) var(--standard); } -.headerLeft { - display: flex; - align-items: center; - gap: var(--xs); -} - .filterBody { display: flex; flex: 1; min-height: 0; - overflow: hidden; padding: 0; + overflow: hidden; } .filterTabs { @@ -72,9 +85,9 @@ } .filterTabList { + flex-basis: 40%; border-right: 1px solid var(--border-secondary); background: var(--background-secondary); - flex-basis: 40%; } .filterContent { @@ -86,10 +99,10 @@ .filterFooter { display: flex; + flex-shrink: 0; align-items: center; justify-content: space-between; padding: var(--sm) var(--standard); - flex-shrink: 0; } .filterTabLabel { @@ -98,233 +111,28 @@ color: inherit; } -.filterTabBadge { - display: flex; - align-items: center; - gap: var(--xs); - flex-shrink: 0; -} - .filterTabDot { width: 10px; height: 10px; + flex-shrink: 0; border-radius: 50%; background: var(--background-primary); border: 1px solid var(--background); - flex-shrink: 0; -} - -.filterTabCount { - color: var(--font-secondary); -} - -.programmaticSelectionDemo { - margin-bottom: 1rem; - padding: 1rem; - background-color: #f5f5f5; - border-radius: 4px; - - &__title { - margin: 0 0 0.5rem 0; - } - - &__description { - margin: 0; - font-size: 14px; - color: #666; - } - - &__selectedRows { - margin: 0.5rem 0 0 0; - font-size: 12px; - color: #888; - } -} - -.programmaticSelectionControls { - margin-bottom: 1rem; - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.programmaticSelectionButton { - padding: 0.5rem 1rem; - border: 1px solid #ccc; - border-radius: 4px; - cursor: pointer; - background: white; - transition: all 0.2s ease; - - &:hover { - background-color: #f8f9fa; - border-color: #adb5bd; - } - - &:active { - background-color: #e9ecef; - } -} - -.virtualizedDemoContainer { - display: flex; - flex-direction: column; - gap: 1rem; - position: relative; - height: 100%; -} - -.virtualizedDemoHeader { - margin-bottom: 1rem; - padding: 1rem; - background-color: #f5f5f5; - border-radius: 4px; - - &__title { - margin: 0 0 0.5rem 0; - } - - &__description { - margin: 0; - font-size: 14px; - color: #666; - } - - &__stats { - margin: 0.5rem 0 0 0; - font-size: 12px; - color: #888; - } -} - -.developmentNotice { - margin-bottom: 1rem; - font-size: 12px; - color: #888; -} - -.virtualizedTableWrapper { - position: relative; - flex: 1; -} - -.loadingOverlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(255, 255, 255, 0.8); - display: flex; - align-items: center; - justify-content: center; - z-index: 10; - border-radius: 4px; -} - -.loadingContent { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; -} - -.loadingText { - font-size: 14px; - color: #666; -} - -// Drawer story styles -.drawerContent { - padding: var(--lg); -} - -.drawerHeader { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--lg); -} - -.drawerTitle { - margin: 0; - font-size: var(--heading-font-size-2xl); - font-weight: var(--font-weight-semi-bold); -} - -.drawerCloseButton { - background: none; - border: none; - cursor: pointer; - padding: var(--xs); - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: background-color 0.2s ease; - - &:hover { - background-color: var(--background-action-hover-weak); - } -} - -.drawerDetails { - display: flex; - flex-direction: column; - gap: var(--standard); -} - -.drawerDetailItem { - display: flex; - flex-direction: column; -} - -.drawerDetailLabel { - color: var(--font-secondary); - font-weight: var(--font-weight-medium); - font-size: var(--body-font-size-sm); - margin-bottom: var(--3xs); -} - -.drawerDetailValue { - margin: 0; - font-size: var(--body-font-size-md); - color: var(--font-main); -} - -.drawerDetailValueCapitalized { - text-transform: capitalize; -} - -.drawerProgressContainer { - margin-top: var(--xs); -} - -.drawerNote { - margin-top: var(--lg); - padding: var(--standard); - background: var(--background-info); - border-radius: 4px; - - p { - margin: 0; - font-size: var(--body-font-size-sm); - color: var(--font-secondary); - } } +// Controlled filters demo: JSON debug panel showing external filter state. .debugPanel { padding: var(--md); - background: var(--background-secondary); border: 1px solid var(--border-secondary); - border-radius: 4px; + border-radius: var(--3xs); + background: var(--background-secondary); } .debugCode { margin: var(--xs) 0 0; padding: var(--sm); + border-radius: var(--3xs); background: var(--background); - border-radius: 4px; font-family: monospace; font-size: var(--body-font-size-sm); overflow-x: auto; @@ -335,8 +143,3 @@ .destructiveAction { color: var(--background-error); } - -.horizontalScrollWrapper { - width: 700px; - height: 320px; -} diff --git a/packages/design-system/src/components/ds-table/stories/filters-panel.stories.tsx b/packages/design-system/src/components/ds-table/stories/filters-panel.stories.tsx index e9117b35a..a4a5b0498 100644 --- a/packages/design-system/src/components/ds-table/stories/filters-panel.stories.tsx +++ b/packages/design-system/src/components/ds-table/stories/filters-panel.stories.tsx @@ -1,17 +1,20 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { expect, screen, userEvent, within } from 'storybook/test'; +import { fn } from 'storybook/test'; import { useState } from 'react'; import type { ColumnDef } from '@tanstack/react-table'; import { DsIcon } from '../../ds-icon'; import DsTable from '../ds-table'; import { DsButtonV3 } from '../../ds-button-v3'; import { DsModal } from '../../ds-modal'; +import { DsStack } from '../../ds-stack'; import { DsVerticalTabs } from '../../ds-vertical-tabs'; import { DsTypography } from '../../ds-typography'; import { DsTagFilter } from '../../ds-tag-filter'; import { useTableFilters } from '../filters/hooks/use-table-filters'; import type { FilterNavItem } from '../filters/types/filter-adapter.types'; import { type Workflow, workflowFilters } from './filters-panel/workflow-filters.config'; +import { fullHeightDecorator } from './common/story-decorators'; +import { TableEmptyState } from './components'; import styles from './ds-table.stories.module.scss'; const sampleUsers = [ @@ -378,21 +381,10 @@ createCustomFilterAdapter({ bordered: true, fullWidth: true, expandable: false, - emptyState:
No data available
, - onRowClick: (row) => console.log('Row clicked:', row), + emptyState: , + onRowClick: fn(), }, - decorators: [ - (Story) => ( -
- - -
- ), - ], + decorators: [fullHeightDecorator], }; export default meta; @@ -402,6 +394,7 @@ export const FiltersPanel: Story = { name: 'Toolbar — Filters Panel', parameters: { docs: { + source: { type: 'code' }, description: { story: ` ### Interactive Filter Example @@ -548,50 +541,45 @@ To add a new filter, just add one adapter to \`workflowFilters\` array. No other setIsOpen(false); }; - // Helper component for filter tab content (label + count badge) const TabLabel = ({ item }: { item: FilterNavItem }) => ( <> {item.label} {!!item.count && ( -
+ - + {item.count} -
+ )} ); return ( -
- {/* Toolbar with filter button */} -
+ + setIsOpen(true)} /> -
+ - {/* Filter chips (automatically generated from filter state) */} {filterChips.length > 0 && ( )} - {/* Table with enhanced columns (includes filter functions) */} - {/* Filter modal with two-column layout pattern */} - + -
+ Filters -
+
@@ -627,102 +615,16 @@ To add a new filter, just add one adapter to \`workflowFilters\` array. No other
-
+ ); }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Verify initial state: table shows all 12 rows - const getTableRows = () => canvas.getAllByRole('row').filter((row) => !row.querySelector('th')); - await expect(getTableRows()).toHaveLength(12); - - // 1. Open filter modal - const filterButton = canvas.getByRole('button', { name: /filter/i }); - await userEvent.click(filterButton); - - // 2. Verify all tabs exist - const statusTab = screen.getByRole('tab', { name: /status/i }); - const runningTab = screen.getByRole('tab', { name: /running\/completed/i }); - const lastEditedTab = screen.getByRole('tab', { name: /last edited/i }); - - // 3. Status filter - select "Active" and "Running" - await userEvent.click(statusTab); - - const activeCheckbox = screen.getByRole('checkbox', { name: /^active$/i }); - const runningCheckbox = screen.getByRole('checkbox', { name: /^running$/i }); - - await userEvent.click(activeCheckbox); - await userEvent.click(runningCheckbox); - - await expect(activeCheckbox).toBeChecked(); - await expect(runningCheckbox).toBeChecked(); - - // 4. Running/Completed filter - set range - await userEvent.click(runningTab); - - const [runningFrom, runningTo] = screen.getAllByRole('spinbutton'); - await userEvent.type(runningFrom as HTMLElement, '0'); - await userEvent.type(runningTo as HTMLElement, '50'); - - // 5. Last edited filter - select editor + time range - await userEvent.click(lastEditedTab); - - const editorCheckbox = screen.getByRole('checkbox', { name: /marry levin/i }); - const timeRangeRadio = screen.getByRole('radio', { name: /last 3 months/i }); - - await userEvent.click(editorCheckbox); - await userEvent.click(timeRangeRadio); - - await expect(editorCheckbox).toBeChecked(); - await expect(timeRangeRadio).toBeChecked(); - - // 6. Apply filters - await userEvent.click(screen.getByRole('button', { name: /apply/i })); - - // Verify chips appear - await expect(canvas.getByText(/status: active/i)).toBeInTheDocument(); - await expect(canvas.getByText(/status: running/i)).toBeInTheDocument(); - await expect(canvas.getByText(/running.*0.*50/i)).toBeInTheDocument(); - await expect(canvas.getByText(/editor: marry levin/i)).toBeInTheDocument(); - await expect(canvas.getByText(/last 3 months/i)).toBeInTheDocument(); - - // 7. Verify table is filtered - await expect(getTableRows().length).toBeLessThan(12); - - // 8. Re-open modal - verify filters preserved - await userEvent.click(filterButton); - - await userEvent.click(screen.getByRole('tab', { name: /status/i })); - - await expect(screen.getByRole('checkbox', { name: /^active$/i })).toBeChecked(); - await expect(screen.getByRole('checkbox', { name: /^running$/i })).toBeChecked(); - - await userEvent.click(screen.getByRole('button', { name: /apply/i })); - - // 9. Delete individual chip. DsTagFilter renders a `Delete tag` X inside each - // chip; the chip itself is also role="button", so the nested button is - // excluded from the accessibility tree — query by label directly. - const activeChip = canvas.getByRole('button', { name: /status: active/i }); - const deleteButton = within(activeChip).getByLabelText(/delete tag/i); - await userEvent.click(deleteButton); - - await expect(canvas.queryByRole('button', { name: /status: active/i })).not.toBeInTheDocument(); - - // 10. Clear all filters - await userEvent.click(canvas.getByRole('button', { name: /clear all/i })); - - await expect(canvas.queryByText(/status:/i)).not.toBeInTheDocument(); - - // Verify table shows all rows again - await expect(getTableRows()).toHaveLength(12); - }, }; export const Controlled: Story = { name: 'Toolbar — Controlled', parameters: { docs: { + source: { type: 'code' }, description: { story: ` ### Controlled Mode Example @@ -794,32 +696,31 @@ The debug panel below shows the current filter state as JSON. {item.label} {!!item.count && ( -
+ - + {item.count} -
+ )} ); return ( -
- {/* Debug panel showing external state */} -
+ + External Filter State (controlled):
{JSON.stringify(appliedFilters, null, 2) || '{}'}
-
+ -
+ setIsOpen(true)} /> -
+ {filterChips.length > 0 && ( @@ -827,12 +728,12 @@ The debug panel below shows the current filter state as JSON. - + -
+ Filters -
+
@@ -868,41 +769,7 @@ The debug panel below shows the current filter state as JSON.
-
+ ); }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Verify initial state: debug panel shows empty object - const debugPanel = canvas.getByText('External Filter State (controlled):'); - await expect(debugPanel).toBeInTheDocument(); - await expect(canvas.getByText('{}')).toBeInTheDocument(); - - // 1. Open filter modal and apply a filter - const filterButton = canvas.getByRole('button', { name: /filter/i }); - await userEvent.click(filterButton); - - // Select Active status - const statusTab = screen.getByRole('tab', { name: /status/i }); - await userEvent.click(statusTab); - - const activeCheckbox = screen.getByRole('checkbox', { name: /^active$/i }); - await userEvent.click(activeCheckbox); - - // Apply - await userEvent.click(screen.getByRole('button', { name: /apply/i })); - - // 2. Verify external state is updated (debug panel shows filter) - await expect(canvas.getByText(/"status"/)).toBeInTheDocument(); - - // 3. Verify chip appears - await expect(canvas.getByText(/status: active/i)).toBeInTheDocument(); - - // 4. Clear all and verify state resets - await userEvent.click(canvas.getByRole('button', { name: /clear all/i })); - - await expect(canvas.getByText('{}')).toBeInTheDocument(); - await expect(canvas.queryByText(/status: active/i)).not.toBeInTheDocument(); - }, }; diff --git a/packages/design-system/tests/storybook/docs-snippets.docs.test.ts b/packages/design-system/tests/storybook/docs-snippets.docs.test.ts index 995c01acb..b56a81d9a 100644 --- a/packages/design-system/tests/storybook/docs-snippets.docs.test.ts +++ b/packages/design-system/tests/storybook/docs-snippets.docs.test.ts @@ -38,6 +38,7 @@ const COMPONENTS = [ 'smart-tabs', 'stack', 'stepper', + 'table', 'tabs', 'text-input', 'textarea', From f32b9fb1bac024515dfe866f36195173a885c9c1 Mon Sep 17 00:00:00 2001 From: vpolessky Date: Fri, 4 Sep 2026 12:31:24 +0200 Subject: [PATCH 2/3] chore(design-system): add cleanup sb table [AR-78174] --- .../ds-main-menu/__tests__/ds-main-menu.browser.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/design-system/src/components/ds-main-menu/__tests__/ds-main-menu.browser.test.tsx b/packages/design-system/src/components/ds-main-menu/__tests__/ds-main-menu.browser.test.tsx index 6fbc191ba..916623ffe 100644 --- a/packages/design-system/src/components/ds-main-menu/__tests__/ds-main-menu.browser.test.tsx +++ b/packages/design-system/src/components/ds-main-menu/__tests__/ds-main-menu.browser.test.tsx @@ -278,7 +278,8 @@ describe('DsMainMenu — tile and utility link behavior', () => { const comingSoonTile = page.getByRole('button', { name: 'Coming soon app' }); const badge = comingSoonTile.element().querySelector('[class*="badge"]') as HTMLElement; - await userEvent.hover(badge); + await comingSoonTile.hover(); + await page.elementLocator(badge).hover(); await expect.element(page.getByRole('tooltip', { name: COMING_SOON_TOOLTIP })).toBeVisible(); }); From 3e21b484795e5c97af45ff43acc0775492f76c89 Mon Sep 17 00:00:00 2001 From: vpolessky Date: Mon, 7 Sep 2026 14:33:23 +0200 Subject: [PATCH 3/3] chore(design-system): split ds-table docs golden per manifest component --- .agents/skills/docs-tests/SKILL.md | 2 + .../ds-table-active-row.docs.snap | 330 + .../ds-table-column-groups.docs.snap | 904 +++ .../__snapshots__/ds-table-columns.docs.snap | 309 + .../__snapshots__/ds-table-editable.docs.snap | 485 ++ .../ds-table-expansion.docs.snap | 391 + .../__snapshots__/ds-table-filters.docs.snap | 219 + .../__snapshots__/ds-table-loading.docs.snap | 382 + .../ds-table-resizable-columns.docs.snap | 939 +++ .../ds-table-row-actions.docs.snap | 1223 ++++ .../ds-table-selection.docs.snap | 575 ++ .../ds-table-virtualized.docs.snap | 640 ++ .../__snapshots__/ds-table.docs.snap | 6410 +---------------- .../storybook/docs-snippets.docs.test.ts | 41 +- 14 files changed, 6436 insertions(+), 6414 deletions(-) create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-active-row.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-column-groups.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-columns.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-editable.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-expansion.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-filters.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-loading.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-resizable-columns.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-row-actions.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-selection.docs.snap create mode 100644 packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-virtualized.docs.snap diff --git a/.agents/skills/docs-tests/SKILL.md b/.agents/skills/docs-tests/SKILL.md index 05244e55b..f06369ba8 100644 --- a/.agents/skills/docs-tests/SKILL.md +++ b/.agents/skills/docs-tests/SKILL.md @@ -37,6 +37,8 @@ ds-{name}/__tests__/__snapshots__/ Coverage is centralized in the global test runner; each component gets one aggregated golden snapshot file colocated in its `__tests__/__snapshots__/` folder. +**Split folders** (listed in `SPLIT_PER_MANIFEST`): a folder whose stories fan out into many story titles (e.g. `ds-table` → `Components/Table`, `Components/Table/Selection`, …) would produce one unnavigable multi-thousand-line golden. Those write **one golden per manifest component**, named from the title-derived id (`components-table-selection` → `ds-table-selection.docs.snap`), colocated in the same `__tests__/__snapshots__/` folder. + ## Opt in a component Add one kebab folder suffix (the `ds-` prefix is implied) to the `COMPONENTS` allowlist in [`docs-snippets.docs.test.ts`](../../../packages/design-system/tests/storybook/docs-snippets.docs.test.ts): diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-active-row.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-active-row.docs.snap new file mode 100644 index 000000000..1d0d9fba0 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-active-row.docs.snap @@ -0,0 +1,330 @@ +# DsTable docs snippets + +## Active Row + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const ActiveRow = () => } + onRowClick={fn()} + activeRowId="3" />; + +## Active Row with Drawer + +### Show code +{ + name: 'Active Row with Drawer', + args: { + data: defaultData.slice(0, 10) + }, + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [selectedPerson, setSelectedPerson] = useState(null); + const activeRowId = selectedPerson?.id; + const isDrawerOpen = !!activeRowId; + const handleRowClick = (person: Person) => { + const isSameRow = activeRowId === person.id; + setSelectedPerson(isSameRow ? null : person); + }; + return <> + + + { + if (!open) { + setSelectedPerson(null); + } + }} columns={4} position="end"> + {selectedPerson &&
+ + + Person Details + setSelectedPerson(null)} /> + + + + + + Full Name + + + {selectedPerson.firstName} {selectedPerson.lastName} + + + + + + Age + + {selectedPerson.age} years old + + + + + Visits + + {selectedPerson.visits} visits + + + + + Status + + + {selectedPerson.status.charAt(0).toUpperCase() + selectedPerson.status.slice(1)} + + + + + + Profile Progress + + + + + +
} +
+ ; + } +} + +### MCP manifest +const WithDrawerAndActiveRow = () => { + const [selectedPerson, setSelectedPerson] = useState(null); + + const activeRowId = selectedPerson?.id; + const isDrawerOpen = !!activeRowId; + + const handleRowClick = (person: Person) => { + const isSameRow = activeRowId === person.id; + + setSelectedPerson(isSameRow ? null : person); + }; + + return ( + <> + } + activeRowId={activeRowId} + onRowClick={handleRowClick} /> + { + if (!open) { + setSelectedPerson(null); + } + }} + columns={4} + position="end"> + {selectedPerson && ( +
+ + + Person Details + setSelectedPerson(null)} + /> + + + + + + Full Name + + + {selectedPerson.firstName} {selectedPerson.lastName} + + + + + + Age + + {selectedPerson.age} years old + + + + + Visits + + {selectedPerson.visits} visits + + + + + Status + + + {selectedPerson.status.charAt(0).toUpperCase() + selectedPerson.status.slice(1)} + + + + + + Profile Progress + + + + + +
+ )} +
+ + ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-column-groups.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-column-groups.docs.snap new file mode 100644 index 000000000..fae275784 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-column-groups.docs.snap @@ -0,0 +1,904 @@ +# DsTable docs snippets + +## Default + +### Show code + {}, + header: 'First Name', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + } + ], + header: 'Identity', + id: 'identity', + meta: { + group: { + collapsible: true, + defaultCollapsed: false + } + } + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ], + header: 'Activity', + id: 'activity', + meta: { + group: { + collapsible: true + } + } + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + selectable + stickyHeader +/> + +### MCP manifest +const Default = () => } />; + +## Initially Collapsed + +### Show code + {}, + header: 'First Name', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + } + ], + header: 'Identity', + id: 'identity', + meta: { + group: { + collapsible: true, + defaultCollapsed: true + } + } + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ], + header: 'Activity', + id: 'activity', + meta: { + group: { + collapsible: true + } + } + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + selectable + stickyHeader +/> + +### MCP manifest +const InitiallyCollapsed = () => } />; + +## Controlled + +### Show code + {}, + header: 'First Name', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + } + ], + header: 'Identity', + id: 'identity', + meta: { + group: { + collapsible: true, + defaultCollapsed: false + } + } + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ], + header: 'Activity', + id: 'activity', + meta: { + group: { + collapsible: true + } + } + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onCollapsedColumnGroupsChange={() => {}} + selectable + stickyHeader +/> + +### MCP manifest +const Controlled = () => { + const [collapsed, setCollapsed] = useState(['activity']); + + return ( + } + collapsedColumnGroups={collapsed} + onCollapsedColumnGroupsChange={setCollapsed} /> + ); +}; + +## With Controls + +### Show code + {}, + header: 'First Name', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + } + ], + header: 'Identity', + id: 'identity', + meta: { + group: { + collapsible: true, + defaultCollapsed: false + } + } + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + meta: { + keepVisibleWhenCollapsed: true + } + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ], + header: 'Activity', + id: 'activity', + meta: { + group: { + collapsible: true + } + } + } + ]} + controls={RED version V2Import REDExport REDActions} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + selectable + stickyHeader +/> + +### MCP manifest +const WithControls = () => } + controls={( + + RED version V2 + + + + Import RED + + + Export RED + + Actions + + )} />; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-columns.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-columns.docs.snap new file mode 100644 index 000000000..3e1faac80 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-columns.docs.snap @@ -0,0 +1,309 @@ +# DsTable docs snippets + +## Progress as Infographic + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader +/> + +### MCP manifest +const WithProgressInfographic = () => { + if ('accessorKey' in col && col.accessorKey === 'progress') { + return { + ...col, + header: 'Profile Progress', + cell: (info) => , + }; + } else if ('accessorKey' in col && col.accessorKey === 'status') { + return { + ...col, + header: 'Status', + cell: (info) => ( + + {info.getValue() as string} + + ), + }; + } + return col; + })} + data={defaultData} + stickyHeader + bordered + fullWidth + expandable={false} + emptyState={} + onRowClick={fn()} />; + +## Column Hiding + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const columnsToToggle = [{ + id: 'age', + label: 'Age' + }, { + id: 'visits', + label: 'Visits' + }, { + id: 'status', + label: 'Status' + }, { + id: 'progress', + label: 'Profile Progress' + }]; + const [columnVisibility, setColumnVisibility] = useState({ + age: true, + visits: true, + status: true, + progress: true + }); + const toggleColumn = (columnId: string) => { + setColumnVisibility(prev => ({ + ...prev, + [columnId]: !prev[columnId] + })); + }; + return + + {columnsToToggle.map(column => toggleColumn(column.id)} />)} + + + + ; + } +} + +### MCP manifest +const ColumnHiding = () => { + const columnsToToggle = [ + { id: 'age', label: 'Age' }, + { id: 'visits', label: 'Visits' }, + { id: 'status', label: 'Status' }, + { id: 'progress', label: 'Profile Progress' }, + ]; + const [columnVisibility, setColumnVisibility] = useState({ + age: true, + visits: true, + status: true, + progress: true, + }); + + const toggleColumn = (columnId: string) => { + setColumnVisibility((prev) => ({ + ...prev, + [columnId]: !prev[columnId], + })); + }; + + return ( + + + {columnsToToggle.map((column) => ( + toggleColumn(column.id)} + /> + ))} + + } + onRowClick={fn()} + columnVisibility={columnVisibility} + onColumnVisibilityChange={setColumnVisibility} /> + + ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-editable.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-editable.docs.snap new file mode 100644 index 000000000..e5ef23ff2 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-editable.docs.snap @@ -0,0 +1,485 @@ +# DsTable docs snippets + +## Editable + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data, setData] = useState(defaultData); + const statusOptions = [{ + label: 'Single', + value: 'single' + }, { + label: 'Relationship', + value: 'relationship' + }, { + label: 'Complicated', + value: 'complicated' + }]; + const statusLabels: Record = { + single: 'Single', + relationship: 'Relationship', + complicated: 'Complicated' + }; + const progressPresets = [25, 50, 75, 100]; + const ProgressEditor = ({ + cellContext + }: { + cellContext: CellContext; + }) => { + const { + value, + setValue, + error + } = useCellEditor({ + cellContext + }); + return +
+ {progressPresets.map(preset => setValue(preset)} />)} +
+ +
; + }; + const columns: ColumnDef[] = [{ + accessorKey: 'id', + header: 'ID', + size: 60, + cell: info => {info.getValue() as string} + }, { + accessorKey: 'firstName', + header: 'First Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'lastName', + header: 'Last Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'age', + header: 'Age', + size: 100, + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'visits', + header: 'Visits', + size: 100, + cell: info => info.getValue(), + editCell: (info: CellContext) => , + editDisabled: (info: CellContext) => { + if (info.row.original.status === 'complicated') { + return { + reason: 'Visits are locked while the status is “Complicated”.' + }; + } + if (info.row.original.age >= 40) { + return true; + } + return false; + } + }, { + accessorKey: 'status', + header: 'Status', + size: 160, + cell: info => statusLabels[info.getValue() as Status], + editCell: (info: CellContext) => + }, { + accessorKey: 'progress', + header: 'Profile Progress', + cell: info => {`${String(info.getValue())}%`}, + editCell: (info: CellContext) => + }]; + return { + setData(rows => rows.map(person => person.id === row.id ? { + ...person, + [columnId]: value + } : person)); + }} />; + } +} + +### MCP manifest +const Editable = () => { + const [data, setData] = useState(defaultData); + + const statusOptions = [ + { label: 'Single', value: 'single' }, + { label: 'Relationship', value: 'relationship' }, + { label: 'Complicated', value: 'complicated' }, + ]; + + const statusLabels: Record = { + single: 'Single', + relationship: 'Relationship', + complicated: 'Complicated', + }; + + const progressPresets = [25, 50, 75, 100]; + + const ProgressEditor = ({ cellContext }: { cellContext: CellContext }) => { + const { value, setValue, error } = useCellEditor({ cellContext }); + + return ( + +
+ {progressPresets.map((preset) => ( + setValue(preset)} + /> + ))} +
+ +
+ ); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + size: 60, + cell: (info) => {info.getValue() as string}, + }, + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'age', + header: 'Age', + size: 100, + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'visits', + header: 'Visits', + size: 100, + cell: (info) => info.getValue(), + editCell: (info: CellContext) => , + editDisabled: (info: CellContext) => { + if (info.row.original.status === 'complicated') { + return { reason: 'Visits are locked while the status is “Complicated”.' }; + } + if (info.row.original.age >= 40) { + return true; + } + return false; + }, + }, + { + accessorKey: 'status', + header: 'Status', + size: 160, + cell: (info) => statusLabels[info.getValue() as Status], + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + cell: (info) => {`${String(info.getValue())}%`}, + editCell: (info: CellContext) => , + }, + ]; + + return ( + } + data={data} + columns={columns} + selectable + onRowClick={fn()} + primaryRowActions={[{ icon: 'delete_outline', label: 'Delete', onClick: fn() }]} + secondaryRowActions={[{ icon: 'info', label: 'Details', onClick: fn() }]} + onCellEdit={(row, columnId, value) => { + setData((rows) => + rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), + ); + }} /> + ); +}; + +## Live Validation + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data, setData] = useState(defaultData); + const personSchema = z.object({ + firstName: z.string().trim().min(1, 'First name is required').max(50, 'Max 50 characters'), + lastName: z.string().trim().min(1, 'Last name is required').max(50, 'Max 50 characters') + }); + const validateField = (columnId: string, value: unknown): string | null => { + const shape: Record = personSchema.shape; + const fieldSchema = shape[columnId]; + if (!fieldSchema) { + return null; + } + const result = fieldSchema.safeParse(value); + return result.success ? null : result.error.issues[0]?.message ?? null; + }; + const columns: ColumnDef[] = [{ + accessorKey: 'id', + header: 'ID', + size: 60, + cell: info => {info.getValue() as string} + }, { + accessorKey: 'firstName', + header: 'First Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'lastName', + header: 'Last Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }]; + return validateField(columnId, value)} onCellEdit={(row, columnId, value) => { + setData(rows => rows.map(person => person.id === row.id ? { + ...person, + [columnId]: value + } : person)); + }} />; + } +} + +### MCP manifest +const LiveValidation = () => { + const [data, setData] = useState(defaultData); + + const personSchema = z.object({ + firstName: z.string().trim().min(1, 'First name is required').max(50, 'Max 50 characters'), + lastName: z.string().trim().min(1, 'Last name is required').max(50, 'Max 50 characters'), + }); + + const validateField = (columnId: string, value: unknown): string | null => { + const shape: Record = personSchema.shape; + const fieldSchema = shape[columnId]; + if (!fieldSchema) { + return null; + } + const result = fieldSchema.safeParse(value); + return result.success ? null : (result.error.issues[0]?.message ?? null); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + size: 60, + cell: (info) => {info.getValue() as string}, + }, + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + ]; + + return ( + } + data={data} + columns={columns} + onCellValidate={(_row, columnId, value) => validateField(columnId, value)} + onCellEdit={(row, columnId, value) => { + setData((rows) => + rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), + ); + }} /> + ); +}; + +## Validate on Async Save + +### Show code +{ + name: 'Validate on Async Save', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data, setData] = useState(defaultData); + const saveFirstName = (value: string, signal: AbortSignal): Promise => new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + const trimmed = value.trim(); + if (trimmed.length === 0) { + resolve('First name is required'); + return; + } + if (trimmed.toLowerCase() === 'taken') { + resolve('This name is already taken'); + return; + } + resolve(null); + }, 900); + signal.addEventListener('abort', () => { + clearTimeout(timeout); + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + const columns: ColumnDef[] = [{ + accessorKey: 'id', + header: 'ID', + size: 60, + cell: info => {info.getValue() as string} + }, { + accessorKey: 'firstName', + header: 'First Name', + cell: info => info.getValue(), + editCell: (info: CellContext) => + }, { + accessorKey: 'lastName', + header: 'Last Name', + cell: info => info.getValue() + }]; + return { + const error = await saveFirstName(value as string, signal); + if (error !== null) { + return error; + } + setData(rows => rows.map(person => person.id === row.id ? { + ...person, + [columnId]: value + } : person)); + }} />; + } +} + +### MCP manifest +const ValidateOnAsyncSave = () => { + const [data, setData] = useState(defaultData); + + const saveFirstName = (value: string, signal: AbortSignal): Promise => + new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + const trimmed = value.trim(); + if (trimmed.length === 0) { + resolve('First name is required'); + return; + } + if (trimmed.toLowerCase() === 'taken') { + resolve('This name is already taken'); + return; + } + resolve(null); + }, 900); + + signal.addEventListener('abort', () => { + clearTimeout(timeout); + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + + const columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + size: 60, + cell: (info) => {info.getValue() as string}, + }, + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + editCell: (info: CellContext) => ( + + ), + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + }, + ]; + + return ( + } + data={data} + columns={columns} + onCellEdit={async (row, columnId, value, signal) => { + const error = await saveFirstName(value as string, signal); + if (error !== null) { + return error; + } + setData((rows) => + rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), + ); + }} /> + ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-expansion.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-expansion.docs.snap new file mode 100644 index 000000000..86a900927 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-expansion.docs.snap @@ -0,0 +1,391 @@ +# DsTable docs snippets + +## Expandable + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + } + ]} + emptyState={} + expandable={() => {}} + fullWidth + onRowClick={() => {}} + renderExpandedRow={() => {}} + stickyHeader +/> + +### MCP manifest +const Expandable = () => row.firstName !== 'Tanner'} + emptyState={} + onRowClick={fn()} + renderExpandedRow={(row) => ( + + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + + + + )} />; + +## Custom Expander Column Width + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + } + ]} + emptyState={} + expandable + expandableColumnWidth={48} + fullWidth + onRowClick={() => {}} + renderExpandedRow={() => {}} + stickyHeader +/> + +### MCP manifest +const CustomExpanderColumnWidth = () => } + onRowClick={fn()} + expandableColumnWidth={48} + renderExpandedRow={(row) => ( + + Details for {row.firstName} + + )} />; + +## Programmatic Expansion + +### Show code +{ + args: { + data: defaultData.slice(0, 5), + expandable: row => row.firstName !== 'Tanner', + renderExpandedRow: row => + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + }, + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const tableRef = useRef>(null); + const [expandedRows, setExpandedRows] = useState([]); + const expandRow = (rowId: string) => { + tableRef.current?.expandRow(rowId); + setExpandedRows(prev => prev.includes(rowId) ? prev : [...prev, rowId]); + }; + const expandAllRows = () => { + tableRef.current?.expandAllRows(); + const expandableRowIds = defaultData.slice(0, 5).filter(row => row.firstName !== 'Tanner').map(row => row.id); + setExpandedRows(expandableRowIds); + }; + const collapseAllRows = () => { + tableRef.current?.collapseAllRows(); + setExpandedRows([]); + }; + const expandFirstThreeRows = () => { + const firstThreeIds = ['2', '3', '4']; + tableRef.current?.expandRows(firstThreeIds); + setExpandedRows(firstThreeIds); + }; + return + + Expanded rows: {expandedRows.length > 0 ? expandedRows.join(', ') : 'None'} + + + + expandRow('2')}> + Expand Kevin + + expandRow('3')}> + Expand John + + expandRow('4')}> + Expand Jane + + + Expand All + + + Collapse All + + + Expand First 3 Expandable + + + + + ; + } +} + +### MCP manifest +const ProgrammaticExpansion = () => { + const tableRef = useRef>(null); + const [expandedRows, setExpandedRows] = useState([]); + + const expandRow = (rowId: string) => { + tableRef.current?.expandRow(rowId); + setExpandedRows((prev) => (prev.includes(rowId) ? prev : [...prev, rowId])); + }; + + const expandAllRows = () => { + tableRef.current?.expandAllRows(); + const expandableRowIds = defaultData + .slice(0, 5) + .filter((row) => row.firstName !== 'Tanner') + .map((row) => row.id); + setExpandedRows(expandableRowIds); + }; + + const collapseAllRows = () => { + tableRef.current?.collapseAllRows(); + setExpandedRows([]); + }; + + const expandFirstThreeRows = () => { + const firstThreeIds = ['2', '3', '4']; + tableRef.current?.expandRows(firstThreeIds); + setExpandedRows(firstThreeIds); + }; + + return ( + + Expanded rows: {expandedRows.length > 0 ? expandedRows.join(', ') : 'None'} + + + expandRow('2')}>Expand Kevin + + expandRow('3')}>Expand John + + expandRow('4')}>Expand Jane + + Expand All + + Collapse All + + Expand First 3 Expandable + + + row.firstName !== 'Tanner'} + emptyState={} + onRowClick={fn()} + renderExpandedRow={(row) => ( + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + )} + ref={tableRef} /> + + ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-filters.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-filters.docs.snap new file mode 100644 index 000000000..906a9d7e4 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-filters.docs.snap @@ -0,0 +1,219 @@ +# DsTable docs snippets + +## Per-Column — Popover + +### Show code +import type { ColumnDef } from '@tanstack/react-table'; +import { + DsTable, + DsTagFilter, + createCheckboxFilterAdapter, + useTableFilters, +} from '@drivenets/design-system'; + +type DeviceRow = { + id: string; + name: string; + type: 'PP-LGX' | 'ME10' | 'OLT'; + site: string; + status: 'active' | 'warning' | 'failed'; +}; + +const typeOptions = [ + { value: 'PP-LGX' as const, label: 'PP-LGX' }, + { value: 'ME10' as const, label: 'ME10' }, + { value: 'OLT' as const, label: 'OLT' }, +]; + +const siteOptions = [ + { value: 'NYC-DC1', label: 'NYC-DC1' }, + { value: 'LON-DC2', label: 'LON-DC2' }, + { value: 'SFO-DC3', label: 'SFO-DC3' }, +]; + +const typeFilter = createCheckboxFilterAdapter({ + id: 'type', + label: 'Type', + items: typeOptions, + searchable: true, + selectAll: true, +}); + +const siteFilter = createCheckboxFilterAdapter({ + id: 'site', + label: 'Site', + items: siteOptions, + searchable: true, + selectAll: true, +}); + +const columns: ColumnDef[] = [ + { id: 'name', accessorKey: 'name', header: 'Name' }, + { id: 'type', accessorKey: 'type', header: 'Type', meta: { filter: { adapterId: 'type' } } }, + { id: 'site', accessorKey: 'site', header: 'Site', meta: { filter: { adapterId: 'site' } } }, + { id: 'status', accessorKey: 'status', header: 'Status' }, +]; + +function DevicesTable({ rows }: { rows: DeviceRow[] }) { + const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ + filterAdapters: [typeFilter, siteFilter], + baseColumns: columns, + }); + + return ( + <> + {filterChips.length > 0 && ( + + )} + + + ); +} + +### MCP manifest +const ColumnFilters = function Render() { + const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ + filterAdapters: adapters, + baseColumns, + }); + + return ( + + {filterChips.length > 0 && ( + + )} + + + + ); +}; + +## Per-Column — Controlled + +### Show code +import { useState } from 'react'; +import type { ColumnDef } from '@tanstack/react-table'; +import { + DsTable, + DsTagFilter, + type CheckboxFilterItem, + type FilterState, + createCheckboxFilterAdapter, + useTableFilters, +} from '@drivenets/design-system'; + +type DeviceRow = { + id: string; + name: string; + type: 'PP-LGX' | 'ME10' | 'OLT'; + site: string; +}; + +const typeFilter = createCheckboxFilterAdapter({ + id: 'type', + label: 'Type', + items: [ + { value: 'PP-LGX', label: 'PP-LGX' }, + { value: 'ME10', label: 'ME10' }, + { value: 'OLT', label: 'OLT' }, + ], + searchable: true, + selectAll: true, +}); + +const columns: ColumnDef[] = [ + { id: 'name', accessorKey: 'name', header: 'Name' }, + { id: 'type', accessorKey: 'type', header: 'Type', meta: { filter: { adapterId: 'type' } } }, + { id: 'site', accessorKey: 'site', header: 'Site' }, +]; + +function ControlledDevicesTable({ rows }: { rows: DeviceRow[] }) { + // Source of truth lives outside the hook (URL, server, parent store, etc.). + const [appliedFilters, setAppliedFilters] = useState>({ + type: [{ value: 'PP-LGX', label: 'PP-LGX' }], + }); + + const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ + filterAdapters: [typeFilter], + baseColumns: columns, + appliedFilters, + onFiltersChange: setAppliedFilters, + }); + + return ( + <> + {filterChips.length > 0 && ( + + )} + + + ); +} + +### MCP manifest +const ControlledColumnFilters = function Render() { + const [appliedFilters, setAppliedFilters] = useState>({ + type: [{ value: 'PP-LGX', label: 'PP-LGX' }], + }); + + const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ + filterAdapters: adapters, + baseColumns, + appliedFilters, + onFiltersChange: setAppliedFilters, + }); + + return ( + + {filterChips.length > 0 && ( + + )} + + + + ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-loading.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-loading.docs.snap new file mode 100644 index 000000000..d2afe164b --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-loading.docs.snap @@ -0,0 +1,382 @@ +# DsTable docs snippets + +## Loading + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + loading + stickyHeader +/> + +### MCP manifest +const Loading = () => } + loading />; + +## Custom Loading Cell + +### Show code + {}, + header: 'First Name', + loadingCell: () => {} + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + loading + stickyHeader +/> + +### MCP manifest +const CustomLoadingCell = () => } + loading />; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-resizable-columns.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-resizable-columns.docs.snap new file mode 100644 index 000000000..e7ffb5266 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-resizable-columns.docs.snap @@ -0,0 +1,939 @@ +# DsTable docs snippets + +## Default + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + resizableColumns + stickyHeader +/> + +### MCP manifest +const Default = () => } />; + +## Fixed And Fill + +### Show code + {}, + header: 'First Name', + size: 200 + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name', + size: 200 + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age', + size: 120 + }, + { + accessorKey: 'visits', + cell: () => {}, + enableResizing: false, + header: 'Visits', + size: 120 + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + resizableColumns + stickyHeader +/> + +### MCP manifest +const FixedAndFill = () => info.getValue(), size: 200 }, + { accessorKey: 'lastName', header: 'Last Name', cell: (info) => info.getValue(), size: 200 }, + { accessorKey: 'age', header: 'Age', cell: (info) => info.getValue(), size: 120 }, + { + accessorKey: 'visits', + header: 'Visits', + cell: (info) => info.getValue(), + enableResizing: false, + size: 120, + }, + { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, + ]} + resizableColumns + stickyHeader + bordered + fullWidth + emptyState={} />; + +## Grouped Columns + +### Show code + {}, + header: 'First Name', + maxSize: 280, + minSize: 80 + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + } + ], + header: 'Identity', + id: 'identity' + }, + { + columns: [ + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Progress' + } + ], + header: 'Activity', + id: 'activity' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + resizableColumns + stickyHeader +/> + +### MCP manifest +const GroupedColumns = () => info.getValue(), + minSize: 80, + maxSize: 280, + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + }, + ], + }, + { + id: 'activity', + header: 'Activity', + columns: [ + { accessorKey: 'visits', header: 'Visits', cell: (info) => info.getValue() }, + { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, + { + accessorKey: 'progress', + header: 'Progress', + cell: (info) => `${String(info.getValue())}%`, + }, + ], + }, + ]} + resizableColumns + stickyHeader + bordered + fullWidth + emptyState={} />; + +## Min And Max Size + +### Show code + {}, + header: 'First Name', + maxSize: 280, + minSize: 80, + size: 200 + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name', + minSize: 120, + size: 200 + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age', + maxSize: 180, + size: 120 + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits', + size: 120 + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + resizableColumns + stickyHeader +/> + +### MCP manifest +const MinAndMaxSize = () => info.getValue(), + size: 200, + minSize: 80, + maxSize: 280, + }, + { + accessorKey: 'lastName', + header: 'Last Name', + cell: (info) => info.getValue(), + size: 200, + minSize: 120, + }, + { + accessorKey: 'age', + header: 'Age', + cell: (info) => info.getValue(), + size: 120, + maxSize: 180, + }, + { accessorKey: 'visits', header: 'Visits', cell: (info) => info.getValue(), size: 120 }, + { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, + ]} + resizableColumns + stickyHeader + bordered + fullWidth + emptyState={} />; + +## Persisted widths + +### Show code +{ + name: 'Persisted widths', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const STORAGE_KEY = 'storybook.ds-table.resizable.persisted-widths'; + const [tableKey, setTableKey] = useState(0); + let persistedWidths: Record; + try { + persistedWidths = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record; + } catch { + persistedWidths = {}; + } + return + + Persisted widths + + Persist the map from onColumnSizingChange, then pass it back as the columnSizing prop on the next + mount to restore widths — no need to stamp columnDef.size. Columns absent from the map are + measured automatically. Resize a column, refresh the story, and the widths come back. Reset stored + widths remounts from the default layout. + + + + + { + localStorage.removeItem(STORAGE_KEY); + setTableKey(key => key + 1); + }}> + Reset stored widths + + + + { + localStorage.setItem(STORAGE_KEY, JSON.stringify(columnSizing)); + }} /> + ; + } +} + +### MCP manifest +const PersistedWidths = () => { + const STORAGE_KEY = 'storybook.ds-table.resizable.persisted-widths'; + const [tableKey, setTableKey] = useState(0); + + let persistedWidths: Record; + try { + persistedWidths = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record; + } catch { + persistedWidths = {}; + } + + return ( + + + Persisted widths + Persist the map from onColumnSizingChange, then pass it back as the columnSizing prop on the next + mount to restore widths — no need to stamp columnDef.size. Columns absent from the map are + measured automatically. Resize a column, refresh the story, and the widths come back. Reset stored + widths remounts from the default layout. + + + + { + localStorage.removeItem(STORAGE_KEY); + setTableKey((key) => key + 1); + }}>Reset stored widths + + + } + key={tableKey} + columnSizing={persistedWidths} + onColumnSizingChange={(columnSizing) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(columnSizing)); + }} /> + + ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-row-actions.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-row-actions.docs.snap new file mode 100644 index 000000000..03b1505b5 --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-row-actions.docs.snap @@ -0,0 +1,1223 @@ +# DsTable docs snippets + +## Reorderable + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + } + ]} + emptyState={} + fullWidth + onOrderChange={() => {}} + onRowClick={() => {}} + reorderable + stickyHeader +/> + +### MCP manifest +const Reorderable = () => } + onRowClick={fn()} + reorderable + onOrderChange={fn()} />; + +## Custom Reorder Column Width + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + } + ]} + emptyState={} + fullWidth + onOrderChange={() => {}} + onRowClick={() => {}} + reorderable + reorderableColumnWidth={80} + stickyHeader +/> + +### MCP manifest +const CustomReorderColumnWidth = () => } + onRowClick={fn()} + reorderable + reorderableColumnWidth={80} + onOrderChange={fn()} />; + +## With Row Actions + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: () => {} + }, + { + disabled: () => {}, + icon: 'open_in_new', + label: 'Open in New Window', + onClick: () => {} + } + ]} + secondaryRowActions={[ + { + className: '_destructiveAction_12uvr_138', + disabled: () => {}, + icon: 'delete_outline', + label: 'Delete', + onClick: () => {}, + tooltip: 'Delete this row' + }, + { + icon: 'info', + label: 'Details', + onClick: () => {}, + tooltip: 'Show details' + }, + { + icon: 'call', + label: () => {}, + onClick: () => {} + } + ]} + stickyHeader +/> + +### MCP manifest +const WithRowActions = () => } + onRowClick={fn()} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: editClickHandler, + }, + { + icon: 'open_in_new', + label: 'Open in New Window', + disabled: (data) => data.firstName === 'Tanner', + onClick: openInNewWindowClickHandler, + }, + ]} + secondaryRowActions={[ + { + icon: 'delete_outline', + label: 'Delete', + tooltip: 'Delete this row', + disabled: (data) => data.status === 'single', + className: styles.destructiveAction, + onClick: fn(), + }, + { + icon: 'info', + label: 'Details', + tooltip: 'Show details', + onClick: fn(), + }, + { + icon: 'call', + label: (row) => `Call ${row.firstName}`, + onClick: fn(), + }, + ]} />; + +## With Conditionally Hidden Actions + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: () => {} + }, + { + hidden: () => {}, + icon: 'open_in_new', + label: 'Open in New Window', + onClick: () => {} + } + ]} + secondaryRowActions={[ + { + hidden: () => {}, + icon: 'check_circle', + label: 'Approve', + onClick: () => {} + }, + { + hidden: () => {}, + icon: 'inventory_2', + label: 'Archive', + onClick: () => {} + }, + { + className: '_destructiveAction_12uvr_138', + hidden: () => {}, + icon: 'delete_outline', + label: 'Delete', + onClick: () => {} + }, + { + icon: 'info', + label: 'Details', + onClick: () => {} + } + ]} + stickyHeader +/> + +### MCP manifest +const WithConditionallyHiddenActions = () => } + onRowClick={fn()} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: fn(), + }, + { + icon: 'open_in_new', + label: 'Open in New Window', + // hidden on 'complicated' rows (e.g. cannot open a record in a bad state) + hidden: (data) => data.status === 'complicated', + onClick: fn(), + }, + ]} + secondaryRowActions={[ + { + icon: 'check_circle', + label: 'Approve', + // only shown on 'single' rows (pending approval) + hidden: (data) => data.status !== 'single', + onClick: fn(), + }, + { + icon: 'inventory_2', + label: 'Archive', + // only shown on 'relationship' rows (live records) + hidden: (data) => data.status !== 'relationship', + onClick: fn(), + }, + { + icon: 'delete_outline', + label: 'Delete', + // hidden on 'relationship' rows (cannot delete live records) + hidden: (data) => data.status === 'relationship', + className: styles.destructiveAction, + onClick: fn(), + }, + { + icon: 'info', + label: 'Details', + onClick: fn(), + }, + ]} />; + +## With Conditionally Disabled Actions + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: () => {} + }, + { + disabled: () => {}, + icon: 'open_in_new', + label: 'Open in New Window', + onClick: () => {} + } + ]} + secondaryRowActions={[ + { + className: '_destructiveAction_12uvr_138', + disabled: () => {}, + icon: 'delete_outline', + label: 'Delete', + onClick: () => {}, + tooltip: 'Delete this row' + }, + { + icon: 'info', + label: 'Details', + onClick: () => {}, + tooltip: 'Show details' + } + ]} + stickyHeader +/> + +### MCP manifest +const WithConditionallyDisabledActions = () => } + onRowClick={fn()} + primaryRowActions={[ + { + icon: 'edit', + label: 'Edit', + onClick: fn(), + }, + { + icon: 'open_in_new', + label: 'Open in New Window', + // disabled on Tanner's row (item stays visible but greyed out) + disabled: (data) => data.firstName === 'Tanner', + onClick: fn(), + }, + ]} + secondaryRowActions={[ + { + icon: 'delete_outline', + label: 'Delete', + tooltip: 'Delete this row', + // disabled on 'single' rows (destructive action guarded) + disabled: (data) => data.status === 'single', + className: styles.destructiveAction, + onClick: fn(), + }, + { + icon: 'info', + label: 'Details', + tooltip: 'Show details', + onClick: fn(), + }, + ]} />; + +## With Bulk Actions + +### Show code + {} + }, + { + icon: 'folder_open', + label: 'Folder', + onClick: () => {} + }, + { + icon: 'delete_outline', + label: 'Delete', + onClick: () => {} + } + ]} + bordered + columns={[ + { + accessorKey: 'firstName', + cell: () => {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + selectable + stickyHeader +/> + +### MCP manifest +const WithBulkActions = () => } + onRowClick={fn()} + selectable + actions={[ + { + icon: 'alarm', + label: 'Notify', + onClick: fn(), + }, + { + icon: 'folder_open', + label: 'Folder', + onClick: fn(), + }, + { + icon: 'delete_outline', + label: 'Delete', + onClick: fn(), + }, + ]} />; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-selection.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-selection.docs.snap new file mode 100644 index 000000000..f8fa32c5a --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-selection.docs.snap @@ -0,0 +1,575 @@ +# DsTable docs snippets + +## Selectable + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + onSelectionChange={() => {}} + selectable + stickyHeader +/> + +### MCP manifest +const Selectable = () => } + onRowClick={fn()} + selectable + onSelectionChange={fn()} />; + +## Custom Select Column Width + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[ + { + age: 33, + firstName: 'Tanner', + id: '1', + lastName: 'Linsley', + progress: 75, + status: 'single', + visits: 100 + }, + { + age: 28, + firstName: 'Kevin', + id: '2', + lastName: 'Fine', + progress: 50, + status: 'relationship', + visits: 200 + }, + { + age: 45, + firstName: 'John', + id: '3', + lastName: 'Doe', + progress: 90, + status: 'complicated', + visits: 50 + }, + { + age: 30, + firstName: 'Jane', + id: '4', + lastName: 'Smith', + progress: 60, + status: 'single', + visits: 150 + }, + { + age: 22, + firstName: 'Peter', + id: '5', + lastName: 'Jones', + progress: 30, + status: 'relationship', + visits: 250 + }, + { + age: 38, + firstName: 'Mary', + id: '6', + lastName: 'Jane', + progress: 85, + status: 'complicated', + visits: 80 + }, + { + age: 50, + firstName: 'David', + id: '7', + lastName: 'Williams', + progress: 40, + status: 'single', + visits: 120 + }, + { + age: 25, + firstName: 'Sarah', + id: '8', + lastName: 'Brown', + progress: 70, + status: 'relationship', + visits: 180 + }, + { + age: 41, + firstName: 'Michael', + id: '9', + lastName: 'Davis', + progress: 20, + status: 'complicated', + visits: 95 + }, + { + age: 36, + firstName: 'Emily', + id: '10', + lastName: 'Miller', + progress: 55, + status: 'single', + visits: 110 + }, + { + age: 29, + firstName: 'Daniel', + id: '11', + lastName: 'Wilson', + progress: 80, + status: 'relationship', + visits: 220 + }, + { + age: 48, + firstName: 'Olivia', + id: '12', + lastName: 'Moore', + progress: 15, + status: 'complicated', + visits: 65 + }, + { + age: 31, + firstName: 'James', + id: '13', + lastName: 'Taylor', + progress: 95, + status: 'single', + visits: 135 + }, + { + age: 27, + firstName: 'Sophia', + id: '14', + lastName: 'Anderson', + progress: 25, + status: 'relationship', + visits: 170 + }, + { + age: 43, + firstName: 'Robert', + id: '15', + lastName: 'Thomas', + progress: 50, + status: 'complicated', + visits: 88 + } + ]} + emptyState={} + fullWidth + onRowClick={() => {}} + onSelectionChange={() => {}} + selectable + selectableColumnWidth={48} + stickyHeader +/> + +### MCP manifest +const CustomSelectColumnWidth = () => } + onRowClick={fn()} + selectable + selectableColumnWidth={48} + onSelectionChange={fn()} />; + +## Programmatic Row Selection + +### Show code +{ + args: { + selectable: true, + showSelectAllCheckbox: false, + stickyHeader: true, + onSelectionChange: fn() + }, + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const tableRef = useRef>(null); + const [selectedRows, setSelectedRows] = useState([]); + const handleSelectionChange = (selection: Record) => { + setSelectedRows(Object.keys(selection)); + }; + return + + Selected rows: {selectedRows.length > 0 ? selectedRows.join(', ') : 'None'} + + + + tableRef.current?.selectRow('1')}> + Select Row 1 + + tableRef.current?.selectRow('2')}> + Select Row 2 + + tableRef.current?.selectRow('3')}> + Select Row 3 + + tableRef.current?.selectAllRows()}> + Select All + + tableRef.current?.deselectAllRows()}> + Deselect All + + tableRef.current?.selectRows(['1', '2', '3'])}> + Select First 3 Rows + + + + + ; + } +} + +### MCP manifest +const ProgrammaticRowSelection = () => { + const tableRef = useRef>(null); + const [selectedRows, setSelectedRows] = useState([]); + + const handleSelectionChange = (selection: Record) => { + setSelectedRows(Object.keys(selection)); + }; + + return ( + + Selected rows: {selectedRows.length > 0 ? selectedRows.join(', ') : 'None'} + + + tableRef.current?.selectRow('1')}>Select Row 1 + + tableRef.current?.selectRow('2')}>Select Row 2 + + tableRef.current?.selectRow('3')}>Select Row 3 + + tableRef.current?.selectAllRows()}>Select All + + tableRef.current?.deselectAllRows()}>Deselect All + + tableRef.current?.selectRows(['1', '2', '3'])}>Select First 3 Rows + + + } + onRowClick={fn()} + selectable + showSelectAllCheckbox={false} + ref={tableRef} + onSelectionChange={handleSelectionChange} /> + + ); +}; + +## Max N Selections + +### Show code +{ + name: 'Max N Selections', + args: { + showSelectAllCheckbox: false, + onSelectionChange: fn() + }, + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [rowSelection, setRowSelection] = useState>({}); + const maxSelections = 2; + const selectedCount = Object.keys(rowSelection).filter(id => rowSelection[id]).length; + const handleSelectionChange = (selection: Record) => { + setRowSelection(selection); + args.onSelectionChange?.(selection); + }; + return + + Selected: {selectedCount} / {maxSelections} + + + rowSelection[rowData.id] || selectedCount < maxSelections} /> + ; + } +} + +### MCP manifest +const MaxSelectionLimit = () => { + const [rowSelection, setRowSelection] = useState>({}); + const maxSelections = 2; + + const selectedCount = Object.keys(rowSelection).filter((id) => rowSelection[id]).length; + + const handleSelectionChange = (selection: Record) => { + setRowSelection(selection); + args.onSelectionChange?.(selection); + }; + + return ( + + Selected: {selectedCount}/ {maxSelections} + + } + onRowClick={fn()} + showSelectAllCheckbox={false} + onSelectionChange={handleSelectionChange} + selectable={(rowData) => rowSelection[rowData.id] || selectedCount < maxSelections} /> + + ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-virtualized.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-virtualized.docs.snap new file mode 100644 index 000000000..cdbd8db1f --- /dev/null +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table-virtualized.docs.snap @@ -0,0 +1,640 @@ +# DsTable docs snippets + +## Empty State + +### Show code + {}, + header: 'First Name' + }, + { + accessorKey: 'lastName', + cell: () => {}, + header: 'Last Name' + }, + { + accessorKey: 'age', + cell: () => {}, + header: 'Age' + }, + { + accessorKey: 'visits', + cell: () => {}, + header: 'Visits' + }, + { + accessorKey: 'status', + cell: () => {}, + header: 'Status' + }, + { + accessorKey: 'progress', + cell: () => {}, + header: 'Profile Progress' + } + ]} + data={[]} + emptyState={} + fullWidth + onRowClick={() => {}} + stickyHeader + virtualized +/> + +### MCP manifest +const EmptyState = () => } + onRowClick={fn()} + virtualized />; + +## Virtualized Selectable Table + +### Show code +{ + name: 'Virtualized Selectable Table', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const pageSize = 10; + const [sorting, setSorting] = useState([]); + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + isLoading + } = useInfiniteQuery({ + queryKey: ['people', sorting], + queryFn: async ({ + pageParam + }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData + }, queryClient); + const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); + const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; + const hasMore = flatData.length < totalRows; + return
+ + {isLoading &&
+ + + + Loading data... + + +
} +
; + }, + args: { + selectable: true, + columns: columns.map(col => { + if ('accessorKey' in col && col.accessorKey === 'age') { + return { + ...col, + size: 100 + }; + } + return col; + }), + onScroll: fn() + } +} + +### MCP manifest +const VirtualizedSelectable = () => { + const pageSize = 10; + const [sorting, setSorting] = useState([]); + + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + isLoading, + } = useInfiniteQuery( + { + queryKey: ['people', sorting], + queryFn: async ({ pageParam }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData, + }, + queryClient, + ); + + const flatData = useMemo( + () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], + [infiniteQueryData], + ); + + const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; + const hasMore = flatData.length < totalRows; + + return ( +
+ { + if ('accessorKey' in col && col.accessorKey === 'age') { + return { + ...col, + size: 100, + }; + } + return col; + })} + stickyHeader + bordered + fullWidth + expandable={false} + emptyState={} + onRowClick={fn()} + selectable + onScroll={fn()} + data={flatData} + onSortingChange={setSorting} + virtualized={true} + infiniteScroll={{ + hasMore, + isLoadingMore: isFetching, + onLoadMore: fetchNextPage, + }} /> + {isLoading && ( +
+ + + + Loading data... + + +
+ )} +
+ ); +}; + +## Virtualized Expandable Table + +### Show code +{ + name: 'Virtualized Expandable Table', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const pageSize = 10; + const [sorting, setSorting] = useState([]); + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + isLoading + } = useInfiniteQuery({ + queryKey: ['people-expandable', sorting], + queryFn: async ({ + pageParam + }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData + }, queryClient); + const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); + const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; + const hasMore = flatData.length < totalRows; + return
+ + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + + + } /> + {isLoading &&
+ + + + Loading data... + + +
} +
; + }, + args: { + columns: columns.map(col => { + if ('accessorKey' in col && col.accessorKey === 'age') { + return { + ...col, + size: 100 + }; + } + return col; + }), + onScroll: fn() + } +} + +### MCP manifest +const VirtualizedExpandable = () => { + const pageSize = 10; + const [sorting, setSorting] = useState([]); + + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + isLoading, + } = useInfiniteQuery( + { + queryKey: ['people-expandable', sorting], + queryFn: async ({ pageParam }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData, + }, + queryClient, + ); + + const flatData = useMemo( + () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], + [infiniteQueryData], + ); + + const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; + const hasMore = flatData.length < totalRows; + + return ( +
+ { + if ('accessorKey' in col && col.accessorKey === 'age') { + return { + ...col, + size: 100, + }; + } + return col; + })} + stickyHeader + bordered + fullWidth + emptyState={} + onRowClick={fn()} + onScroll={fn()} + data={flatData} + onSortingChange={setSorting} + virtualized={true} + expandable={true} + infiniteScroll={{ + hasMore, + isLoadingMore: isFetching, + onLoadMore: fetchNextPage, + }} + renderExpandedRow={(row) => ( + + + Expanded Details for {row.firstName} + ID: {row.id} + + Full Name: {row.firstName} {row.lastName} + + Status: {row.status} + + + + + )} /> + {isLoading && ( +
+ + + + Loading data... + + +
+ )} +
+ ); +}; + +## Virtualized Infinite Scroll + +### Show code +{ + name: 'Virtualized Infinite Scroll', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const pageSize = 5; + const totalRows = 60; + const [sorting, setSorting] = useState([]); + const { + data: infiniteQueryData, + fetchNextPage, + isFetching + } = useInfiniteQuery({ + queryKey: ['people-autofill', sorting], + queryFn: async ({ + pageParam + }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting, totalRows); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData + }, queryClient); + const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); + const fetchedTotal = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? totalRows; + const hasMore = flatData.length < fetchedTotal; + return
+ +
; + } +} + +### MCP manifest +const InfiniteScroll = () => { + const pageSize = 5; + const totalRows = 60; + const [sorting, setSorting] = useState([]); + + const { + data: infiniteQueryData, + fetchNextPage, + isFetching, + } = useInfiniteQuery( + { + queryKey: ['people-autofill', sorting], + queryFn: async ({ pageParam }) => { + const start = pageParam * pageSize; + return await fetchData(start, pageSize, sorting, totalRows); + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + placeholderData: keepPreviousData, + }, + queryClient, + ); + + const flatData = useMemo( + () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], + [infiniteQueryData], + ); + + const fetchedTotal = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? totalRows; + const hasMore = flatData.length < fetchedTotal; + + return ( +
+ } + onRowClick={fn()} + data={flatData} + onSortingChange={setSorting} + virtualized={true} + infiniteScroll={{ + hasMore, + isLoadingMore: isFetching, + onLoadMore: fetchNextPage, + }} /> +
+ ); +}; + +## Virtualized Editable Table + +### Show code +{ + name: 'Virtualized Editable Table', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data, setData] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + return
+ { + setData(rows => updateRow(rows, row.id, columnId as keyof Person, value as never)); + }} /> +
; + } +} + +### MCP manifest +const VirtualizedEditable = () => { + const [data, setData] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + + return ( +
+ } + onRowClick={fn()} + data={data} + columns={editableColumns} + virtualized + onCellEdit={(row: Person, columnId, value) => { + setData((rows) => updateRow(rows, row.id, columnId as keyof Person, value as never)); + }} /> +
+ ); +}; + +## Virtualized With Controls + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + return ; + }, + args: { + controls: + + RED version V2 + + + + Import RED + + + Export RED + + Actions + + + } +} + +### MCP manifest +const VirtualizedWithControls = () => { + const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + + return ( + } + onRowClick={fn()} + controls={( + + RED version V2 + + + + Import RED + + + Export RED + + Actions + + )} + data={data} + virtualized /> + ); +}; + +## Virtualized Resizable Columns + +### Show code +{ + name: 'Virtualized Resizable Columns', + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render(args) { + const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + return
+ +
; + }, + args: { + virtualized: true, + resizableColumns: true + } +} + +### MCP manifest +const VirtualizedResizable = () => { + const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); + + return ( +
+ } + onRowClick={fn()} + virtualized + resizableColumns + data={data} /> +
+ ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table.docs.snap b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table.docs.snap index 70e956d46..b44f3c231 100644 --- a/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table.docs.snap +++ b/packages/design-system/src/components/ds-table/__tests__/__snapshots__/ds-table.docs.snap @@ -837,6412 +837,4 @@ const HorizontalScroll = () => } onRowClick={fn()} - data={defaultData} />; - -# DsTable docs snippets - -## Active Row - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - } - ]} - emptyState={} - fullWidth - onRowClick={() => {}} - stickyHeader -/> - -### MCP manifest -const ActiveRow = () => } - onRowClick={fn()} - activeRowId="3" />; - -## Active Row with Drawer - -### Show code -{ - name: 'Active Row with Drawer', - args: { - data: defaultData.slice(0, 10) - }, - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const [selectedPerson, setSelectedPerson] = useState(null); - const activeRowId = selectedPerson?.id; - const isDrawerOpen = !!activeRowId; - const handleRowClick = (person: Person) => { - const isSameRow = activeRowId === person.id; - setSelectedPerson(isSameRow ? null : person); - }; - return <> - - - { - if (!open) { - setSelectedPerson(null); - } - }} columns={4} position="end"> - {selectedPerson &&
- - - Person Details - setSelectedPerson(null)} /> - - - - - - Full Name - - - {selectedPerson.firstName} {selectedPerson.lastName} - - - - - - Age - - {selectedPerson.age} years old - - - - - Visits - - {selectedPerson.visits} visits - - - - - Status - - - {selectedPerson.status.charAt(0).toUpperCase() + selectedPerson.status.slice(1)} - - - - - - Profile Progress - - - - - -
} -
- ; - } -} - -### MCP manifest -const WithDrawerAndActiveRow = () => { - const [selectedPerson, setSelectedPerson] = useState(null); - - const activeRowId = selectedPerson?.id; - const isDrawerOpen = !!activeRowId; - - const handleRowClick = (person: Person) => { - const isSameRow = activeRowId === person.id; - - setSelectedPerson(isSameRow ? null : person); - }; - - return ( - <> - } - activeRowId={activeRowId} - onRowClick={handleRowClick} /> - { - if (!open) { - setSelectedPerson(null); - } - }} - columns={4} - position="end"> - {selectedPerson && ( -
- - - Person Details - setSelectedPerson(null)} - /> - - - - - - Full Name - - - {selectedPerson.firstName} {selectedPerson.lastName} - - - - - - Age - - {selectedPerson.age} years old - - - - - Visits - - {selectedPerson.visits} visits - - - - - Status - - - {selectedPerson.status.charAt(0).toUpperCase() + selectedPerson.status.slice(1)} - - - - - - Profile Progress - - - - - -
- )} -
- - ); -}; - -# DsTable docs snippets - -## Default - -### Show code - {}, - header: 'First Name', - meta: { - keepVisibleWhenCollapsed: true - } - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - } - ], - header: 'Identity', - id: 'identity', - meta: { - group: { - collapsible: true, - defaultCollapsed: false - } - } - }, - { - columns: [ - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits', - meta: { - keepVisibleWhenCollapsed: true - } - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ], - header: 'Activity', - id: 'activity', - meta: { - group: { - collapsible: true - } - } - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - selectable - stickyHeader -/> - -### MCP manifest -const Default = () => } />; - -## Initially Collapsed - -### Show code - {}, - header: 'First Name', - meta: { - keepVisibleWhenCollapsed: true - } - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - } - ], - header: 'Identity', - id: 'identity', - meta: { - group: { - collapsible: true, - defaultCollapsed: true - } - } - }, - { - columns: [ - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits', - meta: { - keepVisibleWhenCollapsed: true - } - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ], - header: 'Activity', - id: 'activity', - meta: { - group: { - collapsible: true - } - } - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - selectable - stickyHeader -/> - -### MCP manifest -const InitiallyCollapsed = () => } />; - -## Controlled - -### Show code - {}, - header: 'First Name', - meta: { - keepVisibleWhenCollapsed: true - } - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - } - ], - header: 'Identity', - id: 'identity', - meta: { - group: { - collapsible: true, - defaultCollapsed: false - } - } - }, - { - columns: [ - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits', - meta: { - keepVisibleWhenCollapsed: true - } - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ], - header: 'Activity', - id: 'activity', - meta: { - group: { - collapsible: true - } - } - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - onCollapsedColumnGroupsChange={() => {}} - selectable - stickyHeader -/> - -### MCP manifest -const Controlled = () => { - const [collapsed, setCollapsed] = useState(['activity']); - - return ( - } - collapsedColumnGroups={collapsed} - onCollapsedColumnGroupsChange={setCollapsed} /> - ); -}; - -## With Controls - -### Show code - {}, - header: 'First Name', - meta: { - keepVisibleWhenCollapsed: true - } - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - } - ], - header: 'Identity', - id: 'identity', - meta: { - group: { - collapsible: true, - defaultCollapsed: false - } - } - }, - { - columns: [ - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits', - meta: { - keepVisibleWhenCollapsed: true - } - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ], - header: 'Activity', - id: 'activity', - meta: { - group: { - collapsible: true - } - } - } - ]} - controls={RED version V2Import REDExport REDActions} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - selectable - stickyHeader -/> - -### MCP manifest -const WithControls = () => } - controls={( - - RED version V2 - - - - Import RED - - - Export RED - - Actions - - )} />; - -# DsTable docs snippets - -## Progress as Infographic - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - onRowClick={() => {}} - stickyHeader -/> - -### MCP manifest -const WithProgressInfographic = () => { - if ('accessorKey' in col && col.accessorKey === 'progress') { - return { - ...col, - header: 'Profile Progress', - cell: (info) => , - }; - } else if ('accessorKey' in col && col.accessorKey === 'status') { - return { - ...col, - header: 'Status', - cell: (info) => ( - - {info.getValue() as string} - - ), - }; - } - return col; - })} - data={defaultData} - stickyHeader - bordered - fullWidth - expandable={false} - emptyState={} - onRowClick={fn()} />; - -## Column Hiding - -### Show code -{ - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const columnsToToggle = [{ - id: 'age', - label: 'Age' - }, { - id: 'visits', - label: 'Visits' - }, { - id: 'status', - label: 'Status' - }, { - id: 'progress', - label: 'Profile Progress' - }]; - const [columnVisibility, setColumnVisibility] = useState({ - age: true, - visits: true, - status: true, - progress: true - }); - const toggleColumn = (columnId: string) => { - setColumnVisibility(prev => ({ - ...prev, - [columnId]: !prev[columnId] - })); - }; - return - - {columnsToToggle.map(column => toggleColumn(column.id)} />)} - - - - ; - } -} - -### MCP manifest -const ColumnHiding = () => { - const columnsToToggle = [ - { id: 'age', label: 'Age' }, - { id: 'visits', label: 'Visits' }, - { id: 'status', label: 'Status' }, - { id: 'progress', label: 'Profile Progress' }, - ]; - const [columnVisibility, setColumnVisibility] = useState({ - age: true, - visits: true, - status: true, - progress: true, - }); - - const toggleColumn = (columnId: string) => { - setColumnVisibility((prev) => ({ - ...prev, - [columnId]: !prev[columnId], - })); - }; - - return ( - - - {columnsToToggle.map((column) => ( - toggleColumn(column.id)} - /> - ))} - - } - onRowClick={fn()} - columnVisibility={columnVisibility} - onColumnVisibilityChange={setColumnVisibility} /> - - ); -}; - -# DsTable docs snippets - -## Editable - -### Show code -{ - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const [data, setData] = useState(defaultData); - const statusOptions = [{ - label: 'Single', - value: 'single' - }, { - label: 'Relationship', - value: 'relationship' - }, { - label: 'Complicated', - value: 'complicated' - }]; - const statusLabels: Record = { - single: 'Single', - relationship: 'Relationship', - complicated: 'Complicated' - }; - const progressPresets = [25, 50, 75, 100]; - const ProgressEditor = ({ - cellContext - }: { - cellContext: CellContext; - }) => { - const { - value, - setValue, - error - } = useCellEditor({ - cellContext - }); - return -
- {progressPresets.map(preset => setValue(preset)} />)} -
- -
; - }; - const columns: ColumnDef[] = [{ - accessorKey: 'id', - header: 'ID', - size: 60, - cell: info => {info.getValue() as string} - }, { - accessorKey: 'firstName', - header: 'First Name', - cell: info => info.getValue(), - editCell: (info: CellContext) => - }, { - accessorKey: 'lastName', - header: 'Last Name', - cell: info => info.getValue(), - editCell: (info: CellContext) => - }, { - accessorKey: 'age', - header: 'Age', - size: 100, - cell: info => info.getValue(), - editCell: (info: CellContext) => - }, { - accessorKey: 'visits', - header: 'Visits', - size: 100, - cell: info => info.getValue(), - editCell: (info: CellContext) => , - editDisabled: (info: CellContext) => { - if (info.row.original.status === 'complicated') { - return { - reason: 'Visits are locked while the status is “Complicated”.' - }; - } - if (info.row.original.age >= 40) { - return true; - } - return false; - } - }, { - accessorKey: 'status', - header: 'Status', - size: 160, - cell: info => statusLabels[info.getValue() as Status], - editCell: (info: CellContext) => - }, { - accessorKey: 'progress', - header: 'Profile Progress', - cell: info => {`${String(info.getValue())}%`}, - editCell: (info: CellContext) => - }]; - return { - setData(rows => rows.map(person => person.id === row.id ? { - ...person, - [columnId]: value - } : person)); - }} />; - } -} - -### MCP manifest -const Editable = () => { - const [data, setData] = useState(defaultData); - - const statusOptions = [ - { label: 'Single', value: 'single' }, - { label: 'Relationship', value: 'relationship' }, - { label: 'Complicated', value: 'complicated' }, - ]; - - const statusLabels: Record = { - single: 'Single', - relationship: 'Relationship', - complicated: 'Complicated', - }; - - const progressPresets = [25, 50, 75, 100]; - - const ProgressEditor = ({ cellContext }: { cellContext: CellContext }) => { - const { value, setValue, error } = useCellEditor({ cellContext }); - - return ( - -
- {progressPresets.map((preset) => ( - setValue(preset)} - /> - ))} -
- -
- ); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: 'id', - header: 'ID', - size: 60, - cell: (info) => {info.getValue() as string}, - }, - { - accessorKey: 'firstName', - header: 'First Name', - cell: (info) => info.getValue(), - editCell: (info: CellContext) => ( - - ), - }, - { - accessorKey: 'lastName', - header: 'Last Name', - cell: (info) => info.getValue(), - editCell: (info: CellContext) => ( - - ), - }, - { - accessorKey: 'age', - header: 'Age', - size: 100, - cell: (info) => info.getValue(), - editCell: (info: CellContext) => ( - - ), - }, - { - accessorKey: 'visits', - header: 'Visits', - size: 100, - cell: (info) => info.getValue(), - editCell: (info: CellContext) => , - editDisabled: (info: CellContext) => { - if (info.row.original.status === 'complicated') { - return { reason: 'Visits are locked while the status is “Complicated”.' }; - } - if (info.row.original.age >= 40) { - return true; - } - return false; - }, - }, - { - accessorKey: 'status', - header: 'Status', - size: 160, - cell: (info) => statusLabels[info.getValue() as Status], - editCell: (info: CellContext) => ( - - ), - }, - { - accessorKey: 'progress', - header: 'Profile Progress', - cell: (info) => {`${String(info.getValue())}%`}, - editCell: (info: CellContext) => , - }, - ]; - - return ( - } - data={data} - columns={columns} - selectable - onRowClick={fn()} - primaryRowActions={[{ icon: 'delete_outline', label: 'Delete', onClick: fn() }]} - secondaryRowActions={[{ icon: 'info', label: 'Details', onClick: fn() }]} - onCellEdit={(row, columnId, value) => { - setData((rows) => - rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), - ); - }} /> - ); -}; - -## Live Validation - -### Show code -{ - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const [data, setData] = useState(defaultData); - const personSchema = z.object({ - firstName: z.string().trim().min(1, 'First name is required').max(50, 'Max 50 characters'), - lastName: z.string().trim().min(1, 'Last name is required').max(50, 'Max 50 characters') - }); - const validateField = (columnId: string, value: unknown): string | null => { - const shape: Record = personSchema.shape; - const fieldSchema = shape[columnId]; - if (!fieldSchema) { - return null; - } - const result = fieldSchema.safeParse(value); - return result.success ? null : result.error.issues[0]?.message ?? null; - }; - const columns: ColumnDef[] = [{ - accessorKey: 'id', - header: 'ID', - size: 60, - cell: info => {info.getValue() as string} - }, { - accessorKey: 'firstName', - header: 'First Name', - cell: info => info.getValue(), - editCell: (info: CellContext) => - }, { - accessorKey: 'lastName', - header: 'Last Name', - cell: info => info.getValue(), - editCell: (info: CellContext) => - }]; - return validateField(columnId, value)} onCellEdit={(row, columnId, value) => { - setData(rows => rows.map(person => person.id === row.id ? { - ...person, - [columnId]: value - } : person)); - }} />; - } -} - -### MCP manifest -const LiveValidation = () => { - const [data, setData] = useState(defaultData); - - const personSchema = z.object({ - firstName: z.string().trim().min(1, 'First name is required').max(50, 'Max 50 characters'), - lastName: z.string().trim().min(1, 'Last name is required').max(50, 'Max 50 characters'), - }); - - const validateField = (columnId: string, value: unknown): string | null => { - const shape: Record = personSchema.shape; - const fieldSchema = shape[columnId]; - if (!fieldSchema) { - return null; - } - const result = fieldSchema.safeParse(value); - return result.success ? null : (result.error.issues[0]?.message ?? null); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: 'id', - header: 'ID', - size: 60, - cell: (info) => {info.getValue() as string}, - }, - { - accessorKey: 'firstName', - header: 'First Name', - cell: (info) => info.getValue(), - editCell: (info: CellContext) => ( - - ), - }, - { - accessorKey: 'lastName', - header: 'Last Name', - cell: (info) => info.getValue(), - editCell: (info: CellContext) => ( - - ), - }, - ]; - - return ( - } - data={data} - columns={columns} - onCellValidate={(_row, columnId, value) => validateField(columnId, value)} - onCellEdit={(row, columnId, value) => { - setData((rows) => - rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), - ); - }} /> - ); -}; - -## Validate on Async Save - -### Show code -{ - name: 'Validate on Async Save', - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const [data, setData] = useState(defaultData); - const saveFirstName = (value: string, signal: AbortSignal): Promise => new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - const trimmed = value.trim(); - if (trimmed.length === 0) { - resolve('First name is required'); - return; - } - if (trimmed.toLowerCase() === 'taken') { - resolve('This name is already taken'); - return; - } - resolve(null); - }, 900); - signal.addEventListener('abort', () => { - clearTimeout(timeout); - reject(new DOMException('Aborted', 'AbortError')); - }); - }); - const columns: ColumnDef[] = [{ - accessorKey: 'id', - header: 'ID', - size: 60, - cell: info => {info.getValue() as string} - }, { - accessorKey: 'firstName', - header: 'First Name', - cell: info => info.getValue(), - editCell: (info: CellContext) => - }, { - accessorKey: 'lastName', - header: 'Last Name', - cell: info => info.getValue() - }]; - return { - const error = await saveFirstName(value as string, signal); - if (error !== null) { - return error; - } - setData(rows => rows.map(person => person.id === row.id ? { - ...person, - [columnId]: value - } : person)); - }} />; - } -} - -### MCP manifest -const ValidateOnAsyncSave = () => { - const [data, setData] = useState(defaultData); - - const saveFirstName = (value: string, signal: AbortSignal): Promise => - new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - const trimmed = value.trim(); - if (trimmed.length === 0) { - resolve('First name is required'); - return; - } - if (trimmed.toLowerCase() === 'taken') { - resolve('This name is already taken'); - return; - } - resolve(null); - }, 900); - - signal.addEventListener('abort', () => { - clearTimeout(timeout); - reject(new DOMException('Aborted', 'AbortError')); - }); - }); - - const columns: ColumnDef[] = [ - { - accessorKey: 'id', - header: 'ID', - size: 60, - cell: (info) => {info.getValue() as string}, - }, - { - accessorKey: 'firstName', - header: 'First Name', - cell: (info) => info.getValue(), - editCell: (info: CellContext) => ( - - ), - }, - { - accessorKey: 'lastName', - header: 'Last Name', - cell: (info) => info.getValue(), - }, - ]; - - return ( - } - data={data} - columns={columns} - onCellEdit={async (row, columnId, value, signal) => { - const error = await saveFirstName(value as string, signal); - if (error !== null) { - return error; - } - setData((rows) => - rows.map((person) => (person.id === row.id ? { ...person, [columnId]: value } : person)), - ); - }} /> - ); -}; - -# DsTable docs snippets - -## Expandable - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - } - ]} - emptyState={} - expandable={() => {}} - fullWidth - onRowClick={() => {}} - renderExpandedRow={() => {}} - stickyHeader -/> - -### MCP manifest -const Expandable = () => row.firstName !== 'Tanner'} - emptyState={} - onRowClick={fn()} - renderExpandedRow={(row) => ( - - - Expanded Details for {row.firstName} - ID: {row.id} - - Full Name: {row.firstName} {row.lastName} - - Status: {row.status} - - - - - )} />; - -## Custom Expander Column Width - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - } - ]} - emptyState={} - expandable - expandableColumnWidth={48} - fullWidth - onRowClick={() => {}} - renderExpandedRow={() => {}} - stickyHeader -/> - -### MCP manifest -const CustomExpanderColumnWidth = () => } - onRowClick={fn()} - expandableColumnWidth={48} - renderExpandedRow={(row) => ( - - Details for {row.firstName} - - )} />; - -## Programmatic Expansion - -### Show code -{ - args: { - data: defaultData.slice(0, 5), - expandable: row => row.firstName !== 'Tanner', - renderExpandedRow: row => - Expanded Details for {row.firstName} - ID: {row.id} - - Full Name: {row.firstName} {row.lastName} - - Status: {row.status} - - }, - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const tableRef = useRef>(null); - const [expandedRows, setExpandedRows] = useState([]); - const expandRow = (rowId: string) => { - tableRef.current?.expandRow(rowId); - setExpandedRows(prev => prev.includes(rowId) ? prev : [...prev, rowId]); - }; - const expandAllRows = () => { - tableRef.current?.expandAllRows(); - const expandableRowIds = defaultData.slice(0, 5).filter(row => row.firstName !== 'Tanner').map(row => row.id); - setExpandedRows(expandableRowIds); - }; - const collapseAllRows = () => { - tableRef.current?.collapseAllRows(); - setExpandedRows([]); - }; - const expandFirstThreeRows = () => { - const firstThreeIds = ['2', '3', '4']; - tableRef.current?.expandRows(firstThreeIds); - setExpandedRows(firstThreeIds); - }; - return - - Expanded rows: {expandedRows.length > 0 ? expandedRows.join(', ') : 'None'} - - - - expandRow('2')}> - Expand Kevin - - expandRow('3')}> - Expand John - - expandRow('4')}> - Expand Jane - - - Expand All - - - Collapse All - - - Expand First 3 Expandable - - - - - ; - } -} - -### MCP manifest -const ProgrammaticExpansion = () => { - const tableRef = useRef>(null); - const [expandedRows, setExpandedRows] = useState([]); - - const expandRow = (rowId: string) => { - tableRef.current?.expandRow(rowId); - setExpandedRows((prev) => (prev.includes(rowId) ? prev : [...prev, rowId])); - }; - - const expandAllRows = () => { - tableRef.current?.expandAllRows(); - const expandableRowIds = defaultData - .slice(0, 5) - .filter((row) => row.firstName !== 'Tanner') - .map((row) => row.id); - setExpandedRows(expandableRowIds); - }; - - const collapseAllRows = () => { - tableRef.current?.collapseAllRows(); - setExpandedRows([]); - }; - - const expandFirstThreeRows = () => { - const firstThreeIds = ['2', '3', '4']; - tableRef.current?.expandRows(firstThreeIds); - setExpandedRows(firstThreeIds); - }; - - return ( - - Expanded rows: {expandedRows.length > 0 ? expandedRows.join(', ') : 'None'} - - - expandRow('2')}>Expand Kevin - - expandRow('3')}>Expand John - - expandRow('4')}>Expand Jane - - Expand All - - Collapse All - - Expand First 3 Expandable - - - row.firstName !== 'Tanner'} - emptyState={} - onRowClick={fn()} - renderExpandedRow={(row) => ( - - Expanded Details for {row.firstName} - ID: {row.id} - - Full Name: {row.firstName} {row.lastName} - - Status: {row.status} - - )} - ref={tableRef} /> - - ); -}; - -# DsTable docs snippets - -## Per-Column — Popover - -### Show code -import type { ColumnDef } from '@tanstack/react-table'; -import { - DsTable, - DsTagFilter, - createCheckboxFilterAdapter, - useTableFilters, -} from '@drivenets/design-system'; - -type DeviceRow = { - id: string; - name: string; - type: 'PP-LGX' | 'ME10' | 'OLT'; - site: string; - status: 'active' | 'warning' | 'failed'; -}; - -const typeOptions = [ - { value: 'PP-LGX' as const, label: 'PP-LGX' }, - { value: 'ME10' as const, label: 'ME10' }, - { value: 'OLT' as const, label: 'OLT' }, -]; - -const siteOptions = [ - { value: 'NYC-DC1', label: 'NYC-DC1' }, - { value: 'LON-DC2', label: 'LON-DC2' }, - { value: 'SFO-DC3', label: 'SFO-DC3' }, -]; - -const typeFilter = createCheckboxFilterAdapter({ - id: 'type', - label: 'Type', - items: typeOptions, - searchable: true, - selectAll: true, -}); - -const siteFilter = createCheckboxFilterAdapter({ - id: 'site', - label: 'Site', - items: siteOptions, - searchable: true, - selectAll: true, -}); - -const columns: ColumnDef[] = [ - { id: 'name', accessorKey: 'name', header: 'Name' }, - { id: 'type', accessorKey: 'type', header: 'Type', meta: { filter: { adapterId: 'type' } } }, - { id: 'site', accessorKey: 'site', header: 'Site', meta: { filter: { adapterId: 'site' } } }, - { id: 'status', accessorKey: 'status', header: 'Status' }, -]; - -function DevicesTable({ rows }: { rows: DeviceRow[] }) { - const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ - filterAdapters: [typeFilter, siteFilter], - baseColumns: columns, - }); - - return ( - <> - {filterChips.length > 0 && ( - - )} - - - ); -} - -### MCP manifest -const ColumnFilters = function Render() { - const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ - filterAdapters: adapters, - baseColumns, - }); - - return ( - - {filterChips.length > 0 && ( - - )} - - - - ); -}; - -## Per-Column — Controlled - -### Show code -import { useState } from 'react'; -import type { ColumnDef } from '@tanstack/react-table'; -import { - DsTable, - DsTagFilter, - type CheckboxFilterItem, - type FilterState, - createCheckboxFilterAdapter, - useTableFilters, -} from '@drivenets/design-system'; - -type DeviceRow = { - id: string; - name: string; - type: 'PP-LGX' | 'ME10' | 'OLT'; - site: string; -}; - -const typeFilter = createCheckboxFilterAdapter({ - id: 'type', - label: 'Type', - items: [ - { value: 'PP-LGX', label: 'PP-LGX' }, - { value: 'ME10', label: 'ME10' }, - { value: 'OLT', label: 'OLT' }, - ], - searchable: true, - selectAll: true, -}); - -const columns: ColumnDef[] = [ - { id: 'name', accessorKey: 'name', header: 'Name' }, - { id: 'type', accessorKey: 'type', header: 'Type', meta: { filter: { adapterId: 'type' } } }, - { id: 'site', accessorKey: 'site', header: 'Site' }, -]; - -function ControlledDevicesTable({ rows }: { rows: DeviceRow[] }) { - // Source of truth lives outside the hook (URL, server, parent store, etc.). - const [appliedFilters, setAppliedFilters] = useState>({ - type: [{ value: 'PP-LGX', label: 'PP-LGX' }], - }); - - const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ - filterAdapters: [typeFilter], - baseColumns: columns, - appliedFilters, - onFiltersChange: setAppliedFilters, - }); - - return ( - <> - {filterChips.length > 0 && ( - - )} - - - ); -} - -### MCP manifest -const ControlledColumnFilters = function Render() { - const [appliedFilters, setAppliedFilters] = useState>({ - type: [{ value: 'PP-LGX', label: 'PP-LGX' }], - }); - - const { columnFilters, filterChips, enhancedColumns, handlers } = useTableFilters({ - filterAdapters: adapters, - baseColumns, - appliedFilters, - onFiltersChange: setAppliedFilters, - }); - - return ( - - {filterChips.length > 0 && ( - - )} - - - - ); -}; - -# DsTable docs snippets - -## Loading - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - loading - stickyHeader -/> - -### MCP manifest -const Loading = () => } - loading />; - -## Custom Loading Cell - -### Show code - {}, - header: 'First Name', - loadingCell: () => {} - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - loading - stickyHeader -/> - -### MCP manifest -const CustomLoadingCell = () => } - loading />; - -# DsTable docs snippets - -## Default - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - resizableColumns - stickyHeader -/> - -### MCP manifest -const Default = () => } />; - -## Fixed And Fill - -### Show code - {}, - header: 'First Name', - size: 200 - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name', - size: 200 - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age', - size: 120 - }, - { - accessorKey: 'visits', - cell: () => {}, - enableResizing: false, - header: 'Visits', - size: 120 - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - resizableColumns - stickyHeader -/> - -### MCP manifest -const FixedAndFill = () => info.getValue(), size: 200 }, - { accessorKey: 'lastName', header: 'Last Name', cell: (info) => info.getValue(), size: 200 }, - { accessorKey: 'age', header: 'Age', cell: (info) => info.getValue(), size: 120 }, - { - accessorKey: 'visits', - header: 'Visits', - cell: (info) => info.getValue(), - enableResizing: false, - size: 120, - }, - { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, - ]} - resizableColumns - stickyHeader - bordered - fullWidth - emptyState={} />; - -## Grouped Columns - -### Show code - {}, - header: 'First Name', - maxSize: 280, - minSize: 80 - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - } - ], - header: 'Identity', - id: 'identity' - }, - { - columns: [ - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Progress' - } - ], - header: 'Activity', - id: 'activity' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - resizableColumns - stickyHeader -/> - -### MCP manifest -const GroupedColumns = () => info.getValue(), - minSize: 80, - maxSize: 280, - }, - { - accessorKey: 'lastName', - header: 'Last Name', - cell: (info) => info.getValue(), - }, - ], - }, - { - id: 'activity', - header: 'Activity', - columns: [ - { accessorKey: 'visits', header: 'Visits', cell: (info) => info.getValue() }, - { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, - { - accessorKey: 'progress', - header: 'Progress', - cell: (info) => `${String(info.getValue())}%`, - }, - ], - }, - ]} - resizableColumns - stickyHeader - bordered - fullWidth - emptyState={} />; - -## Min And Max Size - -### Show code - {}, - header: 'First Name', - maxSize: 280, - minSize: 80, - size: 200 - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name', - minSize: 120, - size: 200 - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age', - maxSize: 180, - size: 120 - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits', - size: 120 - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - resizableColumns - stickyHeader -/> - -### MCP manifest -const MinAndMaxSize = () => info.getValue(), - size: 200, - minSize: 80, - maxSize: 280, - }, - { - accessorKey: 'lastName', - header: 'Last Name', - cell: (info) => info.getValue(), - size: 200, - minSize: 120, - }, - { - accessorKey: 'age', - header: 'Age', - cell: (info) => info.getValue(), - size: 120, - maxSize: 180, - }, - { accessorKey: 'visits', header: 'Visits', cell: (info) => info.getValue(), size: 120 }, - { accessorKey: 'status', header: 'Status', cell: (info) => info.getValue() }, - ]} - resizableColumns - stickyHeader - bordered - fullWidth - emptyState={} />; - -## Persisted widths - -### Show code -{ - name: 'Persisted widths', - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const STORAGE_KEY = 'storybook.ds-table.resizable.persisted-widths'; - const [tableKey, setTableKey] = useState(0); - let persistedWidths: Record; - try { - persistedWidths = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record; - } catch { - persistedWidths = {}; - } - return - - Persisted widths - - Persist the map from onColumnSizingChange, then pass it back as the columnSizing prop on the next - mount to restore widths — no need to stamp columnDef.size. Columns absent from the map are - measured automatically. Resize a column, refresh the story, and the widths come back. Reset stored - widths remounts from the default layout. - - - - - { - localStorage.removeItem(STORAGE_KEY); - setTableKey(key => key + 1); - }}> - Reset stored widths - - - - { - localStorage.setItem(STORAGE_KEY, JSON.stringify(columnSizing)); - }} /> - ; - } -} - -### MCP manifest -const PersistedWidths = () => { - const STORAGE_KEY = 'storybook.ds-table.resizable.persisted-widths'; - const [tableKey, setTableKey] = useState(0); - - let persistedWidths: Record; - try { - persistedWidths = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record; - } catch { - persistedWidths = {}; - } - - return ( - - - Persisted widths - Persist the map from onColumnSizingChange, then pass it back as the columnSizing prop on the next - mount to restore widths — no need to stamp columnDef.size. Columns absent from the map are - measured automatically. Resize a column, refresh the story, and the widths come back. Reset stored - widths remounts from the default layout. - - - - { - localStorage.removeItem(STORAGE_KEY); - setTableKey((key) => key + 1); - }}>Reset stored widths - - - } - key={tableKey} - columnSizing={persistedWidths} - onColumnSizingChange={(columnSizing) => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(columnSizing)); - }} /> - - ); -}; - -# DsTable docs snippets - -## Reorderable - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - } - ]} - emptyState={} - fullWidth - onOrderChange={() => {}} - onRowClick={() => {}} - reorderable - stickyHeader -/> - -### MCP manifest -const Reorderable = () => } - onRowClick={fn()} - reorderable - onOrderChange={fn()} />; - -## Custom Reorder Column Width - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - } - ]} - emptyState={} - fullWidth - onOrderChange={() => {}} - onRowClick={() => {}} - reorderable - reorderableColumnWidth={80} - stickyHeader -/> - -### MCP manifest -const CustomReorderColumnWidth = () => } - onRowClick={fn()} - reorderable - reorderableColumnWidth={80} - onOrderChange={fn()} />; - -## With Row Actions - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - onRowClick={() => {}} - primaryRowActions={[ - { - icon: 'edit', - label: 'Edit', - onClick: () => {} - }, - { - disabled: () => {}, - icon: 'open_in_new', - label: 'Open in New Window', - onClick: () => {} - } - ]} - secondaryRowActions={[ - { - className: '_destructiveAction_12uvr_138', - disabled: () => {}, - icon: 'delete_outline', - label: 'Delete', - onClick: () => {}, - tooltip: 'Delete this row' - }, - { - icon: 'info', - label: 'Details', - onClick: () => {}, - tooltip: 'Show details' - }, - { - icon: 'call', - label: () => {}, - onClick: () => {} - } - ]} - stickyHeader -/> - -### MCP manifest -const WithRowActions = () => } - onRowClick={fn()} - primaryRowActions={[ - { - icon: 'edit', - label: 'Edit', - onClick: editClickHandler, - }, - { - icon: 'open_in_new', - label: 'Open in New Window', - disabled: (data) => data.firstName === 'Tanner', - onClick: openInNewWindowClickHandler, - }, - ]} - secondaryRowActions={[ - { - icon: 'delete_outline', - label: 'Delete', - tooltip: 'Delete this row', - disabled: (data) => data.status === 'single', - className: styles.destructiveAction, - onClick: fn(), - }, - { - icon: 'info', - label: 'Details', - tooltip: 'Show details', - onClick: fn(), - }, - { - icon: 'call', - label: (row) => `Call ${row.firstName}`, - onClick: fn(), - }, - ]} />; - -## With Conditionally Hidden Actions - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - onRowClick={() => {}} - primaryRowActions={[ - { - icon: 'edit', - label: 'Edit', - onClick: () => {} - }, - { - hidden: () => {}, - icon: 'open_in_new', - label: 'Open in New Window', - onClick: () => {} - } - ]} - secondaryRowActions={[ - { - hidden: () => {}, - icon: 'check_circle', - label: 'Approve', - onClick: () => {} - }, - { - hidden: () => {}, - icon: 'inventory_2', - label: 'Archive', - onClick: () => {} - }, - { - className: '_destructiveAction_12uvr_138', - hidden: () => {}, - icon: 'delete_outline', - label: 'Delete', - onClick: () => {} - }, - { - icon: 'info', - label: 'Details', - onClick: () => {} - } - ]} - stickyHeader -/> - -### MCP manifest -const WithConditionallyHiddenActions = () => } - onRowClick={fn()} - primaryRowActions={[ - { - icon: 'edit', - label: 'Edit', - onClick: fn(), - }, - { - icon: 'open_in_new', - label: 'Open in New Window', - // hidden on 'complicated' rows (e.g. cannot open a record in a bad state) - hidden: (data) => data.status === 'complicated', - onClick: fn(), - }, - ]} - secondaryRowActions={[ - { - icon: 'check_circle', - label: 'Approve', - // only shown on 'single' rows (pending approval) - hidden: (data) => data.status !== 'single', - onClick: fn(), - }, - { - icon: 'inventory_2', - label: 'Archive', - // only shown on 'relationship' rows (live records) - hidden: (data) => data.status !== 'relationship', - onClick: fn(), - }, - { - icon: 'delete_outline', - label: 'Delete', - // hidden on 'relationship' rows (cannot delete live records) - hidden: (data) => data.status === 'relationship', - className: styles.destructiveAction, - onClick: fn(), - }, - { - icon: 'info', - label: 'Details', - onClick: fn(), - }, - ]} />; - -## With Conditionally Disabled Actions - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - onRowClick={() => {}} - primaryRowActions={[ - { - icon: 'edit', - label: 'Edit', - onClick: () => {} - }, - { - disabled: () => {}, - icon: 'open_in_new', - label: 'Open in New Window', - onClick: () => {} - } - ]} - secondaryRowActions={[ - { - className: '_destructiveAction_12uvr_138', - disabled: () => {}, - icon: 'delete_outline', - label: 'Delete', - onClick: () => {}, - tooltip: 'Delete this row' - }, - { - icon: 'info', - label: 'Details', - onClick: () => {}, - tooltip: 'Show details' - } - ]} - stickyHeader -/> - -### MCP manifest -const WithConditionallyDisabledActions = () => } - onRowClick={fn()} - primaryRowActions={[ - { - icon: 'edit', - label: 'Edit', - onClick: fn(), - }, - { - icon: 'open_in_new', - label: 'Open in New Window', - // disabled on Tanner's row (item stays visible but greyed out) - disabled: (data) => data.firstName === 'Tanner', - onClick: fn(), - }, - ]} - secondaryRowActions={[ - { - icon: 'delete_outline', - label: 'Delete', - tooltip: 'Delete this row', - // disabled on 'single' rows (destructive action guarded) - disabled: (data) => data.status === 'single', - className: styles.destructiveAction, - onClick: fn(), - }, - { - icon: 'info', - label: 'Details', - tooltip: 'Show details', - onClick: fn(), - }, - ]} />; - -## With Bulk Actions - -### Show code - {} - }, - { - icon: 'folder_open', - label: 'Folder', - onClick: () => {} - }, - { - icon: 'delete_outline', - label: 'Delete', - onClick: () => {} - } - ]} - bordered - columns={[ - { - accessorKey: 'firstName', - cell: () => {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - onRowClick={() => {}} - selectable - stickyHeader -/> - -### MCP manifest -const WithBulkActions = () => } - onRowClick={fn()} - selectable - actions={[ - { - icon: 'alarm', - label: 'Notify', - onClick: fn(), - }, - { - icon: 'folder_open', - label: 'Folder', - onClick: fn(), - }, - { - icon: 'delete_outline', - label: 'Delete', - onClick: fn(), - }, - ]} />; - -# DsTable docs snippets - -## Selectable - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - onRowClick={() => {}} - onSelectionChange={() => {}} - selectable - stickyHeader -/> - -### MCP manifest -const Selectable = () => } - onRowClick={fn()} - selectable - onSelectionChange={fn()} />; - -## Custom Select Column Width - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[ - { - age: 33, - firstName: 'Tanner', - id: '1', - lastName: 'Linsley', - progress: 75, - status: 'single', - visits: 100 - }, - { - age: 28, - firstName: 'Kevin', - id: '2', - lastName: 'Fine', - progress: 50, - status: 'relationship', - visits: 200 - }, - { - age: 45, - firstName: 'John', - id: '3', - lastName: 'Doe', - progress: 90, - status: 'complicated', - visits: 50 - }, - { - age: 30, - firstName: 'Jane', - id: '4', - lastName: 'Smith', - progress: 60, - status: 'single', - visits: 150 - }, - { - age: 22, - firstName: 'Peter', - id: '5', - lastName: 'Jones', - progress: 30, - status: 'relationship', - visits: 250 - }, - { - age: 38, - firstName: 'Mary', - id: '6', - lastName: 'Jane', - progress: 85, - status: 'complicated', - visits: 80 - }, - { - age: 50, - firstName: 'David', - id: '7', - lastName: 'Williams', - progress: 40, - status: 'single', - visits: 120 - }, - { - age: 25, - firstName: 'Sarah', - id: '8', - lastName: 'Brown', - progress: 70, - status: 'relationship', - visits: 180 - }, - { - age: 41, - firstName: 'Michael', - id: '9', - lastName: 'Davis', - progress: 20, - status: 'complicated', - visits: 95 - }, - { - age: 36, - firstName: 'Emily', - id: '10', - lastName: 'Miller', - progress: 55, - status: 'single', - visits: 110 - }, - { - age: 29, - firstName: 'Daniel', - id: '11', - lastName: 'Wilson', - progress: 80, - status: 'relationship', - visits: 220 - }, - { - age: 48, - firstName: 'Olivia', - id: '12', - lastName: 'Moore', - progress: 15, - status: 'complicated', - visits: 65 - }, - { - age: 31, - firstName: 'James', - id: '13', - lastName: 'Taylor', - progress: 95, - status: 'single', - visits: 135 - }, - { - age: 27, - firstName: 'Sophia', - id: '14', - lastName: 'Anderson', - progress: 25, - status: 'relationship', - visits: 170 - }, - { - age: 43, - firstName: 'Robert', - id: '15', - lastName: 'Thomas', - progress: 50, - status: 'complicated', - visits: 88 - } - ]} - emptyState={} - fullWidth - onRowClick={() => {}} - onSelectionChange={() => {}} - selectable - selectableColumnWidth={48} - stickyHeader -/> - -### MCP manifest -const CustomSelectColumnWidth = () => } - onRowClick={fn()} - selectable - selectableColumnWidth={48} - onSelectionChange={fn()} />; - -## Programmatic Row Selection - -### Show code -{ - args: { - selectable: true, - showSelectAllCheckbox: false, - stickyHeader: true, - onSelectionChange: fn() - }, - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const tableRef = useRef>(null); - const [selectedRows, setSelectedRows] = useState([]); - const handleSelectionChange = (selection: Record) => { - setSelectedRows(Object.keys(selection)); - }; - return - - Selected rows: {selectedRows.length > 0 ? selectedRows.join(', ') : 'None'} - - - - tableRef.current?.selectRow('1')}> - Select Row 1 - - tableRef.current?.selectRow('2')}> - Select Row 2 - - tableRef.current?.selectRow('3')}> - Select Row 3 - - tableRef.current?.selectAllRows()}> - Select All - - tableRef.current?.deselectAllRows()}> - Deselect All - - tableRef.current?.selectRows(['1', '2', '3'])}> - Select First 3 Rows - - - - - ; - } -} - -### MCP manifest -const ProgrammaticRowSelection = () => { - const tableRef = useRef>(null); - const [selectedRows, setSelectedRows] = useState([]); - - const handleSelectionChange = (selection: Record) => { - setSelectedRows(Object.keys(selection)); - }; - - return ( - - Selected rows: {selectedRows.length > 0 ? selectedRows.join(', ') : 'None'} - - - tableRef.current?.selectRow('1')}>Select Row 1 - - tableRef.current?.selectRow('2')}>Select Row 2 - - tableRef.current?.selectRow('3')}>Select Row 3 - - tableRef.current?.selectAllRows()}>Select All - - tableRef.current?.deselectAllRows()}>Deselect All - - tableRef.current?.selectRows(['1', '2', '3'])}>Select First 3 Rows - - - } - onRowClick={fn()} - selectable - showSelectAllCheckbox={false} - ref={tableRef} - onSelectionChange={handleSelectionChange} /> - - ); -}; - -## Max N Selections - -### Show code -{ - name: 'Max N Selections', - args: { - showSelectAllCheckbox: false, - onSelectionChange: fn() - }, - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const [rowSelection, setRowSelection] = useState>({}); - const maxSelections = 2; - const selectedCount = Object.keys(rowSelection).filter(id => rowSelection[id]).length; - const handleSelectionChange = (selection: Record) => { - setRowSelection(selection); - args.onSelectionChange?.(selection); - }; - return - - Selected: {selectedCount} / {maxSelections} - - - rowSelection[rowData.id] || selectedCount < maxSelections} /> - ; - } -} - -### MCP manifest -const MaxSelectionLimit = () => { - const [rowSelection, setRowSelection] = useState>({}); - const maxSelections = 2; - - const selectedCount = Object.keys(rowSelection).filter((id) => rowSelection[id]).length; - - const handleSelectionChange = (selection: Record) => { - setRowSelection(selection); - args.onSelectionChange?.(selection); - }; - - return ( - - Selected: {selectedCount}/ {maxSelections} - - } - onRowClick={fn()} - showSelectAllCheckbox={false} - onSelectionChange={handleSelectionChange} - selectable={(rowData) => rowSelection[rowData.id] || selectedCount < maxSelections} /> - - ); -}; - -# DsTable docs snippets - -## Empty State - -### Show code - {}, - header: 'First Name' - }, - { - accessorKey: 'lastName', - cell: () => {}, - header: 'Last Name' - }, - { - accessorKey: 'age', - cell: () => {}, - header: 'Age' - }, - { - accessorKey: 'visits', - cell: () => {}, - header: 'Visits' - }, - { - accessorKey: 'status', - cell: () => {}, - header: 'Status' - }, - { - accessorKey: 'progress', - cell: () => {}, - header: 'Profile Progress' - } - ]} - data={[]} - emptyState={} - fullWidth - onRowClick={() => {}} - stickyHeader - virtualized -/> - -### MCP manifest -const EmptyState = () => } - onRowClick={fn()} - virtualized />; - -## Virtualized Selectable Table - -### Show code -{ - name: 'Virtualized Selectable Table', - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const pageSize = 10; - const [sorting, setSorting] = useState([]); - const { - data: infiniteQueryData, - fetchNextPage, - isFetching, - isLoading - } = useInfiniteQuery({ - queryKey: ['people', sorting], - queryFn: async ({ - pageParam - }) => { - const start = pageParam * pageSize; - return await fetchData(start, pageSize, sorting); - }, - initialPageParam: 0, - getNextPageParam: (_lastGroup, groups) => groups.length, - placeholderData: keepPreviousData - }, queryClient); - const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); - const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; - const hasMore = flatData.length < totalRows; - return
- - {isLoading &&
- - - - Loading data... - - -
} -
; - }, - args: { - selectable: true, - columns: columns.map(col => { - if ('accessorKey' in col && col.accessorKey === 'age') { - return { - ...col, - size: 100 - }; - } - return col; - }), - onScroll: fn() - } -} - -### MCP manifest -const VirtualizedSelectable = () => { - const pageSize = 10; - const [sorting, setSorting] = useState([]); - - const { - data: infiniteQueryData, - fetchNextPage, - isFetching, - isLoading, - } = useInfiniteQuery( - { - queryKey: ['people', sorting], - queryFn: async ({ pageParam }) => { - const start = pageParam * pageSize; - return await fetchData(start, pageSize, sorting); - }, - initialPageParam: 0, - getNextPageParam: (_lastGroup, groups) => groups.length, - placeholderData: keepPreviousData, - }, - queryClient, - ); - - const flatData = useMemo( - () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], - [infiniteQueryData], - ); - - const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; - const hasMore = flatData.length < totalRows; - - return ( -
- { - if ('accessorKey' in col && col.accessorKey === 'age') { - return { - ...col, - size: 100, - }; - } - return col; - })} - stickyHeader - bordered - fullWidth - expandable={false} - emptyState={} - onRowClick={fn()} - selectable - onScroll={fn()} - data={flatData} - onSortingChange={setSorting} - virtualized={true} - infiniteScroll={{ - hasMore, - isLoadingMore: isFetching, - onLoadMore: fetchNextPage, - }} /> - {isLoading && ( -
- - - - Loading data... - - -
- )} -
- ); -}; - -## Virtualized Expandable Table - -### Show code -{ - name: 'Virtualized Expandable Table', - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const pageSize = 10; - const [sorting, setSorting] = useState([]); - const { - data: infiniteQueryData, - fetchNextPage, - isFetching, - isLoading - } = useInfiniteQuery({ - queryKey: ['people-expandable', sorting], - queryFn: async ({ - pageParam - }) => { - const start = pageParam * pageSize; - return await fetchData(start, pageSize, sorting); - }, - initialPageParam: 0, - getNextPageParam: (_lastGroup, groups) => groups.length, - placeholderData: keepPreviousData - }, queryClient); - const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); - const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; - const hasMore = flatData.length < totalRows; - return
- - - Expanded Details for {row.firstName} - ID: {row.id} - - Full Name: {row.firstName} {row.lastName} - - Status: {row.status} - - - - } /> - {isLoading &&
- - - - Loading data... - - -
} -
; - }, - args: { - columns: columns.map(col => { - if ('accessorKey' in col && col.accessorKey === 'age') { - return { - ...col, - size: 100 - }; - } - return col; - }), - onScroll: fn() - } -} - -### MCP manifest -const VirtualizedExpandable = () => { - const pageSize = 10; - const [sorting, setSorting] = useState([]); - - const { - data: infiniteQueryData, - fetchNextPage, - isFetching, - isLoading, - } = useInfiniteQuery( - { - queryKey: ['people-expandable', sorting], - queryFn: async ({ pageParam }) => { - const start = pageParam * pageSize; - return await fetchData(start, pageSize, sorting); - }, - initialPageParam: 0, - getNextPageParam: (_lastGroup, groups) => groups.length, - placeholderData: keepPreviousData, - }, - queryClient, - ); - - const flatData = useMemo( - () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], - [infiniteQueryData], - ); - - const totalRows = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? 0; - const hasMore = flatData.length < totalRows; - - return ( -
- { - if ('accessorKey' in col && col.accessorKey === 'age') { - return { - ...col, - size: 100, - }; - } - return col; - })} - stickyHeader - bordered - fullWidth - emptyState={} - onRowClick={fn()} - onScroll={fn()} - data={flatData} - onSortingChange={setSorting} - virtualized={true} - expandable={true} - infiniteScroll={{ - hasMore, - isLoadingMore: isFetching, - onLoadMore: fetchNextPage, - }} - renderExpandedRow={(row) => ( - - - Expanded Details for {row.firstName} - ID: {row.id} - - Full Name: {row.firstName} {row.lastName} - - Status: {row.status} - - - - - )} /> - {isLoading && ( -
- - - - Loading data... - - -
- )} -
- ); -}; - -## Virtualized Infinite Scroll - -### Show code -{ - name: 'Virtualized Infinite Scroll', - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const pageSize = 5; - const totalRows = 60; - const [sorting, setSorting] = useState([]); - const { - data: infiniteQueryData, - fetchNextPage, - isFetching - } = useInfiniteQuery({ - queryKey: ['people-autofill', sorting], - queryFn: async ({ - pageParam - }) => { - const start = pageParam * pageSize; - return await fetchData(start, pageSize, sorting, totalRows); - }, - initialPageParam: 0, - getNextPageParam: (_lastGroup, groups) => groups.length, - placeholderData: keepPreviousData - }, queryClient); - const flatData = useMemo(() => infiniteQueryData?.pages.flatMap(page => page.data) ?? [], [infiniteQueryData]); - const fetchedTotal = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? totalRows; - const hasMore = flatData.length < fetchedTotal; - return
- -
; - } -} - -### MCP manifest -const InfiniteScroll = () => { - const pageSize = 5; - const totalRows = 60; - const [sorting, setSorting] = useState([]); - - const { - data: infiniteQueryData, - fetchNextPage, - isFetching, - } = useInfiniteQuery( - { - queryKey: ['people-autofill', sorting], - queryFn: async ({ pageParam }) => { - const start = pageParam * pageSize; - return await fetchData(start, pageSize, sorting, totalRows); - }, - initialPageParam: 0, - getNextPageParam: (_lastGroup, groups) => groups.length, - placeholderData: keepPreviousData, - }, - queryClient, - ); - - const flatData = useMemo( - () => infiniteQueryData?.pages.flatMap((page) => page.data) ?? [], - [infiniteQueryData], - ); - - const fetchedTotal = infiniteQueryData?.pages[0]?.meta.totalRowCount ?? totalRows; - const hasMore = flatData.length < fetchedTotal; - - return ( -
- } - onRowClick={fn()} - data={flatData} - onSortingChange={setSorting} - virtualized={true} - infiniteScroll={{ - hasMore, - isLoadingMore: isFetching, - onLoadMore: fetchNextPage, - }} /> -
- ); -}; - -## Virtualized Editable Table - -### Show code -{ - name: 'Virtualized Editable Table', - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const [data, setData] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); - return
- { - setData(rows => updateRow(rows, row.id, columnId as keyof Person, value as never)); - }} /> -
; - } -} - -### MCP manifest -const VirtualizedEditable = () => { - const [data, setData] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); - - return ( -
- } - onRowClick={fn()} - data={data} - columns={editableColumns} - virtualized - onCellEdit={(row: Person, columnId, value) => { - setData((rows) => updateRow(rows, row.id, columnId as keyof Person, value as never)); - }} /> -
- ); -}; - -## Virtualized With Controls - -### Show code -{ - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); - return ; - }, - args: { - controls: - - RED version V2 - - - - Import RED - - - Export RED - - Actions - - - } -} - -### MCP manifest -const VirtualizedWithControls = () => { - const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); - - return ( - } - onRowClick={fn()} - controls={( - - RED version V2 - - - - Import RED - - - Export RED - - Actions - - )} - data={data} - virtualized /> - ); -}; - -## Virtualized Resizable Columns - -### Show code -{ - name: 'Virtualized Resizable Columns', - parameters: { - docs: { - source: { - type: 'code' - } - } - }, - render: function Render(args) { - const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); - return
- -
; - }, - args: { - virtualized: true, - resizableColumns: true - } -} - -### MCP manifest -const VirtualizedResizable = () => { - const [data] = useState(() => generatePersonData(0, VIRTUALIZED_ROW_COUNT, []).data); - - return ( -
- } - onRowClick={fn()} - virtualized - resizableColumns - data={data} /> -
- ); -}; \ No newline at end of file + data={defaultData} />; \ No newline at end of file diff --git a/packages/design-system/tests/storybook/docs-snippets.docs.test.ts b/packages/design-system/tests/storybook/docs-snippets.docs.test.ts index f90079f93..ee1eec679 100644 --- a/packages/design-system/tests/storybook/docs-snippets.docs.test.ts +++ b/packages/design-system/tests/storybook/docs-snippets.docs.test.ts @@ -68,12 +68,27 @@ const COMPONENTS = [ 'workspace-layout', ]; +// Folders whose stories fan out into many story titles (each a separate manifest +// component) produce a single aggregated golden that runs to thousands of lines +// where every section shares the same `# DsTable` header — unnavigable for humans +// and agents alike. Split those into one golden per manifest component instead. +const SPLIT_PER_MANIFEST = new Set(['table']); + function getComponentSnapshotPath(name: string): string { const folder = `ds-${name}`; return path.join(packageRoot, 'src/components', folder, '__tests__/__snapshots__', `${folder}.docs.snap`); } +// Names the per-manifest golden from the title-derived id so the file mirrors the +// story hierarchy (`components-table-selection` → `ds-table-selection.docs.snap`). +function getManifestSnapshotPath(name: string, component: ManifestComponent): string { + const folder = `ds-${name}`; + const fileName = component.id.replace(/^components-/, 'ds-'); + + return path.join(packageRoot, 'src/components', folder, '__tests__/__snapshots__', `${fileName}.docs.snap`); +} + async function buildComponentDocsSnapshot(page: Page, component: ManifestComponent): Promise { const sections: string[] = [`# ${component.name} docs snippets`, '']; @@ -118,7 +133,7 @@ describe('docs snippets', () => { it.concurrent(`ds-${name} docs snippets match staged authoring rules`, async ({ expect }) => { // A folder may resolve to several manifest components (e.g. ds-form-control); // build each on its own page in parallel, then aggregate in resolved order. - const sections = await Promise.all( + const built = await Promise.all( manifestComponents.map(async (component) => { const page = await browser.newPage({ viewport: { width: 1400, height: 900 }, @@ -126,16 +141,32 @@ describe('docs snippets', () => { }); try { - return await buildComponentDocsSnapshot(page, component); + return { component, document: await buildComponentDocsSnapshot(page, component) }; } finally { await page.close(); } }), ); - const document = sections.join('\n\n'); - - await expect(document).toMatchFileSnapshot(getComponentSnapshotPath(name)); + // Split folders write one golden per manifest component; the rest aggregate + // every manifest component into a single colocated golden. + const snapshots = SPLIT_PER_MANIFEST.has(name) + ? built.map(({ component, document }) => ({ + path: getManifestSnapshotPath(name, component), + document, + })) + : [ + { + path: getComponentSnapshotPath(name), + document: built.map((entry) => entry.document).join('\n\n'), + }, + ]; + + await Promise.all( + snapshots.map(({ path: snapshotPath, document }) => + expect(document).toMatchFileSnapshot(snapshotPath), + ), + ); }, 180_000); } });