@vendure/dashboard 3.3.5-master-202506250724 → 3.3.5-master-202506251305

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.
Files changed (61) hide show
  1. package/dist/plugin/tests/barrel-exports.spec.js +1 -1
  2. package/dist/plugin/vite-plugin-config.js +1 -0
  3. package/dist/plugin/vite-plugin-dashboard-metadata.d.ts +1 -3
  4. package/dist/plugin/vite-plugin-dashboard-metadata.js +1 -8
  5. package/dist/plugin/vite-plugin-tailwind-source.d.ts +7 -0
  6. package/dist/plugin/vite-plugin-tailwind-source.js +49 -0
  7. package/dist/plugin/vite-plugin-vendure-dashboard.js +3 -1
  8. package/package.json +4 -4
  9. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +1 -1
  10. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +43 -34
  11. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +1 -1
  12. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
  13. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +1 -1
  14. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +1 -1
  15. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +1 -1
  16. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +1 -1
  17. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
  18. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +2 -5
  19. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -1
  20. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +1 -1
  21. package/src/app/routes/_authenticated/_products/components/assign-facet-values-dialog.tsx +98 -0
  22. package/src/app/routes/_authenticated/_products/components/assign-to-channel-dialog.tsx +126 -0
  23. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +268 -0
  24. package/src/app/routes/_authenticated/_products/products.graphql.ts +64 -0
  25. package/src/app/routes/_authenticated/_products/products.tsx +31 -2
  26. package/src/app/routes/_authenticated/_products/products_.$id.tsx +14 -9
  27. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -1
  28. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +1 -1
  29. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -1
  30. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +1 -1
  31. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +1 -1
  32. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +1 -1
  33. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -1
  34. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +1 -1
  35. package/src/app/styles.css +3 -0
  36. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +101 -0
  37. package/src/lib/components/data-table/data-table-bulk-actions.tsx +89 -0
  38. package/src/lib/components/data-table/data-table-filter-badge.tsx +16 -8
  39. package/src/lib/components/data-table/data-table-filter-dialog.tsx +4 -4
  40. package/src/lib/components/data-table/data-table-pagination.tsx +2 -2
  41. package/src/lib/components/data-table/data-table.tsx +50 -31
  42. package/src/lib/components/data-table/human-readable-operator.tsx +3 -3
  43. package/src/lib/components/shared/assigned-facet-values.tsx +1 -5
  44. package/src/lib/components/shared/paginated-list-data-table.tsx +47 -11
  45. package/src/lib/framework/data-table/data-table-extensions.ts +21 -0
  46. package/src/lib/framework/data-table/data-table-types.ts +25 -0
  47. package/src/lib/framework/extension-api/define-dashboard-extension.ts +11 -0
  48. package/src/lib/framework/extension-api/extension-api-types.ts +35 -0
  49. package/src/lib/framework/form-engine/use-generated-form.tsx +2 -5
  50. package/src/lib/framework/layout-engine/page-block-provider.tsx +6 -0
  51. package/src/lib/framework/layout-engine/page-layout.tsx +43 -33
  52. package/src/lib/framework/page/list-page.tsx +6 -8
  53. package/src/lib/framework/registry/registry-types.ts +4 -2
  54. package/src/lib/hooks/use-page-block.tsx +10 -0
  55. package/src/lib/index.ts +8 -1
  56. package/vite/tests/barrel-exports.spec.ts +13 -9
  57. package/vite/vite-plugin-config.ts +1 -0
  58. package/vite/vite-plugin-dashboard-metadata.ts +1 -9
  59. package/vite/vite-plugin-tailwind-source.ts +65 -0
  60. package/vite/vite-plugin-vendure-dashboard.ts +5 -3
  61. /package/src/lib/components/data-table/{data-table-types.ts → types.ts} +0 -0
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button.js';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '@/components/ui/dropdown-menu.js';
10
+ import { getBulkActions } from '@/framework/data-table/data-table-extensions.js';
11
+ import { BulkAction } from '@/framework/data-table/data-table-types.js';
12
+ import { usePageBlock } from '@/hooks/use-page-block.js';
13
+ import { usePage } from '@/hooks/use-page.js';
14
+ import { Trans } from '@/lib/trans.js';
15
+ import { Table } from '@tanstack/react-table';
16
+ import { ChevronDown } from 'lucide-react';
17
+ import { useRef } from 'react';
18
+
19
+ interface DataTableBulkActionsProps<TData> {
20
+ table: Table<TData>;
21
+ bulkActions: BulkAction[];
22
+ }
23
+
24
+ export function DataTableBulkActions<TData>({ table, bulkActions }: DataTableBulkActionsProps<TData>) {
25
+ const { pageId } = usePage();
26
+ const { blockId } = usePageBlock();
27
+
28
+ // Cache to store selected items across page changes
29
+ const selectedItemsCache = useRef<Map<string, TData>>(new Map());
30
+ const selectedRowIds = Object.keys(table.getState().rowSelection);
31
+
32
+ // Get selection from cache instead of trying to get from table
33
+ const selection = selectedRowIds
34
+ .map(key => {
35
+ try {
36
+ const row = table.getRow(key);
37
+ if (row) {
38
+ selectedItemsCache.current.set(key, row.original);
39
+ return row.original;
40
+ }
41
+ } catch (error) {
42
+ // ignore the error, it just means the row is not on the
43
+ // current page.
44
+ }
45
+ if (selectedItemsCache.current.has(key)) {
46
+ return selectedItemsCache.current.get(key);
47
+ }
48
+ return undefined;
49
+ })
50
+ .filter((item): item is TData => item !== undefined);
51
+
52
+ if (selection.length === 0) {
53
+ return null;
54
+ }
55
+ const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
56
+ const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
57
+ allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
58
+
59
+ return (
60
+ <div className="flex items-center gap-2 px-2 py-1 bg-muted/50 rounded-md border">
61
+ <span className="text-sm text-muted-foreground">
62
+ <Trans>{selection.length} selected</Trans>
63
+ </span>
64
+ <DropdownMenu>
65
+ <DropdownMenuTrigger asChild>
66
+ <Button variant="outline" size="sm" className="h-8">
67
+ <Trans>With selected...</Trans>
68
+ <ChevronDown className="ml-2 h-4 w-4" />
69
+ </Button>
70
+ </DropdownMenuTrigger>
71
+ <DropdownMenuContent align="start">
72
+ {allBulkActions.length > 0 ? (
73
+ allBulkActions.map((action, index) => (
74
+ <action.component
75
+ key={`bulk-action-${index}`}
76
+ selection={selection}
77
+ table={table}
78
+ />
79
+ ))
80
+ ) : (
81
+ <DropdownMenuItem className="text-muted-foreground" disabled>
82
+ <Trans>No actions available</Trans>
83
+ </DropdownMenuItem>
84
+ )}
85
+ </DropdownMenuContent>
86
+ </DropdownMenu>
87
+ </div>
88
+ );
89
+ }
@@ -1,10 +1,8 @@
1
- import { Filter } from 'lucide-react';
2
-
3
- import { CircleX } from 'lucide-react';
4
- import { Badge } from '../ui/badge.js';
5
1
  import { useLocalFormat } from '@/hooks/use-local-format.js';
