@vendure-io/ui 1.2.0 → 2.0.0-beta.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/README.md +5 -5
- package/package.json +9 -4
- package/src/components/{ui → atoms}/alert-dialog.tsx +4 -4
- package/src/components/{ui → atoms}/badge.tsx +6 -3
- package/src/components/{ui → atoms}/button-group.tsx +1 -2
- package/src/components/{ui → atoms}/button.tsx +8 -2
- package/src/components/{ui → atoms}/calendar.tsx +1 -1
- package/src/components/{ui → atoms}/card.tsx +2 -3
- package/src/components/{ui → atoms}/carousel.tsx +1 -1
- package/src/components/atoms/checkbox.tsx +29 -0
- package/src/components/{ui → atoms}/combobox.tsx +3 -4
- package/src/components/{ui → atoms}/command.tsx +2 -2
- package/src/components/{ui → atoms}/context-menu.tsx +1 -1
- package/src/components/{ui → atoms}/dialog.tsx +4 -4
- package/src/components/{ui → atoms}/drawer.tsx +3 -3
- package/src/components/{ui → atoms}/dropdown-menu.tsx +3 -2
- package/src/components/{ui → atoms}/empty.tsx +1 -1
- package/src/components/{ui → atoms}/field.tsx +2 -2
- package/src/components/{ui → atoms}/hover-card.tsx +1 -1
- package/src/components/{ui → atoms}/input-group.tsx +3 -4
- package/src/components/{ui → atoms}/input-otp.tsx +0 -1
- package/src/components/{ui → atoms}/input.tsx +0 -1
- package/src/components/{ui → atoms}/item.tsx +2 -2
- package/src/components/{ui → atoms}/menubar.tsx +3 -4
- package/src/components/{ui → atoms}/native-select.tsx +0 -1
- package/src/components/{ui → atoms}/navigation-menu.tsx +7 -4
- package/src/components/{ui → atoms}/pagination.tsx +1 -1
- package/src/components/{ui → atoms}/popover.tsx +2 -2
- package/src/components/{ui → atoms}/select.tsx +22 -4
- package/src/components/{ui → atoms}/sheet.tsx +4 -4
- package/src/components/{ui → atoms}/sidebar.tsx +6 -6
- package/src/components/atoms/skeleton.tsx +16 -0
- package/src/components/{ui → atoms}/slider.tsx +0 -1
- package/src/components/{ui → atoms}/sonner.tsx +1 -1
- package/src/components/{ui → atoms}/switch.tsx +0 -1
- package/src/components/{ui → atoms}/tabs.tsx +6 -4
- package/src/components/{ui → atoms}/textarea.tsx +0 -1
- package/src/components/{ui → atoms}/toggle-group.tsx +3 -3
- package/src/components/{ui → atoms}/toggle.tsx +0 -1
- package/src/components/{ui → atoms}/tooltip.tsx +2 -1
- package/src/components/molecules/anonymized-token.tsx +170 -0
- package/src/components/molecules/chip.tsx +85 -0
- package/src/components/molecules/combobox-free-text.tsx +160 -0
- package/src/components/molecules/confirm-dialog.tsx +108 -0
- package/src/components/molecules/copyable-text.tsx +102 -0
- package/src/components/molecules/data-table/data-table-bulk-actions.tsx +69 -0
- package/src/components/molecules/data-table/data-table-column-header.tsx +61 -0
- package/src/components/molecules/data-table/data-table-filters.tsx +198 -0
- package/src/components/molecules/data-table/data-table-helpers.tsx +73 -0
- package/src/components/molecules/data-table/data-table-types.tsx +241 -0
- package/src/components/molecules/data-table/data-table-view-options.tsx +73 -0
- package/src/components/molecules/data-table/data-table.tsx +500 -0
- package/src/components/molecules/data-table/list-header.tsx +53 -0
- package/src/components/molecules/data-table/table-pagination.tsx +263 -0
- package/src/components/molecules/date-time.tsx +73 -0
- package/src/components/molecules/description-list.tsx +120 -0
- package/src/components/molecules/format-provider.tsx +46 -0
- package/src/components/molecules/id-chip.tsx +79 -0
- package/src/components/molecules/illustrations/access-denied.tsx +55 -0
- package/src/components/molecules/illustrations/empty-collection.tsx +66 -0
- package/src/components/molecules/illustrations/empty-database.tsx +44 -0
- package/src/components/molecules/illustrations/empty-media.tsx +68 -0
- package/src/components/molecules/illustrations/error.tsx +64 -0
- package/src/components/molecules/illustrations/first-run.tsx +55 -0
- package/src/components/molecules/illustrations/illustration-types.tsx +9 -0
- package/src/components/molecules/illustrations/no-activity.tsx +49 -0
- package/src/components/molecules/illustrations/no-deployments.tsx +49 -0
- package/src/components/molecules/illustrations/no-documents.tsx +67 -0
- package/src/components/molecules/illustrations/no-keys.tsx +50 -0
- package/src/components/molecules/illustrations/no-logs.tsx +53 -0
- package/src/components/molecules/illustrations/no-members.tsx +56 -0
- package/src/components/molecules/illustrations/no-notifications.tsx +52 -0
- package/src/components/molecules/illustrations/no-orders.tsx +48 -0
- package/src/components/molecules/illustrations/no-plugins.tsx +72 -0
- package/src/components/molecules/illustrations/no-results.tsx +69 -0
- package/src/components/molecules/illustrations/not-found.tsx +48 -0
- package/src/components/molecules/illustrations/offline.tsx +43 -0
- package/src/components/molecules/illustrations/pending-approval.tsx +56 -0
- package/src/components/molecules/illustrations/upload-dropzone.tsx +59 -0
- package/src/components/molecules/money.tsx +91 -0
- package/src/components/{custom → molecules}/multi-select.tsx +2 -2
- package/src/components/molecules/page-header.tsx +80 -0
- package/src/components/molecules/password-input.tsx +47 -0
- package/src/components/molecules/relative-time.tsx +81 -0
- package/src/components/molecules/stat-card.tsx +134 -0
- package/src/components/molecules/state-views/empty-state.tsx +75 -0
- package/src/components/molecules/state-views/error-state.tsx +91 -0
- package/src/components/molecules/state-views/loading-state.tsx +80 -0
- package/src/components/molecules/status-badge.tsx +100 -0
- package/src/hooks/use-copy.ts +51 -0
- package/src/lib/base-ui.ts +2 -1
- package/src/lib/format-relative.ts +41 -0
- package/src/lib/state-dictionary.ts +137 -0
- package/src/components/ui/checkbox.tsx +0 -30
- package/src/components/ui/skeleton.tsx +0 -13
- /package/src/components/{ui → atoms}/accordion.tsx +0 -0
- /package/src/components/{ui → atoms}/alert.tsx +0 -0
- /package/src/components/{ui → atoms}/aspect-ratio.tsx +0 -0
- /package/src/components/{ui → atoms}/avatar.tsx +0 -0
- /package/src/components/{ui → atoms}/breadcrumb.tsx +0 -0
- /package/src/components/{ui → atoms}/chart.tsx +0 -0
- /package/src/components/{ui → atoms}/collapsible.tsx +0 -0
- /package/src/components/{ui → atoms}/direction.tsx +0 -0
- /package/src/components/{ui → atoms}/kbd.tsx +0 -0
- /package/src/components/{ui → atoms}/label.tsx +0 -0
- /package/src/components/{ui → atoms}/progress.tsx +0 -0
- /package/src/components/{ui → atoms}/radio-group.tsx +0 -0
- /package/src/components/{ui → atoms}/resizable.tsx +0 -0
- /package/src/components/{ui → atoms}/scroll-area.tsx +0 -0
- /package/src/components/{ui → atoms}/separator.tsx +0 -0
- /package/src/components/{ui → atoms}/spinner.tsx +0 -0
- /package/src/components/{ui → atoms}/table.tsx +0 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Column,
|
|
5
|
+
type ColumnDef,
|
|
6
|
+
type ColumnFiltersState,
|
|
7
|
+
flexRender,
|
|
8
|
+
functionalUpdate,
|
|
9
|
+
getCoreRowModel,
|
|
10
|
+
getFilteredRowModel,
|
|
11
|
+
getPaginationRowModel,
|
|
12
|
+
getSortedRowModel,
|
|
13
|
+
type OnChangeFn,
|
|
14
|
+
type PaginationState,
|
|
15
|
+
type RowSelectionState,
|
|
16
|
+
type SortingState,
|
|
17
|
+
type TableOptions,
|
|
18
|
+
type TableState,
|
|
19
|
+
useReactTable,
|
|
20
|
+
type VisibilityState,
|
|
21
|
+
} from '@tanstack/react-table';
|
|
22
|
+
import { Checkbox } from '@vendure-io/ui/components/atoms/checkbox';
|
|
23
|
+
import {
|
|
24
|
+
ContextMenu,
|
|
25
|
+
ContextMenuContent,
|
|
26
|
+
ContextMenuTrigger,
|
|
27
|
+
} from '@vendure-io/ui/components/atoms/context-menu';
|
|
28
|
+
import { Skeleton } from '@vendure-io/ui/components/atoms/skeleton';
|
|
29
|
+
import {
|
|
30
|
+
Table,
|
|
31
|
+
TableBody,
|
|
32
|
+
TableCell,
|
|
33
|
+
TableHead,
|
|
34
|
+
TableHeader,
|
|
35
|
+
TableRow,
|
|
36
|
+
} from '@vendure-io/ui/components/atoms/table';
|
|
37
|
+
import {
|
|
38
|
+
DataTableBulkActions,
|
|
39
|
+
useSelectionCache,
|
|
40
|
+
} from '@vendure-io/ui/components/molecules/data-table/data-table-bulk-actions';
|
|
41
|
+
import { DataTableColumnHeader } from '@vendure-io/ui/components/molecules/data-table/data-table-column-header';
|
|
42
|
+
import {
|
|
43
|
+
DataTableAddFilter,
|
|
44
|
+
DataTableAppliedFilters,
|
|
45
|
+
} from '@vendure-io/ui/components/molecules/data-table/data-table-filters';
|
|
46
|
+
import {
|
|
47
|
+
buildDisplayColumns,
|
|
48
|
+
resolveSlot,
|
|
49
|
+
} from '@vendure-io/ui/components/molecules/data-table/data-table-helpers';
|
|
50
|
+
import type {
|
|
51
|
+
ControlledState,
|
|
52
|
+
DataTableProps,
|
|
53
|
+
} from '@vendure-io/ui/components/molecules/data-table/data-table-types';
|
|
54
|
+
import { DataTableViewOptions } from '@vendure-io/ui/components/molecules/data-table/data-table-view-options';
|
|
55
|
+
import {
|
|
56
|
+
ListHeader,
|
|
57
|
+
ListHeaderChips,
|
|
58
|
+
ListHeaderControls,
|
|
59
|
+
} from '@vendure-io/ui/components/molecules/data-table/list-header';
|
|
60
|
+
import { TablePagination } from '@vendure-io/ui/components/molecules/data-table/table-pagination';
|
|
61
|
+
import { cn } from '@vendure-io/ui/lib/utils';
|
|
62
|
+
import * as React from 'react';
|
|
63
|
+
|
|
64
|
+
// The composition root. Owns the single `useReactTable` instance and the
|
|
65
|
+
// controlled/uncontrolled bridge, then lays out header → controls → chips →
|
|
66
|
+
// bulk overlay → Table → footer. Every capability follows the Phase-1 rule
|
|
67
|
+
// verbatim: a feature renders only if its config is wired — no feature flags, no
|
|
68
|
+
// disabled placeholders. Only this folder imports TanStack; the composed
|
|
69
|
+
// ListHeader/TablePagination/Chip primitives stay TanStack-free.
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Bridge a capability config to a `[value, onChange]` pair TanStack can consume
|
|
73
|
+
* as fully controlled. Controlled configs (`value`) are the source of truth every
|
|
74
|
+
* render; uncontrolled configs (`defaultValue` or nothing) get internal state the
|
|
75
|
+
* core owns, with any `onChange` observing. This fixes the donor's seed-then-emit
|
|
76
|
+
* bug: the prop, not a one-time seed, drives every render.
|
|
77
|
+
*/
|
|
78
|
+
function useControlledTableState<T>(
|
|
79
|
+
config: ControlledState<T> | undefined,
|
|
80
|
+
fallback: T,
|
|
81
|
+
): readonly [T, OnChangeFn<T>] {
|
|
82
|
+
const controlled = config?.value !== undefined;
|
|
83
|
+
const [internal, setInternal] = React.useState<T>(config?.defaultValue ?? fallback);
|
|
84
|
+
const value = controlled ? (config?.value as T) : internal;
|
|
85
|
+
const onChange = React.useCallback<OnChangeFn<T>>(
|
|
86
|
+
(updater) => {
|
|
87
|
+
if (!controlled) setInternal((old) => functionalUpdate(updater, old));
|
|
88
|
+
config?.onChange?.(updater);
|
|
89
|
+
},
|
|
90
|
+
[controlled, config],
|
|
91
|
+
);
|
|
92
|
+
return [value, onChange] as const;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A string label for a column, from a string `header`, else the column id. */
|
|
96
|
+
function headerLabelText<TData>(column: Column<TData, unknown>): string {
|
|
97
|
+
const header = column.columnDef.header;
|
|
98
|
+
return typeof header === 'string' ? header : column.id;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ariaSort<TData>(
|
|
102
|
+
hasSorting: boolean,
|
|
103
|
+
column: Column<TData, unknown>,
|
|
104
|
+
): 'ascending' | 'descending' | 'none' | undefined {
|
|
105
|
+
if (!hasSorting || !column.getCanSort()) return undefined;
|
|
106
|
+
const sorted = column.getIsSorted();
|
|
107
|
+
return sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : 'none';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function DataTable<TData>({
|
|
111
|
+
rows,
|
|
112
|
+
columns,
|
|
113
|
+
getRowId,
|
|
114
|
+
isLoading,
|
|
115
|
+
skeletonRowCount = 10,
|
|
116
|
+
pagination,
|
|
117
|
+
sorting,
|
|
118
|
+
filters,
|
|
119
|
+
columnVisibility,
|
|
120
|
+
rowSelection,
|
|
121
|
+
header,
|
|
122
|
+
toolbar,
|
|
123
|
+
bulkActions,
|
|
124
|
+
rowActions,
|
|
125
|
+
contextActions,
|
|
126
|
+
emptyState,
|
|
127
|
+
renderRow,
|
|
128
|
+
setTableOptions,
|
|
129
|
+
labels,
|
|
130
|
+
className,
|
|
131
|
+
}: DataTableProps<TData>) {
|
|
132
|
+
const warnedMissingPaginatedSelectionRowId = React.useRef(false);
|
|
133
|
+
if (
|
|
134
|
+
process.env.NODE_ENV !== 'production' &&
|
|
135
|
+
rowSelection != null &&
|
|
136
|
+
pagination != null &&
|
|
137
|
+
pagination.mode !== 'client' &&
|
|
138
|
+
getRowId == null &&
|
|
139
|
+
!warnedMissingPaginatedSelectionRowId.current
|
|
140
|
+
) {
|
|
141
|
+
warnedMissingPaginatedSelectionRowId.current = true;
|
|
142
|
+
console.warn(
|
|
143
|
+
'[DataTable] rowSelection with server pagination requires getRowId. ' +
|
|
144
|
+
"TanStack's default page-index row ids can resolve bulkActions selection to the wrong row after changing pages.",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Resolve labels once per `labels` identity so the derived closures are stable
|
|
149
|
+
// deps for the display-column memo below.
|
|
150
|
+
const l = React.useMemo(
|
|
151
|
+
() => ({
|
|
152
|
+
empty: labels?.empty ?? 'No results',
|
|
153
|
+
selectAllRows: labels?.selectAllRows ?? 'Select all rows',
|
|
154
|
+
selectRow: labels?.selectRow ?? ((index: number) => `Select row ${index + 1}`),
|
|
155
|
+
rowActions: labels?.rowActions ?? 'Open row actions',
|
|
156
|
+
columnsTrigger: labels?.columnsTrigger ?? 'Columns',
|
|
157
|
+
columnsHeading: labels?.columnsHeading ?? 'Toggle columns',
|
|
158
|
+
addFilter: labels?.addFilter ?? 'Add filter',
|
|
159
|
+
removeFilter:
|
|
160
|
+
labels?.removeFilter ?? ((columnLabel: string) => `Remove ${columnLabel} filter`),
|
|
161
|
+
sortLabel: labels?.sortLabel ?? ((columnLabel: string) => `Sort by ${columnLabel}`),
|
|
162
|
+
filtersCollapsed: labels?.filtersCollapsed ?? ((count: number) => `${count} filters`),
|
|
163
|
+
pagination: labels?.pagination,
|
|
164
|
+
}),
|
|
165
|
+
[labels],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const [sortingState, onSortingChange] = useControlledTableState<SortingState>(sorting, []);
|
|
169
|
+
const [filtersState, onColumnFiltersChange] = useControlledTableState<ColumnFiltersState>(
|
|
170
|
+
filters,
|
|
171
|
+
[],
|
|
172
|
+
);
|
|
173
|
+
const [visibilityState, onColumnVisibilityChange] = useControlledTableState<VisibilityState>(
|
|
174
|
+
columnVisibility,
|
|
175
|
+
{},
|
|
176
|
+
);
|
|
177
|
+
const [selectionState, onRowSelectionChange] = useControlledTableState<RowSelectionState>(
|
|
178
|
+
rowSelection,
|
|
179
|
+
{},
|
|
180
|
+
);
|
|
181
|
+
const selectColumnId = rowSelection?.selectColumnId ?? 'select';
|
|
182
|
+
|
|
183
|
+
// Inject the select/actions display columns around the data columns. `meta` on
|
|
184
|
+
// the consumer's columns is never touched — it's the dashboard's field-info seam.
|
|
185
|
+
const displayColumns = React.useMemo(() => {
|
|
186
|
+
// biome-ignore lint/suspicious/noExplicitAny: matches TanStack's ColumnDef value-type convention
|
|
187
|
+
const selectColumn: ColumnDef<TData, any> | undefined = rowSelection
|
|
188
|
+
? {
|
|
189
|
+
id: selectColumnId,
|
|
190
|
+
enableSorting: false,
|
|
191
|
+
enableHiding: false,
|
|
192
|
+
// The checkboxes stay hidden until they're relevant: a header/row
|
|
193
|
+
// checkbox reveals on hover of its row (the enclosing `group/*`), on
|
|
194
|
+
// keyboard focus, or once it carries a checked/indeterminate state —
|
|
195
|
+
// so an idle table reads as content, not a selection grid. They're
|
|
196
|
+
// floated absolutely over the (zero-width) select cell so an empty
|
|
197
|
+
// selection reserves no gutter — no left gap to read as a bug.
|
|
198
|
+
header: ({ table }) => (
|
|
199
|
+
<Checkbox
|
|
200
|
+
checked={table.getIsAllPageRowsSelected()}
|
|
201
|
+
indeterminate={table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()}
|
|
202
|
+
onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}
|
|
203
|
+
aria-label={l.selectAllRows}
|
|
204
|
+
className="absolute top-1/2 left-2 z-10 -translate-y-1/2 opacity-0 transition-opacity group-hover/header-row:opacity-100 focus-visible:opacity-100 data-checked:opacity-100 data-indeterminate:opacity-100"
|
|
205
|
+
/>
|
|
206
|
+
),
|
|
207
|
+
cell: ({ row }) => (
|
|
208
|
+
<Checkbox
|
|
209
|
+
checked={row.getIsSelected()}
|
|
210
|
+
disabled={!row.getCanSelect()}
|
|
211
|
+
onCheckedChange={(checked) => row.toggleSelected(checked)}
|
|
212
|
+
aria-label={l.selectRow(row.index)}
|
|
213
|
+
className="absolute top-1/2 left-2 z-10 -translate-y-1/2 opacity-0 transition-opacity group-hover/row:opacity-100 focus-visible:opacity-100 data-checked:opacity-100"
|
|
214
|
+
/>
|
|
215
|
+
),
|
|
216
|
+
}
|
|
217
|
+
: undefined;
|
|
218
|
+
|
|
219
|
+
// biome-ignore lint/suspicious/noExplicitAny: matches TanStack's ColumnDef value-type convention
|
|
220
|
+
const actionsColumn: ColumnDef<TData, any> | undefined = rowActions
|
|
221
|
+
? {
|
|
222
|
+
id: 'actions',
|
|
223
|
+
enableSorting: false,
|
|
224
|
+
enableHiding: false,
|
|
225
|
+
header: () => <span className="sr-only">{l.rowActions}</span>,
|
|
226
|
+
cell: ({ row, table }) => rowActions(row.original, { row, table }),
|
|
227
|
+
}
|
|
228
|
+
: undefined;
|
|
229
|
+
|
|
230
|
+
return buildDisplayColumns(columns, { select: selectColumn, actions: actionsColumn });
|
|
231
|
+
}, [
|
|
232
|
+
columns,
|
|
233
|
+
rowSelection,
|
|
234
|
+
rowActions,
|
|
235
|
+
selectColumnId,
|
|
236
|
+
l.selectAllRows,
|
|
237
|
+
l.selectRow,
|
|
238
|
+
l.rowActions,
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
const state: Partial<TableState> = {};
|
|
242
|
+
if (sorting) state.sorting = sortingState;
|
|
243
|
+
if (filters) state.columnFilters = filtersState;
|
|
244
|
+
if (columnVisibility) state.columnVisibility = visibilityState;
|
|
245
|
+
if (rowSelection) state.rowSelection = selectionState;
|
|
246
|
+
if (pagination) {
|
|
247
|
+
state.pagination = { pageIndex: pagination.page - 1, pageSize: pagination.pageSize };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const onPaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
|
251
|
+
if (!pagination) return;
|
|
252
|
+
const current: PaginationState = {
|
|
253
|
+
pageIndex: pagination.page - 1,
|
|
254
|
+
pageSize: pagination.pageSize,
|
|
255
|
+
};
|
|
256
|
+
const next = functionalUpdate(updater, current);
|
|
257
|
+
if (next.pageSize !== current.pageSize) pagination.onPageSizeChange?.(next.pageSize);
|
|
258
|
+
if (next.pageIndex !== current.pageIndex) pagination.onPageChange(next.pageIndex + 1);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const baseOptions: TableOptions<TData> = {
|
|
262
|
+
data: rows,
|
|
263
|
+
columns: displayColumns,
|
|
264
|
+
state,
|
|
265
|
+
getRowId,
|
|
266
|
+
getCoreRowModel: getCoreRowModel(),
|
|
267
|
+
enableSorting: sorting != null,
|
|
268
|
+
enableFilters: filters != null,
|
|
269
|
+
// Server mode (default) emits only; client mode enables the matching engine.
|
|
270
|
+
manualSorting: sorting ? sorting.mode !== 'client' : undefined,
|
|
271
|
+
manualFiltering: filters ? filters.mode !== 'client' : undefined,
|
|
272
|
+
manualPagination: pagination ? pagination.mode !== 'client' : undefined,
|
|
273
|
+
rowCount: pagination && pagination.mode !== 'client' ? pagination.totalItems : undefined,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (sorting) {
|
|
277
|
+
baseOptions.onSortingChange = onSortingChange;
|
|
278
|
+
if (sorting.mode === 'client') baseOptions.getSortedRowModel = getSortedRowModel();
|
|
279
|
+
}
|
|
280
|
+
if (filters) {
|
|
281
|
+
baseOptions.onColumnFiltersChange = onColumnFiltersChange;
|
|
282
|
+
if (filters.mode === 'client') baseOptions.getFilteredRowModel = getFilteredRowModel();
|
|
283
|
+
}
|
|
284
|
+
if (columnVisibility) baseOptions.onColumnVisibilityChange = onColumnVisibilityChange;
|
|
285
|
+
if (rowSelection) {
|
|
286
|
+
baseOptions.onRowSelectionChange = onRowSelectionChange;
|
|
287
|
+
baseOptions.enableRowSelection = rowSelection.enableRowSelection ?? true;
|
|
288
|
+
}
|
|
289
|
+
if (pagination) {
|
|
290
|
+
baseOptions.onPaginationChange = onPaginationChange;
|
|
291
|
+
if (pagination.mode === 'client') baseOptions.getPaginationRowModel = getPaginationRowModel();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const table = useReactTable(setTableOptions ? setTableOptions(baseOptions) : baseOptions);
|
|
295
|
+
|
|
296
|
+
const selectionCache = useSelectionCache(table);
|
|
297
|
+
|
|
298
|
+
const bodyRows = table.getRowModel().rows;
|
|
299
|
+
const columnCount = table.getVisibleLeafColumns().length;
|
|
300
|
+
|
|
301
|
+
// The select checkbox is floated absolutely over a zero-width cell, so the
|
|
302
|
+
// gutter it needs is folded into the leading data column as left padding —
|
|
303
|
+
// an idle table reads as an intentional indent, not an empty checkbox cell,
|
|
304
|
+
// and the revealed checkbox never overlaps the first column's content.
|
|
305
|
+
const leadingColumnId =
|
|
306
|
+
rowSelection != null
|
|
307
|
+
? table.getVisibleLeafColumns().find((column) => column.id !== selectColumnId)?.id
|
|
308
|
+
: undefined;
|
|
309
|
+
const columnClass = (id: string): string | undefined => {
|
|
310
|
+
if (id === selectColumnId) return 'relative w-0 p-0';
|
|
311
|
+
if (id === leadingColumnId) return 'pl-8';
|
|
312
|
+
// Actions cells drop vertical padding so a row-action button (taller than a
|
|
313
|
+
// text line) sits inside the natural row height rather than inflating every
|
|
314
|
+
// row; `align-middle` on the cell keeps it centred.
|
|
315
|
+
if (id === 'actions') return 'py-0';
|
|
316
|
+
return undefined;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const toolbarNode = toolbar != null ? resolveSlot(toolbar, table) : undefined;
|
|
320
|
+
const showViewOptions = columnVisibility != null && columnVisibility.showViewOptions !== false;
|
|
321
|
+
const hasFilterMenu = Boolean(
|
|
322
|
+
filters?.showAddFilter !== false && filters?.columns && filters.columns.length > 0,
|
|
323
|
+
);
|
|
324
|
+
const showControls = toolbar != null || showViewOptions || hasFilterMenu;
|
|
325
|
+
const showChips =
|
|
326
|
+
filters != null &&
|
|
327
|
+
filters.showAppliedFilters !== false &&
|
|
328
|
+
table.getState().columnFilters.length > 0;
|
|
329
|
+
const showHeader = header != null || showControls || showChips;
|
|
330
|
+
const showBulk = rowSelection != null && bulkActions != null;
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<div data-slot="data-table" className={cn('flex flex-col gap-4', className)}>
|
|
334
|
+
{showHeader && (
|
|
335
|
+
<ListHeader>
|
|
336
|
+
{header}
|
|
337
|
+
{showControls && (
|
|
338
|
+
<ListHeaderControls>
|
|
339
|
+
{toolbarNode}
|
|
340
|
+
{hasFilterMenu && filters?.columns && (
|
|
341
|
+
<DataTableAddFilter table={table} columns={filters.columns} label={l.addFilter} />
|
|
342
|
+
)}
|
|
343
|
+
{showViewOptions && (
|
|
344
|
+
<DataTableViewOptions
|
|
345
|
+
table={table}
|
|
346
|
+
triggerLabel={l.columnsTrigger}
|
|
347
|
+
heading={l.columnsHeading}
|
|
348
|
+
/>
|
|
349
|
+
)}
|
|
350
|
+
</ListHeaderControls>
|
|
351
|
+
)}
|
|
352
|
+
{showChips && (
|
|
353
|
+
<ListHeaderChips>
|
|
354
|
+
<DataTableAppliedFilters
|
|
355
|
+
table={table}
|
|
356
|
+
columns={filters?.columns}
|
|
357
|
+
inlineChipLimit={filters?.inlineChipLimit}
|
|
358
|
+
removeLabel={l.removeFilter}
|
|
359
|
+
collapsedLabel={l.filtersCollapsed}
|
|
360
|
+
/>
|
|
361
|
+
</ListHeaderChips>
|
|
362
|
+
)}
|
|
363
|
+
</ListHeader>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{showBulk && bulkActions && (
|
|
367
|
+
<DataTableBulkActions table={table} cache={selectionCache} render={bulkActions} />
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
<Table>
|
|
371
|
+
<TableHeader>
|
|
372
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
373
|
+
<TableRow key={headerGroup.id} className="group/header-row">
|
|
374
|
+
{headerGroup.headers.map((headerCell) => {
|
|
375
|
+
const content = headerCell.isPlaceholder
|
|
376
|
+
? null
|
|
377
|
+
: flexRender(headerCell.column.columnDef.header, headerCell.getContext());
|
|
378
|
+
return (
|
|
379
|
+
<TableHead
|
|
380
|
+
key={headerCell.id}
|
|
381
|
+
aria-sort={ariaSort(sorting != null, headerCell.column)}
|
|
382
|
+
className={columnClass(headerCell.column.id)}
|
|
383
|
+
>
|
|
384
|
+
{sorting != null && !headerCell.isPlaceholder ? (
|
|
385
|
+
<DataTableColumnHeader
|
|
386
|
+
column={headerCell.column}
|
|
387
|
+
sortLabel={l.sortLabel(headerLabelText(headerCell.column))}
|
|
388
|
+
>
|
|
389
|
+
{content}
|
|
390
|
+
</DataTableColumnHeader>
|
|
391
|
+
) : (
|
|
392
|
+
content
|
|
393
|
+
)}
|
|
394
|
+
</TableHead>
|
|
395
|
+
);
|
|
396
|
+
})}
|
|
397
|
+
</TableRow>
|
|
398
|
+
))}
|
|
399
|
+
</TableHeader>
|
|
400
|
+
<TableBody>
|
|
401
|
+
{isLoading && rows.length === 0 ? (
|
|
402
|
+
Array.from({ length: skeletonRowCount }).map((_, rowIndex) => (
|
|
403
|
+
<TableRow key={`skeleton-${rowIndex}`}>
|
|
404
|
+
{Array.from({ length: Math.max(columnCount, 1) }).map((__, cellIndex) => (
|
|
405
|
+
<TableCell key={`skeleton-cell-${cellIndex}`}>
|
|
406
|
+
<Skeleton className="h-4 w-full" />
|
|
407
|
+
</TableCell>
|
|
408
|
+
))}
|
|
409
|
+
</TableRow>
|
|
410
|
+
))
|
|
411
|
+
) : bodyRows.length === 0 ? (
|
|
412
|
+
<TableRow className="hover:bg-transparent">
|
|
413
|
+
<TableCell
|
|
414
|
+
colSpan={Math.max(columnCount, 1)}
|
|
415
|
+
className="text-muted-foreground h-24 text-center"
|
|
416
|
+
>
|
|
417
|
+
{emptyState ?? l.empty}
|
|
418
|
+
</TableCell>
|
|
419
|
+
</TableRow>
|
|
420
|
+
) : (
|
|
421
|
+
bodyRows.map((row) => {
|
|
422
|
+
const rowNode = (
|
|
423
|
+
<TableRow
|
|
424
|
+
className="group/row"
|
|
425
|
+
data-state={row.getIsSelected() ? 'selected' : undefined}
|
|
426
|
+
>
|
|
427
|
+
{row.getVisibleCells().map((cell) => (
|
|
428
|
+
<TableCell key={cell.id} className={columnClass(cell.column.id)}>
|
|
429
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
430
|
+
</TableCell>
|
|
431
|
+
))}
|
|
432
|
+
</TableRow>
|
|
433
|
+
);
|
|
434
|
+
// Right-click accelerator: the row itself is the context-menu
|
|
435
|
+
// trigger, so the consumer supplies only the items and the core
|
|
436
|
+
// owns the menu chrome.
|
|
437
|
+
const defaultRow = contextActions ? (
|
|
438
|
+
<ContextMenu>
|
|
439
|
+
<ContextMenuTrigger render={rowNode} />
|
|
440
|
+
<ContextMenuContent>
|
|
441
|
+
{contextActions(row.original, { row, table })}
|
|
442
|
+
</ContextMenuContent>
|
|
443
|
+
</ContextMenu>
|
|
444
|
+
) : (
|
|
445
|
+
rowNode
|
|
446
|
+
);
|
|
447
|
+
// Row-render seam: consumers can swap the default row for a
|
|
448
|
+
// full-width utility row or a per-row wrapper the cell grid can't
|
|
449
|
+
// express. Returning `defaultRow` keeps the built-in rendering.
|
|
450
|
+
return (
|
|
451
|
+
<React.Fragment key={row.id}>
|
|
452
|
+
{renderRow ? renderRow(row, { table, columnCount, defaultRow }) : defaultRow}
|
|
453
|
+
</React.Fragment>
|
|
454
|
+
);
|
|
455
|
+
})
|
|
456
|
+
)}
|
|
457
|
+
</TableBody>
|
|
458
|
+
</Table>
|
|
459
|
+
|
|
460
|
+
{pagination && (
|
|
461
|
+
<TablePagination
|
|
462
|
+
page={pagination.page}
|
|
463
|
+
pageSize={pagination.pageSize}
|
|
464
|
+
totalItems={pagination.totalItems ?? table.getPrePaginationRowModel().rows.length}
|
|
465
|
+
onPageChange={pagination.onPageChange}
|
|
466
|
+
onPageSizeChange={pagination.onPageSizeChange}
|
|
467
|
+
pageSizeOptions={pagination.pageSizeOptions}
|
|
468
|
+
getPageHref={pagination.getPageHref}
|
|
469
|
+
formatRange={pagination.formatRange}
|
|
470
|
+
navLabel={l.pagination?.navLabel}
|
|
471
|
+
previousLabel={l.pagination?.previousLabel}
|
|
472
|
+
nextLabel={l.pagination?.nextLabel}
|
|
473
|
+
pageSizeLabel={l.pagination?.pageSizeLabel}
|
|
474
|
+
/>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export { DataTable };
|
|
481
|
+
|
|
482
|
+
// Re-export the consumer-facing TanStack types so columns can be authored from
|
|
483
|
+
// the same import as `DataTable` — no direct `@tanstack/react-table` dependency
|
|
484
|
+
// in consumer code just to type a `ColumnDef`.
|
|
485
|
+
export type {
|
|
486
|
+
CellContext,
|
|
487
|
+
ColumnDef,
|
|
488
|
+
ColumnFiltersState,
|
|
489
|
+
HeaderContext,
|
|
490
|
+
OnChangeFn,
|
|
491
|
+
Row,
|
|
492
|
+
RowSelectionState,
|
|
493
|
+
SortingState,
|
|
494
|
+
Table,
|
|
495
|
+
TableOptions,
|
|
496
|
+
VisibilityState,
|
|
497
|
+
} from '@tanstack/react-table';
|
|
498
|
+
// Re-export the full DataTable type surface so a consumer imports everything
|
|
499
|
+
// (props, capability configs, contexts, labels) from this one module.
|
|
500
|
+
export type * from '@vendure-io/ui/components/molecules/data-table/data-table-types';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cn } from '@vendure-io/ui/lib/utils';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
|
|
4
|
+
// The three-zone list-page header: title row / control row / chip row. Ships
|
|
5
|
+
// layout, not logic — TanStack-free, fetch-agnostic, router-agnostic.
|
|
6
|
+
//
|
|
7
|
+
// By decision, the title row is NOT re-implemented here: it complements
|
|
8
|
+
// `PageHeader` rather than competing with it, so consumers compose the existing
|
|
9
|
+
// `PageHeader` compound as the first child. The chip row is rendered by the
|
|
10
|
+
// consumer only when filters are active — ListHeaderChips does no
|
|
11
|
+
// `React.Children` introspection and holds no active-filter logic.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Root of the list-page header: a vertical stack of zones. Render free children
|
|
15
|
+
* top to bottom — typically `PageHeader`, then `ListHeaderControls`, then
|
|
16
|
+
* (only when filters are active) `ListHeaderChips`.
|
|
17
|
+
*/
|
|
18
|
+
function ListHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
19
|
+
return (
|
|
20
|
+
<div data-slot="list-header" className={cn('flex flex-col gap-4', className)} {...props} />
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The control row: a horizontal flex row for search input, filter triggers, and
|
|
26
|
+
* view options. Layout only — children are free.
|
|
27
|
+
*/
|
|
28
|
+
function ListHeaderControls({ className, ...props }: React.ComponentProps<'div'>) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
data-slot="list-header-controls"
|
|
32
|
+
className={cn('flex flex-wrap items-center gap-2', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The chip row: a horizontal wrapping flex row of applied-filter `Chip`s. Dumb
|
|
40
|
+
* by design — render it only when filters are active; it does not inspect its
|
|
41
|
+
* children or decide when to appear.
|
|
42
|
+
*/
|
|
43
|
+
function ListHeaderChips({ className, ...props }: React.ComponentProps<'div'>) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
data-slot="list-header-chips"
|
|
47
|
+
className={cn('flex flex-wrap items-center gap-2', className)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { ListHeader, ListHeaderControls, ListHeaderChips };
|