@agregio-solutions/design-system 1.90.1 → 1.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/design-system.cjs +9 -5
- package/dist/design-system.js +14 -6
- package/dist/packages/components/Accordion/doc.md +342 -0
- package/dist/packages/components/Badge/doc.md +192 -0
- package/dist/packages/components/Breadcrumbs/doc.md +332 -0
- package/dist/packages/components/Button/doc.md +425 -0
- package/dist/packages/components/Calendar/doc.md +465 -0
- package/dist/packages/components/ChartLegend/doc.md +151 -0
- package/dist/packages/components/ChartTooltip/doc.md +124 -0
- package/dist/packages/components/Checkbox/doc.md +329 -0
- package/dist/packages/components/CheckboxGroup/doc.md +242 -0
- package/dist/packages/components/Chip/doc.md +99 -0
- package/dist/packages/components/Combobox/Combobox.d.ts +8 -0
- package/dist/packages/components/Combobox/doc.md +680 -0
- package/dist/packages/components/DataTable/doc.md +1124 -0
- package/dist/packages/components/DatePicker/doc.md +579 -0
- package/dist/packages/components/DateRangePicker/doc.md +638 -0
- package/dist/packages/components/Drawer/doc.md +338 -0
- package/dist/packages/components/Dropdown/Dropdown.d.ts +4 -0
- package/dist/packages/components/Dropdown/doc.md +205 -0
- package/dist/packages/components/EmptyState/doc.md +101 -0
- package/dist/packages/components/FileUpload/doc.md +449 -0
- package/dist/packages/components/Filter/doc.md +196 -0
- package/dist/packages/components/Header/doc.md +373 -0
- package/dist/packages/components/I18nProvider/doc.md +187 -0
- package/dist/packages/components/Icon/doc.md +63 -0
- package/dist/packages/components/Label/doc.md +60 -0
- package/dist/packages/components/LinearProgressBar/doc.md +148 -0
- package/dist/packages/components/Link/doc.md +206 -0
- package/dist/packages/components/List/doc.md +481 -0
- package/dist/packages/components/Loader/doc.md +53 -0
- package/dist/packages/components/Menu/Menu.d.ts +5 -1
- package/dist/packages/components/Menu/doc.md +231 -0
- package/dist/packages/components/Message/doc.md +166 -0
- package/dist/packages/components/Modal/doc.md +289 -0
- package/dist/packages/components/Navigation/doc.md +992 -0
- package/dist/packages/components/NavigationItem/doc.md +167 -0
- package/dist/packages/components/NotificationCard/doc.md +206 -0
- package/dist/packages/components/Notifications/doc.md +240 -0
- package/dist/packages/components/NumberField/doc.md +582 -0
- package/dist/packages/components/PageLayout/doc.md +651 -0
- package/dist/packages/components/Pagination/doc.md +227 -0
- package/dist/packages/components/Popover/doc.md +245 -0
- package/dist/packages/components/Radio/doc.md +370 -0
- package/dist/packages/components/RouterProvider/doc.md +64 -0
- package/dist/packages/components/SearchBar/doc.md +504 -0
- package/dist/packages/components/SegmentedControl/doc.md +398 -0
- package/dist/packages/components/Select/Select.d.ts +4 -0
- package/dist/packages/components/Select/doc.md +1133 -0
- package/dist/packages/components/Skeleton/doc.md +129 -0
- package/dist/packages/components/Slider/doc.md +362 -0
- package/dist/packages/components/Stepper/doc.md +104 -0
- package/dist/packages/components/Switch/doc.md +296 -0
- package/dist/packages/components/Tabs/doc.md +295 -0
- package/dist/packages/components/Tag/doc.md +81 -0
- package/dist/packages/components/TextInput/doc.md +490 -0
- package/dist/packages/components/TimeField/doc.md +353 -0
- package/dist/packages/components/Timeline/doc.md +1046 -0
- package/dist/packages/components/Toaster/doc.md +263 -0
- package/dist/packages/components/ToggleButton/doc.md +108 -0
- package/dist/packages/components/ToggleButtonGroup/doc.md +307 -0
- package/dist/packages/components/Tooltip/doc.md +206 -0
- package/dist/packages/components/YearMonthPicker/YearMonthPicker.d.ts +8 -0
- package/dist/packages/components/YearMonthPicker/doc.md +638 -0
- package/dist/public_docs/components.md +68 -0
- package/dist/public_docs/index.md +30 -0
- package/dist/public_docs/tokens.md +121 -0
- package/package.json +3 -2
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
# DataTable
|
|
2
|
+
|
|
3
|
+
## Props
|
|
4
|
+
|
|
5
|
+
The complete Props documentation with JS doc for this component is available at this path:
|
|
6
|
+
|
|
7
|
+
node_modules/@agregio-solutions/design-system/dist/packages/components/DataTable/DataTable.d.ts
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
Here are the Storybook Stories.
|
|
12
|
+
|
|
13
|
+
Base stories:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
|
+
import Filter, { TypeTableColumnWithFilter } from "../Filter/Filter";
|
|
18
|
+
import arrayOfLength from "@internal/arrayOfLength/arrayOfLength";
|
|
19
|
+
import {
|
|
20
|
+
ColumnFiltersState,
|
|
21
|
+
ColumnSort,
|
|
22
|
+
SortingState,
|
|
23
|
+
createColumnHelper,
|
|
24
|
+
getCoreRowModel,
|
|
25
|
+
getExpandedRowModel,
|
|
26
|
+
getFilteredRowModel,
|
|
27
|
+
getPaginationRowModel,
|
|
28
|
+
getSortedRowModel,
|
|
29
|
+
useReactTable,
|
|
30
|
+
} from "@tanstack/react-table";
|
|
31
|
+
import DataTableHeader from "@packages/components/DataTable/DataTableHeader/DataTableHeader";
|
|
32
|
+
import { useMemo, useState } from "react";
|
|
33
|
+
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
|
34
|
+
import DataTableCell from "@packages/components/DataTable/DataTableCell/DataTableCell";
|
|
35
|
+
import DataTable from "@packages/components/DataTable/DataTable";
|
|
36
|
+
import CheckboxGroup from "../CheckboxGroup/CheckboxGroup";
|
|
37
|
+
import Checkbox from "../Checkbox/Checkbox";
|
|
38
|
+
import Radio from "../Radio/Radio";
|
|
39
|
+
import { I18nProvider } from "react-aria-components";
|
|
40
|
+
import Select from "../Select/Select";
|
|
41
|
+
import SelectItem from "../SelectItem/SelectItem";
|
|
42
|
+
import Button from "../Button/Button";
|
|
43
|
+
import Switch from "../Switch/Switch";
|
|
44
|
+
import { within } from "storybook/test";
|
|
45
|
+
import { someTime } from "@internal/test-utils-storybook/test-utils-storybook";
|
|
46
|
+
import DataTableRoot from "./DataTableRoot/DataTableRoot";
|
|
47
|
+
import DataTableRow from "./DataTableRow/DataTableRow";
|
|
48
|
+
|
|
49
|
+
const meta: Meta<typeof DataTable> = {
|
|
50
|
+
component: DataTable,
|
|
51
|
+
parameters: {
|
|
52
|
+
layout: "padded",
|
|
53
|
+
},
|
|
54
|
+
globals: {
|
|
55
|
+
backgrounds: { value: "light" },
|
|
56
|
+
},
|
|
57
|
+
decorators: [
|
|
58
|
+
(Story) => (
|
|
59
|
+
<I18nProvider locale={"en-EN"}>
|
|
60
|
+
<Story />
|
|
61
|
+
</I18nProvider>
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
export default meta;
|
|
66
|
+
|
|
67
|
+
export const WithReactTableFrontEndManaged: StoryObj<typeof DataTable> = {
|
|
68
|
+
render: () => {
|
|
69
|
+
const ParentComponent = () => {
|
|
70
|
+
// Below are just type definition of the data and its generation
|
|
71
|
+
// Typically you would get this from your API and will not have to create them here
|
|
72
|
+
// We let it so you can fully understand how the filter works based on the data provided
|
|
73
|
+
|
|
74
|
+
type User = {
|
|
75
|
+
firstname: string;
|
|
76
|
+
lastname: string;
|
|
77
|
+
email: string;
|
|
78
|
+
isValidated?: boolean;
|
|
79
|
+
day?: string;
|
|
80
|
+
foods?: Array<string>;
|
|
81
|
+
organization?: { id: string; name: string };
|
|
82
|
+
isActive?: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const ALL_FOODS = useMemo(
|
|
86
|
+
() => ["Pizza", "Hamburger", "Ramen", "Sushi", "Fries"],
|
|
87
|
+
[],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const FOODS = useMemo(
|
|
91
|
+
() => [
|
|
92
|
+
[ALL_FOODS[0], ALL_FOODS[1]],
|
|
93
|
+
[ALL_FOODS[1], ALL_FOODS[2]],
|
|
94
|
+
[ALL_FOODS[2], ALL_FOODS[3]],
|
|
95
|
+
[ALL_FOODS[3], ALL_FOODS[4]],
|
|
96
|
+
[ALL_FOODS[4], ALL_FOODS[0]],
|
|
97
|
+
],
|
|
98
|
+
[ALL_FOODS],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const ORGANIZATIONS = useMemo(
|
|
102
|
+
() => [
|
|
103
|
+
{ id: "1", name: "EDF Store and Forecast" },
|
|
104
|
+
{ id: "2", name: "Agregio Solutions" },
|
|
105
|
+
{ id: "3", name: "Dalkia" },
|
|
106
|
+
],
|
|
107
|
+
[],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const DAYS = useMemo(
|
|
111
|
+
() => ["Monday", "Tuesday", "Wednesday", "Thursday"],
|
|
112
|
+
[],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const data: Array<User> = useMemo(
|
|
116
|
+
() =>
|
|
117
|
+
arrayOfLength(300).map((index) => ({
|
|
118
|
+
firstname: `Firstname ${index + 1}`,
|
|
119
|
+
lastname: `Lastname ${index + 1}`,
|
|
120
|
+
email: `john-doe${index + 1}@example.com`,
|
|
121
|
+
isValidated: index % 2 === 0,
|
|
122
|
+
foods: FOODS[index % FOODS.length],
|
|
123
|
+
day: DAYS[index % DAYS.length],
|
|
124
|
+
organization: ORGANIZATIONS[index % ORGANIZATIONS.length],
|
|
125
|
+
isActive: index % 2 === 0,
|
|
126
|
+
})),
|
|
127
|
+
[DAYS, FOODS, ORGANIZATIONS],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Here starts the interesting stuff: columns definitions.
|
|
131
|
+
// All the features of the filters comes from those columns definitions
|
|
132
|
+
// Please read carefully each column, they all serve as a different example of how to use the filters
|
|
133
|
+
const columnHelper = useMemo(() => createColumnHelper<User>(), []);
|
|
134
|
+
|
|
135
|
+
const columns = useMemo(
|
|
136
|
+
() => [
|
|
137
|
+
columnHelper.accessor("firstname", {
|
|
138
|
+
// Here, because there is no "FilterInDrawer" component, there will be nothing in the filter drawer related to the firstname
|
|
139
|
+
// But the firstname can still be filtered by the global filter (search input)
|
|
140
|
+
header: ({ header }) => (
|
|
141
|
+
<DataTableHeader
|
|
142
|
+
onSortClick={header.column.getToggleSortingHandler()}
|
|
143
|
+
sortDirection={header.column.getIsSorted() || "none"}
|
|
144
|
+
text="Firstname"
|
|
145
|
+
/>
|
|
146
|
+
),
|
|
147
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
148
|
+
}),
|
|
149
|
+
// Here is an example using a "FilterInDrawer" component that render a radio group to filter the day
|
|
150
|
+
// Please note the "filterFn" is "arrIncludes" for this king of value (Array<string>)
|
|
151
|
+
// Also note the usage of `satisfies TypeTableColumnWithFilter<User>` to help you type check the column definition (because meta is a custom property)
|
|
152
|
+
// Finally, you will note that the Chips are auto-generated, if you do not want to use them you can set the `skipFilterChips` to true
|
|
153
|
+
columnHelper.accessor("day", {
|
|
154
|
+
id: "Day",
|
|
155
|
+
header: () => <DataTableHeader text="Day" />,
|
|
156
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
157
|
+
filterFn: "equals",
|
|
158
|
+
meta: {
|
|
159
|
+
FilterInDrawer: ({ column }) => (
|
|
160
|
+
<div>
|
|
161
|
+
<b>Filter by day:</b>
|
|
162
|
+
|
|
163
|
+
{DAYS.map((day) => (
|
|
164
|
+
<Radio
|
|
165
|
+
label={day}
|
|
166
|
+
key={day}
|
|
167
|
+
checked={column.getFilterValue() === day}
|
|
168
|
+
onChange={() => column.setFilterValue(day)}
|
|
169
|
+
/>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
),
|
|
173
|
+
} satisfies TypeTableColumnWithFilter<User>,
|
|
174
|
+
}),
|
|
175
|
+
// Here is another example using a "FilterInDrawer" component that render a checkbox group to filter the food
|
|
176
|
+
// Please note the "filterFn" is "arrIncludesSome" -> we want to include all entries that match at least one food
|
|
177
|
+
// Also note the usage of `satisfies TypeTableColumnWithFilter<User>` to help you type check the column definition (because meta is a custom property)
|
|
178
|
+
// Finally, you will note that the Chips are auto-generated, if you do not want to use them you can set the `skipFilterChips` to true
|
|
179
|
+
columnHelper.accessor("foods", {
|
|
180
|
+
id: "Foods",
|
|
181
|
+
header: () => <DataTableHeader text="Foods" />,
|
|
182
|
+
cell: (props) => (
|
|
183
|
+
<DataTableCell>{props.getValue()?.join(", ")}</DataTableCell>
|
|
184
|
+
),
|
|
185
|
+
filterFn: "arrIncludesSome",
|
|
186
|
+
meta: {
|
|
187
|
+
FilterInDrawer: ({ column }) => (
|
|
188
|
+
<div style={{ marginTop: 8 }}>
|
|
189
|
+
<b>Filter by food (at least one):</b>
|
|
190
|
+
<CheckboxGroup
|
|
191
|
+
value={(column.getFilterValue() as any) || []}
|
|
192
|
+
onChange={column.setFilterValue}
|
|
193
|
+
aria-label="Filter by food (at least one)"
|
|
194
|
+
>
|
|
195
|
+
{ALL_FOODS.map((food) => (
|
|
196
|
+
<div key={food}>
|
|
197
|
+
<Checkbox label={food} value={food} />
|
|
198
|
+
</div>
|
|
199
|
+
))}
|
|
200
|
+
</CheckboxGroup>
|
|
201
|
+
</div>
|
|
202
|
+
),
|
|
203
|
+
} satisfies TypeTableColumnWithFilter<User>,
|
|
204
|
+
}),
|
|
205
|
+
// Here is another example using a "FilterInDrawer" component that render a select to filter the organization
|
|
206
|
+
// Please note the "filterFn" is a custom function that will filter the data based on the organization id
|
|
207
|
+
// Also note the usage of `satisfies TypeTableColumnWithFilter<User>` to help you type check the column definition (because meta is a custom property)
|
|
208
|
+
// And most important here, we have a custom meta property `filterChips` that will generate a chip to display the selected organization
|
|
209
|
+
// The Filter can not handle automatic chips when the data is a complex object, so you have to handle that manually
|
|
210
|
+
columnHelper.accessor("organization", {
|
|
211
|
+
id: "Organization",
|
|
212
|
+
header: () => <DataTableHeader text="Organization" />,
|
|
213
|
+
cell: (props) => (
|
|
214
|
+
<DataTableCell>{props.getValue()?.name}</DataTableCell>
|
|
215
|
+
),
|
|
216
|
+
filterFn: (row, _, filterValue) => {
|
|
217
|
+
if (!filterValue) return true;
|
|
218
|
+
return row.original.organization?.id === filterValue;
|
|
219
|
+
},
|
|
220
|
+
meta: {
|
|
221
|
+
filterChips: ({ column }) => {
|
|
222
|
+
const value = column.getFilterValue();
|
|
223
|
+
const organization = ORGANIZATIONS.find(
|
|
224
|
+
(org) => org.id === value,
|
|
225
|
+
);
|
|
226
|
+
if (!organization) return [];
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
value: `Organization: ${organization.name}`,
|
|
230
|
+
onClose: () => column.setFilterValue(null),
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
},
|
|
234
|
+
FilterInDrawer: ({ column }) => (
|
|
235
|
+
<Select
|
|
236
|
+
id="organization-select"
|
|
237
|
+
label="Filter by organization:"
|
|
238
|
+
mode="single"
|
|
239
|
+
value={(column.getFilterValue() as any) || null}
|
|
240
|
+
onChange={column.setFilterValue}
|
|
241
|
+
fullWidth
|
|
242
|
+
wrapperProps={{ style: { marginBottom: 8 } }}
|
|
243
|
+
>
|
|
244
|
+
{ORGANIZATIONS.map((organization) => (
|
|
245
|
+
<SelectItem
|
|
246
|
+
text={organization.name}
|
|
247
|
+
id={organization.id}
|
|
248
|
+
key={organization.id}
|
|
249
|
+
/>
|
|
250
|
+
))}
|
|
251
|
+
</Select>
|
|
252
|
+
),
|
|
253
|
+
} satisfies TypeTableColumnWithFilter<User>,
|
|
254
|
+
}),
|
|
255
|
+
// Here is an example that uses a filter outside of the drawer.
|
|
256
|
+
// See the "extraLeftContent" prop to see how to connect it to react-table filtering features
|
|
257
|
+
columnHelper.accessor("isActive", {
|
|
258
|
+
header: () => <DataTableHeader text="Active" />,
|
|
259
|
+
cell: (props) => (
|
|
260
|
+
<DataTableCell>{props.getValue() ? "✅" : "❌"}</DataTableCell>
|
|
261
|
+
),
|
|
262
|
+
filterFn: (row, _, filterValue) => {
|
|
263
|
+
if (!filterValue) return true;
|
|
264
|
+
return !!row.original.isActive;
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
// Here is an example of an empty column, for example to display an actions bar
|
|
268
|
+
// You can note that the header is empty, the text is optional
|
|
269
|
+
columnHelper.display({
|
|
270
|
+
id: "actions",
|
|
271
|
+
header: () => <DataTableHeader />,
|
|
272
|
+
cell: () => (
|
|
273
|
+
<DataTableCell>
|
|
274
|
+
<Button mode="secondary" iconLeft="edit" />
|
|
275
|
+
</DataTableCell>
|
|
276
|
+
),
|
|
277
|
+
}),
|
|
278
|
+
],
|
|
279
|
+
[ALL_FOODS, DAYS, ORGANIZATIONS, columnHelper],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// And here we define the table as usual
|
|
283
|
+
const table = useReactTable({
|
|
284
|
+
data,
|
|
285
|
+
columns,
|
|
286
|
+
initialState: {
|
|
287
|
+
globalFilter: "", // Add this to show the global filter (aka the Search bar)
|
|
288
|
+
pagination: {
|
|
289
|
+
pageSize: 12, // Add this to customize the page size
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
getCoreRowModel: getCoreRowModel(),
|
|
293
|
+
getSortedRowModel: getSortedRowModel(),
|
|
294
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
295
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Get the pagination state
|
|
299
|
+
const pagination = table.getState().pagination;
|
|
300
|
+
|
|
301
|
+
// Render the table using the base DataTable component
|
|
302
|
+
return (
|
|
303
|
+
<>
|
|
304
|
+
<Filter<User>
|
|
305
|
+
onDownloadClick={() => alert("Download")}
|
|
306
|
+
onSettingsClick={() => alert("Settings")}
|
|
307
|
+
table={table}
|
|
308
|
+
extraLeftContent={
|
|
309
|
+
<>
|
|
310
|
+
<Switch
|
|
311
|
+
label="Only active"
|
|
312
|
+
isSelected={!!table.getColumn("isActive")?.getFilterValue()}
|
|
313
|
+
onChange={() =>
|
|
314
|
+
table
|
|
315
|
+
.getColumn("isActive")
|
|
316
|
+
?.setFilterValue((prevValue: boolean) => !prevValue)
|
|
317
|
+
}
|
|
318
|
+
/>
|
|
319
|
+
</>
|
|
320
|
+
}
|
|
321
|
+
extraRightContent={
|
|
322
|
+
<>
|
|
323
|
+
<Button
|
|
324
|
+
mode="secondary"
|
|
325
|
+
onClick={() => alert("Secondary")}
|
|
326
|
+
text="Secondary"
|
|
327
|
+
/>
|
|
328
|
+
<Button
|
|
329
|
+
mode="primary"
|
|
330
|
+
onClick={() => alert("Primary")}
|
|
331
|
+
text="Primary"
|
|
332
|
+
/>
|
|
333
|
+
</>
|
|
334
|
+
}
|
|
335
|
+
/>
|
|
336
|
+
<DataTable
|
|
337
|
+
table={table}
|
|
338
|
+
pagination={pagination}
|
|
339
|
+
itemsPerPageOptions={[12, 24, 36, 48, 60]} // Add this to customize the page size options available in the select
|
|
340
|
+
/>
|
|
341
|
+
</>
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return <ParentComponent />;
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
type User = {
|
|
350
|
+
firstname: string;
|
|
351
|
+
lastname: string;
|
|
352
|
+
email: string;
|
|
353
|
+
isValidated?: boolean;
|
|
354
|
+
day?: string;
|
|
355
|
+
foods?: Array<string>;
|
|
356
|
+
organization?: { id: string; name: string };
|
|
357
|
+
isActive?: boolean;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Just a fake API to simulate a backend
|
|
361
|
+
async function api(params: {
|
|
362
|
+
page: number;
|
|
363
|
+
pageSize: number;
|
|
364
|
+
sorting: ColumnSort;
|
|
365
|
+
emails?: Array<string>;
|
|
366
|
+
}) {
|
|
367
|
+
await someTime(100);
|
|
368
|
+
const { page, pageSize, sorting, emails } = params;
|
|
369
|
+
|
|
370
|
+
let data: Array<User> = arrayOfLength(300).map((index) => ({
|
|
371
|
+
firstname: `Firstname ${index + 1}`,
|
|
372
|
+
lastname: `Lastname ${index + 1}`,
|
|
373
|
+
email: `john-doe${index + 1}@example.com`,
|
|
374
|
+
}));
|
|
375
|
+
|
|
376
|
+
if (emails && emails.length > 0) {
|
|
377
|
+
data = data.filter((user) => emails.includes(user.email));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (sorting) {
|
|
381
|
+
const key = sorting.id;
|
|
382
|
+
|
|
383
|
+
data.sort((a: any, b: any) => {
|
|
384
|
+
if (sorting.desc) {
|
|
385
|
+
return b[key].localeCompare(a[key], undefined, { numeric: true });
|
|
386
|
+
} else {
|
|
387
|
+
a[key].localeCompare(b[key], undefined, { numeric: true });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
data: data.slice(page * pageSize, (page + 1) * pageSize),
|
|
394
|
+
total: data.length,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export const WithReactTableBackEndManaged: StoryObj<typeof DataTable> = {
|
|
399
|
+
parameters: {
|
|
400
|
+
chromatic: { disableSnapshot: true },
|
|
401
|
+
layout: "padded",
|
|
402
|
+
},
|
|
403
|
+
globals: {
|
|
404
|
+
backgrounds: { value: "light" },
|
|
405
|
+
},
|
|
406
|
+
render: () => {
|
|
407
|
+
function UsersTable() {
|
|
408
|
+
// Create state for the pagination
|
|
409
|
+
const [pagination, setPagination] = useState({
|
|
410
|
+
pageIndex: 0, //initial page index
|
|
411
|
+
pageSize: 10, //default page size
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Create state for the sorting (with an initial state)
|
|
415
|
+
const [sorting, setSorting] = useState<SortingState>([
|
|
416
|
+
{ id: "email", desc: false },
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
// Create state for the filters
|
|
420
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
|
|
421
|
+
[],
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Fetch data using the table state (filters, pagination, sorting, etc.)
|
|
425
|
+
const { data } = useQuery({
|
|
426
|
+
placeholderData: keepPreviousData, // from @tanstack/react-query, recommended to use it in this scenario
|
|
427
|
+
queryKey: ["users", pagination, sorting, columnFilters],
|
|
428
|
+
queryFn: () =>
|
|
429
|
+
// You API call goes here, you have access to the table state to build your query params
|
|
430
|
+
api({
|
|
431
|
+
page: pagination.pageIndex,
|
|
432
|
+
pageSize: pagination.pageSize,
|
|
433
|
+
sorting: sorting[0], // handle only one sorting for this example
|
|
434
|
+
emails: columnFilters.find((filter) => filter.id === "email")
|
|
435
|
+
?.value as Array<string>,
|
|
436
|
+
}),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// You will need to create a column helper
|
|
440
|
+
// ⚠ This should be memoized!
|
|
441
|
+
const columnHelper = useMemo(() => createColumnHelper<User>(), []);
|
|
442
|
+
|
|
443
|
+
// Then, you can define the columns
|
|
444
|
+
// ⚠ This should be memoized!
|
|
445
|
+
const columns = useMemo(
|
|
446
|
+
() => [
|
|
447
|
+
// Example of a custom column for initials (no data model, so no filters, etc)
|
|
448
|
+
columnHelper.display({
|
|
449
|
+
id: "initials",
|
|
450
|
+
header: () => <DataTableHeader text="Initials" />,
|
|
451
|
+
cell: (props) => (
|
|
452
|
+
<DataTableCell>
|
|
453
|
+
{/*Here is an example of how you can access the data model values directly (if needed) */}
|
|
454
|
+
{props.row.original.firstname[0]}
|
|
455
|
+
{props.row.original.lastname[0]}
|
|
456
|
+
</DataTableCell>
|
|
457
|
+
),
|
|
458
|
+
}),
|
|
459
|
+
// Example of a column with sorting (accessor means it has data model)
|
|
460
|
+
columnHelper.accessor("firstname", {
|
|
461
|
+
header: ({ header }) => (
|
|
462
|
+
<DataTableHeader
|
|
463
|
+
onSortClick={header.column.getToggleSortingHandler()}
|
|
464
|
+
sortDirection={header.column.getIsSorted() || "none"}
|
|
465
|
+
text="Firstname"
|
|
466
|
+
/>
|
|
467
|
+
),
|
|
468
|
+
cell: (props) => (
|
|
469
|
+
<DataTableCell iconLeft="star_outline">
|
|
470
|
+
{/* because the accessor key ("firstname") is a data model object key, we can access it directly with getValue() */}
|
|
471
|
+
{props.getValue()}
|
|
472
|
+
</DataTableCell>
|
|
473
|
+
),
|
|
474
|
+
}),
|
|
475
|
+
columnHelper.accessor("lastname", {
|
|
476
|
+
header: ({ header }) => (
|
|
477
|
+
<DataTableHeader
|
|
478
|
+
onSortClick={header.column.getToggleSortingHandler()}
|
|
479
|
+
sortDirection={header.column.getIsSorted() || "none"}
|
|
480
|
+
text="Lastname"
|
|
481
|
+
/>
|
|
482
|
+
),
|
|
483
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
484
|
+
}),
|
|
485
|
+
columnHelper.accessor("email", {
|
|
486
|
+
header: ({ header }) => (
|
|
487
|
+
<DataTableHeader
|
|
488
|
+
onSortClick={header.column.getToggleSortingHandler()}
|
|
489
|
+
sortDirection={header.column.getIsSorted() || "none"}
|
|
490
|
+
text="Email"
|
|
491
|
+
iconLeft="send"
|
|
492
|
+
/>
|
|
493
|
+
),
|
|
494
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
495
|
+
meta: {
|
|
496
|
+
FilterInDrawer: ({ column }) => (
|
|
497
|
+
<Select
|
|
498
|
+
id="email-select"
|
|
499
|
+
label="Filter by email:"
|
|
500
|
+
mode="multiple"
|
|
501
|
+
selectedKeys={(column.getFilterValue() as any) || []}
|
|
502
|
+
onSelectionChange={column.setFilterValue}
|
|
503
|
+
fullWidth
|
|
504
|
+
wrapperProps={{ style: { marginBottom: 8 } }}
|
|
505
|
+
>
|
|
506
|
+
{arrayOfLength(10).map((index) => (
|
|
507
|
+
<SelectItem
|
|
508
|
+
text={`john-doe${index + 1}@example.com`}
|
|
509
|
+
id={`john-doe${index + 1}@example.com`}
|
|
510
|
+
key={`john-doe${index + 1}@example.com`}
|
|
511
|
+
/>
|
|
512
|
+
))}
|
|
513
|
+
</Select>
|
|
514
|
+
),
|
|
515
|
+
} satisfies TypeTableColumnWithFilter<User>,
|
|
516
|
+
}),
|
|
517
|
+
],
|
|
518
|
+
[columnHelper],
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// Instantiate the table with @tanstack/react-table
|
|
522
|
+
const table = useReactTable({
|
|
523
|
+
data: data?.data || [],
|
|
524
|
+
columns,
|
|
525
|
+
getCoreRowModel: getCoreRowModel(), // from @tanstack/react-table
|
|
526
|
+
manualPagination: true,
|
|
527
|
+
manualSorting: true,
|
|
528
|
+
manualFiltering: true,
|
|
529
|
+
onPaginationChange: setPagination, //update the pagination state when internal APIs mutate the pagination state
|
|
530
|
+
onSortingChange: setSorting, //update the sorting state when internal APIs mutate the sorting state
|
|
531
|
+
onColumnFiltersChange: setColumnFilters, //update the column filters state when internal APIs mutate the column filters state
|
|
532
|
+
rowCount: data?.total, //total number of rows
|
|
533
|
+
state: {
|
|
534
|
+
pagination,
|
|
535
|
+
sorting,
|
|
536
|
+
columnFilters,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Render the table using the base DataTable component
|
|
541
|
+
return (
|
|
542
|
+
<>
|
|
543
|
+
<Filter<User> table={table} />
|
|
544
|
+
<DataTable table={table} pagination={pagination} />
|
|
545
|
+
</>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return <UsersTable />;
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
export const Empty: StoryObj<typeof DataTable> = {
|
|
554
|
+
render: () => {
|
|
555
|
+
const ParentComponent = () => {
|
|
556
|
+
const columnHelper = useMemo(() => createColumnHelper<User>(), []);
|
|
557
|
+
|
|
558
|
+
const data = useMemo(() => [], []);
|
|
559
|
+
|
|
560
|
+
const columns = useMemo(
|
|
561
|
+
() => [
|
|
562
|
+
columnHelper.display({
|
|
563
|
+
id: "example",
|
|
564
|
+
header: () => <DataTableHeader text="Example" />,
|
|
565
|
+
cell: () => <DataTableCell>Example</DataTableCell>,
|
|
566
|
+
}),
|
|
567
|
+
],
|
|
568
|
+
[columnHelper],
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const table = useReactTable({
|
|
572
|
+
data,
|
|
573
|
+
columns,
|
|
574
|
+
initialState: {
|
|
575
|
+
globalFilter: "",
|
|
576
|
+
},
|
|
577
|
+
getCoreRowModel: getCoreRowModel(),
|
|
578
|
+
getSortedRowModel: getSortedRowModel(),
|
|
579
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
580
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const pagination = table.getState().pagination;
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<>
|
|
587
|
+
<Filter<User>
|
|
588
|
+
onDownloadClick={() => alert("Download")}
|
|
589
|
+
onSettingsClick={() => alert("Settings")}
|
|
590
|
+
table={table}
|
|
591
|
+
/>
|
|
592
|
+
<DataTable table={table} pagination={pagination} />
|
|
593
|
+
</>
|
|
594
|
+
);
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
return <ParentComponent />;
|
|
598
|
+
},
|
|
599
|
+
play: async ({ canvasElement }) => {
|
|
600
|
+
const canvas = within(canvasElement);
|
|
601
|
+
await canvas.findByText("Sorry, there is currently no data to display.");
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
export const WithSubColumns: StoryObj<typeof DataTable> = {
|
|
606
|
+
render: () => {
|
|
607
|
+
const data: Array<User> = arrayOfLength(300).map((index) => ({
|
|
608
|
+
firstname: `Firstname ${index + 1}`,
|
|
609
|
+
lastname: `Lastname ${index + 1}`,
|
|
610
|
+
email: `john-doe${index + 1}@example.com`,
|
|
611
|
+
}));
|
|
612
|
+
|
|
613
|
+
const ParentComponent = () => {
|
|
614
|
+
const columnHelper = useMemo(() => createColumnHelper<User>(), []);
|
|
615
|
+
|
|
616
|
+
const columns = useMemo(
|
|
617
|
+
() => [
|
|
618
|
+
columnHelper.group({
|
|
619
|
+
id: "name",
|
|
620
|
+
header: (group) => (
|
|
621
|
+
<DataTableHeader
|
|
622
|
+
text="Name group"
|
|
623
|
+
colSpan={group.header.colSpan}
|
|
624
|
+
/>
|
|
625
|
+
),
|
|
626
|
+
columns: [
|
|
627
|
+
columnHelper.accessor("firstname", {
|
|
628
|
+
id: "Firstname",
|
|
629
|
+
header: () => <DataTableHeader text="Firstname" />,
|
|
630
|
+
cell: (props) => (
|
|
631
|
+
<DataTableCell>{props.getValue()}</DataTableCell>
|
|
632
|
+
),
|
|
633
|
+
}),
|
|
634
|
+
columnHelper.accessor("lastname", {
|
|
635
|
+
id: "Lastname",
|
|
636
|
+
header: () => <DataTableHeader text="Lastname" />,
|
|
637
|
+
cell: (props) => (
|
|
638
|
+
<DataTableCell>{props.getValue()}</DataTableCell>
|
|
639
|
+
),
|
|
640
|
+
}),
|
|
641
|
+
],
|
|
642
|
+
}),
|
|
643
|
+
columnHelper.group({
|
|
644
|
+
id: "other",
|
|
645
|
+
header: (group) => (
|
|
646
|
+
<DataTableHeader
|
|
647
|
+
text="Other group"
|
|
648
|
+
colSpan={group.header.colSpan}
|
|
649
|
+
border="left"
|
|
650
|
+
/>
|
|
651
|
+
),
|
|
652
|
+
columns: [
|
|
653
|
+
columnHelper.accessor("email", {
|
|
654
|
+
id: "Email",
|
|
655
|
+
header: () => <DataTableHeader border="left" text="Email" />,
|
|
656
|
+
cell: (props) => (
|
|
657
|
+
<DataTableCell border="left">
|
|
658
|
+
{props.getValue()}
|
|
659
|
+
</DataTableCell>
|
|
660
|
+
),
|
|
661
|
+
}),
|
|
662
|
+
],
|
|
663
|
+
}),
|
|
664
|
+
],
|
|
665
|
+
[columnHelper],
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
const table = useReactTable({
|
|
669
|
+
data,
|
|
670
|
+
columns,
|
|
671
|
+
initialState: {
|
|
672
|
+
globalFilter: "",
|
|
673
|
+
},
|
|
674
|
+
getCoreRowModel: getCoreRowModel(),
|
|
675
|
+
getSortedRowModel: getSortedRowModel(),
|
|
676
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
677
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const pagination = table.getState().pagination;
|
|
681
|
+
|
|
682
|
+
return (
|
|
683
|
+
<>
|
|
684
|
+
<Filter<User> table={table} />
|
|
685
|
+
<DataTable table={table} pagination={pagination} />
|
|
686
|
+
</>
|
|
687
|
+
);
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
return <ParentComponent />;
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
export const WithContentOverflow: StoryObj<typeof DataTable> = {
|
|
695
|
+
render: () => {
|
|
696
|
+
const data: Array<any> = arrayOfLength(300).map((index) => ({
|
|
697
|
+
firstname: `Firstnameverylongthatwilloverflowonthescreensqhjdhqskdhkqshdkhqkdshhqsdhkqhshqhdskqjshdjhqsjkhdkqhskjdhqjkshdqkshdkhqsdkjqhdsjkhqsdk ${index + 1}`,
|
|
698
|
+
}));
|
|
699
|
+
|
|
700
|
+
const ParentComponent = () => {
|
|
701
|
+
const columnHelper = useMemo(() => createColumnHelper<any>(), []);
|
|
702
|
+
|
|
703
|
+
const columns = useMemo(
|
|
704
|
+
() => [
|
|
705
|
+
columnHelper.accessor("firstname", {
|
|
706
|
+
id: "Firstname",
|
|
707
|
+
header: () => <DataTableHeader text="Firstname" />,
|
|
708
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
709
|
+
}),
|
|
710
|
+
],
|
|
711
|
+
[columnHelper],
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
const table = useReactTable({
|
|
715
|
+
data,
|
|
716
|
+
columns,
|
|
717
|
+
initialState: {
|
|
718
|
+
globalFilter: "",
|
|
719
|
+
},
|
|
720
|
+
getCoreRowModel: getCoreRowModel(),
|
|
721
|
+
getSortedRowModel: getSortedRowModel(),
|
|
722
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
723
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const pagination = table.getState().pagination;
|
|
727
|
+
|
|
728
|
+
return (
|
|
729
|
+
<>
|
|
730
|
+
<Filter<User> table={table} />
|
|
731
|
+
<DataTable table={table} pagination={pagination} />
|
|
732
|
+
</>
|
|
733
|
+
);
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
return <ParentComponent />;
|
|
737
|
+
},
|
|
738
|
+
decorators: [
|
|
739
|
+
(Story) => (
|
|
740
|
+
<div
|
|
741
|
+
style={{ width: "100%", maxWidth: "700px", border: "1px solid red" }}
|
|
742
|
+
>
|
|
743
|
+
<Story />
|
|
744
|
+
</div>
|
|
745
|
+
),
|
|
746
|
+
],
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
export const CustomExpandUi: StoryObj<typeof DataTable> = {
|
|
750
|
+
render: () => {
|
|
751
|
+
const ParentComponent = () => {
|
|
752
|
+
const columnHelper = useMemo(() => createColumnHelper<User>(), []);
|
|
753
|
+
|
|
754
|
+
// Fake data, should come from an API typically
|
|
755
|
+
const data = useMemo(
|
|
756
|
+
() => [
|
|
757
|
+
{ firstname: "John", lastname: "Doe", email: "john-doe@example.com" },
|
|
758
|
+
{ firstname: "Jane", lastname: "Doe", email: "jane-doe@example.com" },
|
|
759
|
+
],
|
|
760
|
+
[],
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
const columns = useMemo(
|
|
764
|
+
() => [
|
|
765
|
+
columnHelper.accessor("firstname", {
|
|
766
|
+
header: () => <DataTableHeader text="Firstname" />,
|
|
767
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
768
|
+
}),
|
|
769
|
+
columnHelper.display({
|
|
770
|
+
id: "expander",
|
|
771
|
+
header: () => <DataTableHeader text="" />,
|
|
772
|
+
cell: ({ row }) => (
|
|
773
|
+
<DataTableCell style={{ width: 72 }}>
|
|
774
|
+
<Button
|
|
775
|
+
mode="tertiary"
|
|
776
|
+
iconLeft={row.getIsExpanded() ? "expand_less" : "expand_more"} // Show a different icon when the row is expanded
|
|
777
|
+
onClick={row.getToggleExpandedHandler()} // Here you plug your button with react-table expanded handler
|
|
778
|
+
/>
|
|
779
|
+
</DataTableCell>
|
|
780
|
+
),
|
|
781
|
+
}),
|
|
782
|
+
],
|
|
783
|
+
[columnHelper],
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
const table = useReactTable({
|
|
787
|
+
data,
|
|
788
|
+
columns,
|
|
789
|
+
getRowCanExpand: () => true, // tell the table that all rows can be expanded
|
|
790
|
+
getCoreRowModel: getCoreRowModel(),
|
|
791
|
+
getSortedRowModel: getSortedRowModel(),
|
|
792
|
+
getExpandedRowModel: getExpandedRowModel(), // from @tanstack/react-table
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
return (
|
|
796
|
+
<>
|
|
797
|
+
<DataTable
|
|
798
|
+
table={table}
|
|
799
|
+
// Here you can tell the table how to render an expanded row
|
|
800
|
+
renderSubComponent={({ row }) => (
|
|
801
|
+
<pre>
|
|
802
|
+
<code>{JSON.stringify(row.original, null, 2)}</code>
|
|
803
|
+
</pre>
|
|
804
|
+
)}
|
|
805
|
+
/>
|
|
806
|
+
</>
|
|
807
|
+
);
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
return <ParentComponent />;
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
export const ExpandingSubRows: StoryObj<typeof DataTable> = {
|
|
815
|
+
render: () => {
|
|
816
|
+
type UserWithAffiliates = User & { affiliates?: Array<UserWithAffiliates> };
|
|
817
|
+
|
|
818
|
+
const ParentComponent = () => {
|
|
819
|
+
const columnHelper = useMemo(
|
|
820
|
+
() => createColumnHelper<UserWithAffiliates>(),
|
|
821
|
+
[],
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
// Fake data, should come from an API typically
|
|
825
|
+
const data = useMemo(
|
|
826
|
+
() => [
|
|
827
|
+
{
|
|
828
|
+
firstname: "John",
|
|
829
|
+
lastname: "Doe",
|
|
830
|
+
email: "john-doe@example.com",
|
|
831
|
+
affiliates: [
|
|
832
|
+
{
|
|
833
|
+
firstname: "Marcel",
|
|
834
|
+
lastname: "Potter",
|
|
835
|
+
email: "marcel@example.com",
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
firstname: "Willy",
|
|
839
|
+
lastname: "Potter",
|
|
840
|
+
email: "willy@example.com",
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
firstname: "Michele",
|
|
844
|
+
lastname: "Potter",
|
|
845
|
+
email: "michele@example.com",
|
|
846
|
+
},
|
|
847
|
+
],
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
firstname: "Jane",
|
|
851
|
+
lastname: "Doe",
|
|
852
|
+
email: "jane-doe@example.com",
|
|
853
|
+
affiliates: [
|
|
854
|
+
{
|
|
855
|
+
firstname: "Marcel",
|
|
856
|
+
lastname: "Potter",
|
|
857
|
+
email: "marcel@example.com",
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
},
|
|
861
|
+
],
|
|
862
|
+
[],
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
const columns = useMemo(
|
|
866
|
+
() => [
|
|
867
|
+
columnHelper.accessor("firstname", {
|
|
868
|
+
header: () => <DataTableHeader text="Firstname" />,
|
|
869
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
870
|
+
}),
|
|
871
|
+
columnHelper.accessor("lastname", {
|
|
872
|
+
id: "Lastname",
|
|
873
|
+
header: () => <DataTableHeader text="Lastname" />,
|
|
874
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
875
|
+
}),
|
|
876
|
+
columnHelper.display({
|
|
877
|
+
id: "expander",
|
|
878
|
+
header: () => <DataTableHeader text="" />,
|
|
879
|
+
cell: ({ row }) => (
|
|
880
|
+
<DataTableCell style={{ width: 72 }}>
|
|
881
|
+
{row.getCanExpand() && (
|
|
882
|
+
<Button
|
|
883
|
+
mode="tertiary"
|
|
884
|
+
iconLeft={
|
|
885
|
+
// Show a different icon when the row is expanded
|
|
886
|
+
row.getIsExpanded() ? "expand_less" : "expand_more"
|
|
887
|
+
}
|
|
888
|
+
onClick={row.getToggleExpandedHandler()} // Here you plug your button with react-table expanded handler
|
|
889
|
+
/>
|
|
890
|
+
)}
|
|
891
|
+
</DataTableCell>
|
|
892
|
+
),
|
|
893
|
+
}),
|
|
894
|
+
],
|
|
895
|
+
[columnHelper],
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
const table = useReactTable({
|
|
899
|
+
data,
|
|
900
|
+
columns,
|
|
901
|
+
getSubRows: (row) => row.affiliates,
|
|
902
|
+
getCoreRowModel: getCoreRowModel(),
|
|
903
|
+
getSortedRowModel: getSortedRowModel(),
|
|
904
|
+
getExpandedRowModel: getExpandedRowModel(), // from @tanstack/react-table
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
return <DataTable table={table} />;
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
return <ParentComponent />;
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
export const WithoutReactTable: StoryObj<typeof DataTable> = {
|
|
915
|
+
render: () => {
|
|
916
|
+
const data = [
|
|
917
|
+
{ firstname: "John", lastname: "Doe", email: "john-doe@example.com" },
|
|
918
|
+
{ firstname: "Jane", lastname: "Doe", email: "jane-doe@example.com" },
|
|
919
|
+
{ firstname: "Jim", lastname: "Beam", email: "jim-beam@example.com" },
|
|
920
|
+
];
|
|
921
|
+
|
|
922
|
+
return (
|
|
923
|
+
<DataTableRoot>
|
|
924
|
+
<thead>
|
|
925
|
+
<tr>
|
|
926
|
+
<DataTableHeader text="Firstname" />
|
|
927
|
+
<DataTableHeader text="Lastname" />
|
|
928
|
+
<DataTableHeader text="Email" />
|
|
929
|
+
</tr>
|
|
930
|
+
</thead>
|
|
931
|
+
<tbody>
|
|
932
|
+
{data.map((item) => (
|
|
933
|
+
<DataTableRow key={item.email}>
|
|
934
|
+
<DataTableCell>{item.firstname}</DataTableCell>
|
|
935
|
+
<DataTableCell>{item.lastname}</DataTableCell>
|
|
936
|
+
<DataTableCell>{item.email}</DataTableCell>
|
|
937
|
+
</DataTableRow>
|
|
938
|
+
))}
|
|
939
|
+
</tbody>
|
|
940
|
+
</DataTableRoot>
|
|
941
|
+
);
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
export const Sizes: StoryObj<typeof DataTable> = {
|
|
946
|
+
render: () => {
|
|
947
|
+
const ParentComponent = () => {
|
|
948
|
+
const data: Array<User> = useMemo(
|
|
949
|
+
() =>
|
|
950
|
+
arrayOfLength(5).map((index) => ({
|
|
951
|
+
firstname: `Firstname ${index + 1}`,
|
|
952
|
+
lastname: `Lastname ${index + 1}`,
|
|
953
|
+
email: `john-doe${index + 1}@example.com`,
|
|
954
|
+
})),
|
|
955
|
+
[],
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
const columnHelper = useMemo(() => createColumnHelper<User>(), []);
|
|
959
|
+
|
|
960
|
+
const columns = useMemo(
|
|
961
|
+
() => [
|
|
962
|
+
columnHelper.accessor("firstname", {
|
|
963
|
+
header: () => <DataTableHeader text="Firstname" />,
|
|
964
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
965
|
+
}),
|
|
966
|
+
columnHelper.accessor("lastname", {
|
|
967
|
+
header: () => <DataTableHeader text="Lastname" />,
|
|
968
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
969
|
+
}),
|
|
970
|
+
columnHelper.accessor("email", {
|
|
971
|
+
header: () => <DataTableHeader text="Email" />,
|
|
972
|
+
cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
|
|
973
|
+
}),
|
|
974
|
+
],
|
|
975
|
+
[columnHelper],
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
const TableWithSize = ({
|
|
979
|
+
size,
|
|
980
|
+
}: {
|
|
981
|
+
size: "small" | "default" | "large";
|
|
982
|
+
}) => {
|
|
983
|
+
const table = useReactTable({
|
|
984
|
+
data,
|
|
985
|
+
columns,
|
|
986
|
+
getCoreRowModel: getCoreRowModel(),
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
return (
|
|
990
|
+
<div style={{ marginBottom: 32 }}>
|
|
991
|
+
<h3 style={{ marginBottom: 8 }}>Size: {size}</h3>
|
|
992
|
+
<DataTable table={table} size={size} />
|
|
993
|
+
</div>
|
|
994
|
+
);
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
return (
|
|
998
|
+
<>
|
|
999
|
+
<TableWithSize size="small" />
|
|
1000
|
+
<TableWithSize size="default" />
|
|
1001
|
+
<TableWithSize size="large" />
|
|
1002
|
+
</>
|
|
1003
|
+
);
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
return <ParentComponent />;
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
## Developer notes
|
|
1012
|
+
|
|
1013
|
+
Here are the notes available for the developer on the built Storybook, you can read them to understand the component and how to use it.
|
|
1014
|
+
|
|
1015
|
+
```mdx
|
|
1016
|
+
import { Meta, DocsStory } from "@storybook/addon-docs/blocks";
|
|
1017
|
+
|
|
1018
|
+
import * as DataTable from "./DataTable.stories";
|
|
1019
|
+
import * as DataTableRoot from "./DataTableRoot/DataTableRoot.stories";
|
|
1020
|
+
|
|
1021
|
+
<Meta of={DataTable} />
|
|
1022
|
+
|
|
1023
|
+
# DataTable
|
|
1024
|
+
|
|
1025
|
+
## Philosophy and usage
|
|
1026
|
+
|
|
1027
|
+
Creating a table consists mainly of two parts:
|
|
1028
|
+
|
|
1029
|
+
- Creating the UI to display the data
|
|
1030
|
+
- Handling the data manipulation (sorting, filtering, paginating, etc.)
|
|
1031
|
+
|
|
1032
|
+
The Design System will only provide the UI part, and **you will have to handle the data manipulation yourself**.
|
|
1033
|
+
|
|
1034
|
+
But because this is not trivial at all, we encourage you to use a third-party library.
|
|
1035
|
+
Our recommendation is to use the excellent headless library [react-table](https://tanstack.com/table/latest).
|
|
1036
|
+
And because we know your time is precious, **we provide you with a good starting point to create your custom abstraction using `react-table` that you can copy-paste in your project**.
|
|
1037
|
+
|
|
1038
|
+
Feel free to make some adaptations when needed, and to create your own abstraction above this UI component that fits your project's specific constraints and patterns.
|
|
1039
|
+
All those examples are here to help you get started, they are not meant to be a one-size-fits-all solution.
|
|
1040
|
+
|
|
1041
|
+
## Creating the UI
|
|
1042
|
+
|
|
1043
|
+
You will need quite a few components to create a table:
|
|
1044
|
+
|
|
1045
|
+
- [DataTableRoot](?path=/docs/components-datatable-datatableroot--docs): very basic root to give base styles to your table
|
|
1046
|
+
- [DataTableHeader](?path=/docs/components-datatable-datatableheader--docs): to display the header of your table
|
|
1047
|
+
- [DataTableRow](?path=/docs/components-datatable-datatablerow--docs): to display a row of data
|
|
1048
|
+
- [DataTableCell](?path=/docs/components-datatable-datatablecell--docs): to display a cell of data
|
|
1049
|
+
- [Pagination](?path=/docs/components-pagination--docs): to handle the pagination
|
|
1050
|
+
- [Filter](?path=/docs/components-filter--docs): to handle the filtering
|
|
1051
|
+
|
|
1052
|
+
If you do not have specific needs, **you can use the `DataTable` component**, which is a composition of the above components.
|
|
1053
|
+
This way, you can start creating your table quickly.
|
|
1054
|
+
|
|
1055
|
+
But if one day you need to add a specific feature, you can easily replace the `DataTable` component with your own composition (feel free to copy paste the code of the `DataTable` as a starting point).
|
|
1056
|
+
|
|
1057
|
+
## Example Integration with react-table
|
|
1058
|
+
|
|
1059
|
+
When manipulating data, there is basically two ways to handle it:
|
|
1060
|
+
|
|
1061
|
+
- Front-end managed: the data is fetched once and all the data manipulation is done on the front-end
|
|
1062
|
+
- Back-end managed: the data is fetched on demand, and the data manipulation is done on the back-end
|
|
1063
|
+
|
|
1064
|
+
Most of the time, just go with front-end managed data, it's easier to implement, and it's faster for the user in most cases.
|
|
1065
|
+
|
|
1066
|
+
The decision to switch to back-end managed data should be made when there are performance issues.
|
|
1067
|
+
Should it be from a too intensive load for the database, a payload so big that the network request becomes very slow, or any other reason.
|
|
1068
|
+
|
|
1069
|
+
Below are two examples of how to integrate the `DataTable` component with `react-table` in both cases.
|
|
1070
|
+
|
|
1071
|
+
### Front-end managed
|
|
1072
|
+
|
|
1073
|
+
When the data is small, it is better to handle the data manipulation on the front-end.
|
|
1074
|
+
|
|
1075
|
+
Here is a good starting point for creating a front-end managed table, using `react-table`, ready to copy-paste ! (click on "Show code").
|
|
1076
|
+
|
|
1077
|
+
<DocsStory of={DataTable.WithReactTableFrontEndManaged} />
|
|
1078
|
+
|
|
1079
|
+
### Back-end managed
|
|
1080
|
+
|
|
1081
|
+
Letting the back-end handle the data manipulation requires a bit more work, you will need to handle some parts of the table state manually to properly send the proper parameters to your back-end (like the current page, the sorting, the filtering, etc.).
|
|
1082
|
+
|
|
1083
|
+
Here is a good starting point for creating a back-end managed table, using `react-table`, ready to copy-paste ! (click on "Show code").
|
|
1084
|
+
|
|
1085
|
+
<DocsStory of={DataTable.WithReactTableBackEndManaged} />
|
|
1086
|
+
|
|
1087
|
+
## With sub columns
|
|
1088
|
+
|
|
1089
|
+
Here is an example of how to create a table with sub columns, **click on "Show code"**.
|
|
1090
|
+
|
|
1091
|
+
Find more explanations on this in the [TanStack Table documentation](https://tanstack.com/table/latest/docs/guide/column-defs) (in the "Grouping" section).
|
|
1092
|
+
|
|
1093
|
+
<DocsStory of={DataTable.WithSubColumns} />
|
|
1094
|
+
|
|
1095
|
+
## With custom UI to expand in a row
|
|
1096
|
+
|
|
1097
|
+
Here is an example of how to create a table with a custom UI to expand in a row, **click on "Show code"**.
|
|
1098
|
+
|
|
1099
|
+
Find more explanations on this in the [TanStack Table documentation and examples](https://tanstack.com/table/latest/docs/framework/react/examples/sub-components) ("Sub Components" example, and also the "Expanding guide").
|
|
1100
|
+
|
|
1101
|
+
<DocsStory of={DataTable.CustomExpandUi} />
|
|
1102
|
+
|
|
1103
|
+
## With sub rows
|
|
1104
|
+
|
|
1105
|
+
Here is an example of how to create a table with sub rows, **click on "Show code"**.
|
|
1106
|
+
|
|
1107
|
+
You can have any number of sub row depth, and the table will handle the UI for you (the data schema should be recursive).
|
|
1108
|
+
|
|
1109
|
+
Find more explanations on this in the [TanStack Table documentation and examples](https://tanstack.com/table/v8/docs/framework/react/examples/expanding) ("Expanding" example, and also the "Expanding guide" docs).
|
|
1110
|
+
|
|
1111
|
+
<DocsStory of={DataTable.ExpandingSubRows} />
|
|
1112
|
+
|
|
1113
|
+
## Available components in Cells
|
|
1114
|
+
|
|
1115
|
+
Our approach to this question is to be flexible.
|
|
1116
|
+
|
|
1117
|
+
We do not want to limit you to a set of predefined components to use in your cells, nor do we expose you with tons of props to handle every possible combination of components.
|
|
1118
|
+
|
|
1119
|
+
The only recommandation we have is **to only use one component per cell**, ideally (though it's not forbidden).
|
|
1120
|
+
|
|
1121
|
+
Here are some examples of components you can use in your cells, click on "Show code" to see how it's done.
|
|
1122
|
+
|
|
1123
|
+
<DocsStory of={DataTableRoot.WithSpecificComponents} />
|
|
1124
|
+
```
|