6
- import { ColumnDataType } from './data-table-types.js';
7
- import { HumanReadableOperator } from './human-readable-operator.js';
2
+ import { CircleX, Filter } from 'lucide-react';
3
+ import { Badge } from '../ui/badge.js';
4
+ import { HumanReadableOperator, Operator } from './human-readable-operator.js';
5
+ import { ColumnDataType } from './types.js';
8
6
 
9
7
  export function DataTableFilterBadge({
10
8
  filter,
@@ -22,7 +20,9 @@ export function DataTableFilterBadge({
22
20
  <Badge key={filter.id} className="flex gap-1 items-center" variant="secondary">
23
21
  <Filter size="12" className="opacity-50" />
24
22
  <div>{filter.id}</div>
25
- <div className="text-muted-foreground"><HumanReadableOperator operator={operator} mode="short" /></div>
23
+ <div className="text-muted-foreground">
24
+ <HumanReadableOperator operator={operator as Operator} mode="short" />
25
+ </div>
26
26
  <FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
27
27
  <button className="cursor-pointer" onClick={() => onRemove(filter)}>
28
28
  <CircleX size="14" />
@@ -31,7 +31,15 @@ export function DataTableFilterBadge({
31
31
  );
32
32
  }
33
33
 
34
- function FilterValue({ value, dataType, currencyCode }: { value: unknown, dataType: ColumnDataType, currencyCode: string }) {
34
+ function FilterValue({
35
+ value,
36
+ dataType,
37
+ currencyCode,
38
+ }: {
39
+ value: unknown;
40
+ dataType: ColumnDataType;
41
+ currencyCode: string;
42
+ }) {
35
43
  const { formatDate, formatCurrency } = useLocalFormat();
36
44
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
37
45
  return Object.entries(value as Record<string, unknown>).map(([key, value]) => (
@@ -15,7 +15,7 @@ import { DataTableDateTimeFilter } from './filters/data-table-datetime-filter.js
15
15
  import { DataTableIdFilter } from './filters/data-table-id-filter.js';
16
16
  import { DataTableNumberFilter } from './filters/data-table-number-filter.js';
17
17
  import { DataTableStringFilter } from './filters/data-table-string-filter.js';
18
- import { ColumnDataType } from './data-table-types.js';
18
+ import { ColumnDataType } from './types.js';
19
19
 
20
20
  export interface DataTableFilterDialogProps {
21
21
  column: Column<any>;
@@ -38,7 +38,7 @@ export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
38
38
  {columnDataType === 'String' ? (
39
39
  <DataTableStringFilter value={filter} onChange={e => setFilter(e)} />
40
40
  ) : columnDataType === 'Int' || columnDataType === 'Float' ? (
41
- <DataTableNumberFilter value={filter} onChange={e => setFilter(e)} mode='number' />
41
+ <DataTableNumberFilter value={filter} onChange={e => setFilter(e)} mode="number" />
42
42
  ) : columnDataType === 'DateTime' ? (
43
43
  <DataTableDateTimeFilter value={filter} onChange={e => setFilter(e)} />
44
44
  ) : columnDataType === 'Boolean' ? (
@@ -46,7 +46,7 @@ export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
46
46
  ) : columnDataType === 'ID' ? (
47
47
  <DataTableIdFilter value={filter} onChange={e => setFilter(e)} />
48
48
  ) : columnDataType === 'Money' ? (
49
- <DataTableNumberFilter value={filter} onChange={e => setFilter(e)} mode='money' />
49
+ <DataTableNumberFilter value={filter} onChange={e => setFilter(e)} mode="money" />
50
50
  ) : null}
51
51
  <DialogFooter className="sm:justify-end">
52
52
  {columnFilter && (
@@ -58,7 +58,7 @@ export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
58
58
  <Button
59
59
  type="button"
60
60
  variant="secondary"
61
- onClick={e => {
61
+ onClick={() => {
62
62
  column.setFilterValue(filter);
63
63
  setFilter(undefined);
64
64
  }}
@@ -9,11 +9,11 @@ interface DataTablePaginationProps<TData> {
9
9
  }
10
10
 
11
11
  export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
12
+ const selectedRowCount = Object.keys(table.getState().rowSelection).length;
12
13
  return (
13
14
  <div className="flex items-center justify-between px-2">
14
15
  <div className="flex-1 text-sm text-muted-foreground">
15
- {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length}{' '}
16
- row(s) selected.
16
+ {selectedRowCount} of {table.getFilteredRowModel().rows.length} row(s) selected.
17
17
  </div>
18
18
  <div className="flex items-center space-x-6 lg:space-x-8">
19
19
  <div className="flex items-center space-x-2">
@@ -2,8 +2,11 @@
2
2
 
3
3
  import { DataTablePagination } from '@/components/data-table/data-table-pagination.js';
4
4
  import { DataTableViewOptions } from '@/components/data-table/data-table-view-options.js';
5
+ import { RefreshButton } from '@/components/data-table/refresh-button.js';
5
6
  import { Input } from '@/components/ui/input.js';
6
7
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.js';
8
+ import { BulkAction } from '@/framework/data-table/data-table-types.js';
9
+ import { useChannel } from '@/hooks/use-channel.js';
7
10
  import {
8
11
  ColumnDef,
9
12
  ColumnFilter,
@@ -17,13 +20,12 @@ import {
17
20
  useReactTable,
18
21
  VisibilityState,
19
22
  } from '@tanstack/react-table';
20
- import { TableOptions } from '@tanstack/table-core';
23
+ import { RowSelectionState, TableOptions } from '@tanstack/table-core';
21
24
  import React, { Suspense, useEffect } from 'react';
22
25
  import { AddFilterMenu } from './add-filter-menu.js';
26
+ import { DataTableBulkActions } from './data-table-bulk-actions.js';
23
27
  import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
24
28
  import { DataTableFilterBadge } from './data-table-filter-badge.js';
25
- import { useChannel } from '@/hooks/use-channel.js';
26
- import { RefreshButton } from '@/components/data-table/refresh-button.js';
27
29
 
28
30
  export interface FacetedFilter {
29
31
  title: string;
@@ -48,6 +50,7 @@ interface DataTableProps<TData> {
48
50
  defaultColumnVisibility?: VisibilityState;
49
51
  facetedFilters?: { [key: string]: FacetedFilter | undefined };
50
52
  disableViewOptions?: boolean;
53
+ bulkActions?: BulkAction[];
51
54
  /**
52
55
  * This property allows full control over _all_ features of TanStack Table
53
56
  * when needed.
@@ -57,24 +60,25 @@ interface DataTableProps<TData> {
57
60
  }
58
61
 
59
62
  export function DataTable<TData>({
60
- columns,
61
- data,
62
- totalItems,
63
- page,
64
- itemsPerPage,
65
- sorting: sortingInitialState,
66
- columnFilters: filtersInitialState,
67
- onPageChange,
68
- onSortChange,
69
- onFilterChange,
70
- onSearchTermChange,
71
- onColumnVisibilityChange,
72
- defaultColumnVisibility,
73
- facetedFilters,
74
- disableViewOptions,
75
- setTableOptions,
76
- onRefresh,
77
- }: DataTableProps<TData>) {
63
+ columns,
64
+ data,
65
+ totalItems,
66
+ page,
67
+ itemsPerPage,
68
+ sorting: sortingInitialState,
69
+ columnFilters: filtersInitialState,
70
+ onPageChange,
71
+ onSortChange,
72
+ onFilterChange,
73
+ onSearchTermChange,
74
+ onColumnVisibilityChange,
75
+ defaultColumnVisibility,
76
+ facetedFilters,
77
+ disableViewOptions,
78
+ bulkActions,
79
+ setTableOptions,
80
+ onRefresh,
81
+ }: DataTableProps<TData>) {
78
82
  const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
79
83
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
80
84
  const { activeChannel } = useChannel();
@@ -85,11 +89,15 @@ export function DataTable<TData>({
85
89
  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
86
90
  defaultColumnVisibility ?? {},
87
91
  );
92
+ const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
88
93
 
89
94
  useEffect(() => {
90
95
  // If the defaultColumnVisibility changes externally (e.g. the user reset the table settings),
91
96
  // we want to reset the column visibility to the default.
92
- if (defaultColumnVisibility && JSON.stringify(defaultColumnVisibility) !== JSON.stringify(columnVisibility)) {
97
+ if (
98
+ defaultColumnVisibility &&
99
+ JSON.stringify(defaultColumnVisibility) !== JSON.stringify(columnVisibility)
100
+ ) {
93
101
  setColumnVisibility(defaultColumnVisibility);
94
102
  }
95
103
  // We intentionally do not include `columnVisibility` in the dependency array
@@ -98,6 +106,7 @@ export function DataTable<TData>({
98
106
  let tableOptions: TableOptions<TData> = {
99
107
  data,
100
108
  columns,
109
+ getRowId: row => (row as { id: string }).id,
101
110
  getCoreRowModel: getCoreRowModel(),
102
111
  getPaginationRowModel: getPaginationRowModel(),
103
112
  manualPagination: true,
@@ -108,11 +117,13 @@ export function DataTable<TData>({
108
117
  onSortingChange: setSorting,
109
118
  onColumnVisibilityChange: setColumnVisibility,
110
119
  onColumnFiltersChange: setColumnFilters,
120
+ onRowSelectionChange: setRowSelection,
111
121
  state: {
112
122
  pagination,
113
123
  sorting,
114
124
  columnVisibility,
115
125
  columnFilters,
126
+ rowSelection,
116
127
  },
117
128
  };
118
129
 
@@ -171,12 +182,19 @@ export function DataTable<TData>({
171
182
  .map(f => {
172
183
  const column = table.getColumn(f.id);
173
184
  const currency = activeChannel?.defaultCurrencyCode ?? 'USD';
174
- return <DataTableFilterBadge
175
- key={f.id}
176
- filter={f}
177
- currencyCode={currency}
178
- dataType={(column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'}
179
- onRemove={() => setColumnFilters(old => old.filter(x => x.id !== f.id))} />;
185
+ return (
186
+ <DataTableFilterBadge
187
+ key={f.id}
188
+ filter={f}
189
+ currencyCode={currency}
190
+ dataType={
191
+ (column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'
192
+ }
193
+ onRemove={() =>
194
+ setColumnFilters(old => old.filter(x => x.id !== f.id))
195
+ }
196
+ />
197
+ );
180
198
  })}
181
199
  </div>
182
200
  </div>
@@ -185,6 +203,7 @@ export function DataTable<TData>({
185
203
  {onRefresh && <RefreshButton onRefresh={onRefresh} />}
186
204
  </div>
187
205
  </div>
206
+ <DataTableBulkActions bulkActions={bulkActions ?? []} table={table} />
188
207
  <div className="rounded-md border my-2">
189
208
  <Table>
190
209
  <TableHeader>
@@ -196,9 +215,9 @@ export function DataTable<TData>({
196
215
  {header.isPlaceholder
197
216
  ? null
198
217
  : flexRender(
199
- header.column.columnDef.header,
200
- header.getContext(),
201
- )}
218
+ header.column.columnDef.header,
219
+ header.getContext(),
220
+ )}
202
221
  </TableHead>
203
222
  );
204
223
  })}
@@ -1,11 +1,11 @@
1
- import { DATETIME_OPERATORS } from './filters/data-table-datetime-filter.js';
1
+ import { Trans } from '@/lib/trans.js';
2
2
  import { BOOLEAN_OPERATORS } from './filters/data-table-boolean-filter.js';
3
+ import { DATETIME_OPERATORS } from './filters/data-table-datetime-filter.js';
3
4
  import { ID_OPERATORS } from './filters/data-table-id-filter.js';
4
5
  import { NUMBER_OPERATORS } from './filters/data-table-number-filter.js';
5
6
  import { STRING_OPERATORS } from './filters/data-table-string-filter.js';
6
- import { Trans } from '@/lib/trans.js';
7
7
 
8
- type Operator =
8
+ export type Operator =
9
9
  | (typeof DATETIME_OPERATORS)[number]
10
10
  | (typeof BOOLEAN_OPERATORS)[number]
11
11
  | (typeof ID_OPERATORS)[number]
@@ -57,11 +57,7 @@ export function AssignedFacetValues({
57
57
  );
58
58
  })}
59
59
  </div>
60
- {canUpdate && (
61
- <FacetValueSelector
62
- onValueSelect={onSelectHandler}
63
- />
64
- )}
60
+ {canUpdate && <FacetValueSelector onValueSelect={onSelectHandler} />}
65
61
  </>
66
62
  );
67
63
  }
@@ -7,15 +7,9 @@ import {
7
7
  } from '@/framework/document-introspection/get-document-structure.js';
8
8
  import { useListQueryFields } from '@/framework/document-introspection/hooks.js';
9
9
  import { api } from '@/graphql/api.js';
10
- import { useMutation, useQueryClient } from '@tanstack/react-query';
10
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
11
11
  import { useDebounce } from '@uidotdev/usehooks';
12
12
 
13
- import {
14
- DropdownMenu,
15
- DropdownMenuContent,
16
- DropdownMenuItem,
17
- DropdownMenuTrigger,
18
- } from '@/components/ui/dropdown-menu.js';
19
13
  import {
20
14
  AlertDialog,
21
15
  AlertDialogAction,
@@ -27,11 +21,17 @@ import {
27
21
  AlertDialogTitle,
28
22
  AlertDialogTrigger,
29
23
  } from '@/components/ui/alert-dialog.js';
24
+ import {
25
+ DropdownMenu,
26
+ DropdownMenuContent,
27
+ DropdownMenuItem,
28
+ DropdownMenuTrigger,
29
+ } from '@/components/ui/dropdown-menu.js';
30
30
  import { DisplayComponent } from '@/framework/component-registry/dynamic-component.js';
31
+ import { BulkAction } from '@/framework/data-table/data-table-types.js';
31
32
  import { ResultOf } from '@/graphql/graphql.js';
32
33
  import { Trans, useLingui } from '@/lib/trans.js';
33
34
  import { TypedDocumentNode } from '@graphql-typed-document-node/core';
34
- import { useQuery } from '@tanstack/react-query';
35
35
  import {
36
36
  ColumnFiltersState,
37
37
  ColumnSort,
@@ -44,6 +44,7 @@ import { EllipsisIcon, TrashIcon } from 'lucide-react';
44
44
  import React, { useMemo } from 'react';
45
45
  import { toast } from 'sonner';
46
46
  import { Button } from '../ui/button.js';
47
+ import { Checkbox } from '../ui/checkbox.js';
47
48
 
48
49
  // Type that identifies a paginated list structure (has items array and totalItems)
49
50
  type IsPaginatedList<T> = T extends { items: any[]; totalItems: number } ? true : false;
@@ -227,6 +228,7 @@ export interface PaginatedListDataTableProps<
227
228
  onColumnVisibilityChange?: (table: Table<any>, columnVisibility: VisibilityState) => void;
228
229
  facetedFilters?: FacetedFilterConfig<T>;
229
230
  rowActions?: RowAction<PaginatedListItemFields<T>>[];
231
+ bulkActions?: BulkAction[];
230
232
  disableViewOptions?: boolean;
231
233
  transformData?: (data: PaginatedListItemFields<T>[]) => PaginatedListItemFields<T>[];
232
234
  setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
@@ -265,6 +267,7 @@ export function PaginatedListDataTable<
265
267
  onColumnVisibilityChange,
266
268
  facetedFilters,
267
269
  rowActions,
270
+ bulkActions,
268
271
  disableViewOptions,
269
272
  setTableOptions,
270
273
  transformData,
@@ -309,6 +312,7 @@ export function PaginatedListDataTable<
309
312
  function refetchPaginatedList() {
310
313
  queryClient.invalidateQueries({ queryKey });
311
314
  }
315
+
312
316
  registerRefresher?.(refetchPaginatedList);
313
317
 
314
318
  const { data } = useQuery({
@@ -427,7 +431,10 @@ export function PaginatedListDataTable<
427
431
  // existing order
428
432
  const orderedColumns = finalColumns
429
433
  .filter(column => column.id && defaultColumnOrder.includes(column.id as any))
430
- .sort((a, b) => defaultColumnOrder.indexOf(a.id as any) - defaultColumnOrder.indexOf(b.id as any));
434
+ .sort(
435
+ (a, b) =>
436
+ defaultColumnOrder.indexOf(a.id as any) - defaultColumnOrder.indexOf(b.id as any),
437
+ );
431
438
  const remainingColumns = finalColumns.filter(
432
439
  column => !column.id || !defaultColumnOrder.includes(column.id as any),
433
440
  );
@@ -441,6 +448,31 @@ export function PaginatedListDataTable<
441
448
  }
442
449
  }
443
450
 
451
+ // Add the row selection column
452
+ finalColumns.unshift({
453
+ id: 'selection',
454
+ accessorKey: 'selection',
455
+ header: ({ table }) => (
456
+ <Checkbox
457
+ className="mx-1"
458
+ checked={table.getIsAllRowsSelected()}
459
+ onCheckedChange={checked =>
460
+ table.toggleAllRowsSelected(checked === 'indeterminate' ? undefined : checked)
461
+ }
462
+ />
463
+ ),
464
+ enableColumnFilter: false,
465
+ cell: ({ row }) => {
466
+ return (
467
+ <Checkbox
468
+ className="mx-1"
469
+ checked={row.getIsSelected()}
470
+ onCheckedChange={row.getToggleSelectedHandler()}
471
+ />
472
+ );
473
+ },
474
+ });
475
+
444
476
  return { columns: finalColumns, customFieldColumnNames };
445
477
  }, [fields, customizeColumns, rowActions]);
446
478
 
@@ -465,6 +497,7 @@ export function PaginatedListDataTable<
465
497
  defaultColumnVisibility={columnVisibility}
466
498
  facetedFilters={facetedFilters}
467
499
  disableViewOptions={disableViewOptions}
500
+ bulkActions={bulkActions}
468
501
  setTableOptions={setTableOptions}
469
502
  onRefresh={refetchPaginatedList}
470
503
  />
@@ -536,7 +569,7 @@ function DeleteMutationRowAction({
536
569
  return (
537
570
  <AlertDialog>
538
571
  <AlertDialogTrigger asChild>
539
- <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
572
+ <DropdownMenuItem onSelect={e => e.preventDefault()}>
540
573
  <div className="flex items-center gap-2 text-destructive">
541
574
  <TrashIcon className="w-4 h-4 text-destructive" />
542
575
  <Trans>Delete</Trans>
@@ -549,7 +582,9 @@ function DeleteMutationRowAction({
549
582
  <Trans>Confirm deletion</Trans>
550
583
  </AlertDialogTitle>
551
584
  <AlertDialogDescription>
552
- <Trans>Are you sure you want to delete this item? This action cannot be undone.</Trans>
585
+ <Trans>
586
+ Are you sure you want to delete this item? This action cannot be undone.
587
+ </Trans>
553
588
  </AlertDialogDescription>
554
589
  </AlertDialogHeader>
555
590
  <AlertDialogFooter>
@@ -567,6 +602,7 @@ function DeleteMutationRowAction({
567
602
  </AlertDialog>
568
603
  );
569
604
  }
605
+
570
606
  /**
571
607
  * Returns the default column visibility configuration.
572
608
  */
@@ -0,0 +1,21 @@
1
+ import { BulkAction } from '@/framework/data-table/data-table-types.js';
2
+
3
+ import { globalRegistry } from '../registry/global-registry.js';
4
+
5
+ globalRegistry.register('bulkActionsRegistry', new Map<string, BulkAction[]>());
6
+
7
+ export function getBulkActions(pageId: string, blockId = 'list-table'): BulkAction[] {
8
+ const key = createKey(pageId, blockId);
9
+ return globalRegistry.get('bulkActionsRegistry').get(key) || [];
10
+ }
11
+
12
+ export function addBulkAction(pageId: string, blockId: string | undefined, action: BulkAction) {
13
+ const bulkActionsRegistry = globalRegistry.get('bulkActionsRegistry');
14
+ const key = createKey(pageId, blockId);
15
+ const existingActions = bulkActionsRegistry.get(key) || [];
16
+ bulkActionsRegistry.set(key, [...existingActions, action]);
17
+ }
18
+
19
+ function createKey(pageId: string, blockId: string | undefined): string {
20
+ return `${pageId}__${blockId ?? 'list-table'}`;
21
+ }
@@ -0,0 +1,25 @@
1
+ import { Table } from '@tanstack/react-table';
2
+
3
+ export type BulkActionContext<Item extends { id: string } & Record<string, any>> = {
4
+ selection: Item[];
5
+ table: Table<Item>;
6
+ };
7
+
8
+ export type BulkActionComponent<Item extends { id: string } & Record<string, any>> = React.FunctionComponent<
9
+ BulkActionContext<Item>
10
+ >;
11
+
12
+ /**
13
+ * @description
14
+ * **Status: Developer Preview**
15
+ *
16
+ * A bulk action is a component that will be rendered in the bulk actions dropdown.
17
+ *
18
+ * @docsCategory components
19
+ * @docsPage DataTableBulkActions
20
+ * @since 3.4.0
21
+ */
22
+ export type BulkAction = {
23
+ order?: number;
24
+ component: BulkActionComponent<any>;
25
+ };
@@ -1,3 +1,5 @@
1
+ import { addBulkAction } from '@/framework/data-table/data-table-extensions.js';
2
+
1
3
  import { registerDashboardWidget } from '../dashboard-widget/widget-extensions.js';
2
4
  import { addCustomFormComponent } from '../form-engine/custom-form-component-extensions.js';
3
5
  import {
@@ -82,6 +84,15 @@ export function defineDashboardExtension(extension: DashboardExtension) {
82
84
  addCustomFormComponent(component);
83
85
  }
84
86
  }
87
+ if (extension.dataTables) {
88
+ for (const dataTable of extension.dataTables) {
89
+ if (dataTable.bulkActions?.length) {
90
+ for (const action of dataTable.bulkActions) {
91
+ addBulkAction(dataTable.pageId, dataTable.blockId, action);
92
+ }
93
+ }
94
+ }
95
+ }
85
96
  const callbacks = globalRegistry.get('extensionSourceChangeCallbacks');
86
97
  if (callbacks.size) {
87
98
  for (const callback of callbacks) {
@@ -5,6 +5,7 @@ import type React from 'react';
5
5
 
6
6
  import { DashboardAlertDefinition } from '../alert/types.js';
7
7
  import { DashboardWidgetDefinition } from '../dashboard-widget/types.js';
8
+ import { BulkAction } from '../data-table/data-table-types.js';
8
9
  import { CustomFormComponentInputProps } from '../form-engine/custom-form-component.js';
9
10
  import { NavMenuItem } from '../nav-menu/nav-menu-extensions.js';
10
11
 
@@ -109,6 +110,35 @@ export interface DashboardPageBlockDefinition {
109
110
  requiresPermission?: string | string[];
110
111
  }
111
112
 
113
+ /**
114
+ * @description
115
+ * **Status: Developer Preview**
116
+ *
117
+ * This allows you to customize aspects of existing data tables in the dashboard.
118
+ *
119
+ * @docsCategory extensions
120
+ * @since 3.4.0
121
+ */
122
+ export interface DashboardDataTableDefinition {
123
+ /**
124
+ * @description
125
+ * The ID of the page where the data table is located, e.g. `'product-list'`, `'order-list'`.
126
+ */
127
+ pageId: string;
128
+ /**
129
+ * @description
130
+ * The ID of the data table block. Defaults to `'list-table'`, which is the default blockId
131
+ * for the standard list pages. However, some other pages may use a different blockId,
132
+ * such as `'product-variants-table'` on the `'product-detail'` page.
133
+ */
134
+ blockId?: string;
135
+ /**
136
+ * @description
137
+ * An array of additional bulk actions that will be available on the data table.
138
+ */
139
+ bulkActions?: BulkAction[];
140
+ }
141
+
112
142
  /**
113
143
  * @description
114
144
  * **Status: Developer Preview**
@@ -155,4 +185,9 @@ export interface DashboardExtension {
155
185
  * Allows you to define custom form components for custom fields in the dashboard.
156
186
  */
157
187
  customFormComponents?: DashboardCustomFormComponent[];
188
+ /**
189
+ * @description
190
+ * Allows you to customize aspects of existing data tables in the dashboard.
191
+ */
192
+ dataTables?: DashboardDataTableDefinition[];
158
193
  }
@@ -1,8 +1,5 @@
1
1
  import { getOperationVariablesFields } from '@/framework/document-introspection/get-document-structure.js';
2
- import {
3
- createFormSchemaFromFields,
4
- getDefaultValuesFromFields,
5
- } from '@/framework/form-engine/form-schema-tools.js';
2
+ import { createFormSchemaFromFields, getDefaultValuesFromFields } from '@/framework/form-engine/form-schema-tools.js';
6
3
  import { useChannel } from '@/hooks/use-channel.js';
7
4
  import { useServerConfig } from '@/hooks/use-server-config.js';
8
5
  import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
@@ -57,7 +54,7 @@ export function useGeneratedForm<
57
54
  },
58
55
  mode: 'onChange',
59
56
  defaultValues,
60
- values: processedEntity ? processedEntity : defaultValues,
57
+ values: processedEntity ? setValues(processedEntity) : defaultValues,
61
58
  });
62
59
  let submitHandler = (event: FormEvent) => {
63
60
  event.preventDefault();