@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.
- package/dist/plugin/tests/barrel-exports.spec.js +1 -1
- package/dist/plugin/vite-plugin-config.js +1 -0
- package/dist/plugin/vite-plugin-dashboard-metadata.d.ts +1 -3
- package/dist/plugin/vite-plugin-dashboard-metadata.js +1 -8
- package/dist/plugin/vite-plugin-tailwind-source.d.ts +7 -0
- package/dist/plugin/vite-plugin-tailwind-source.js +49 -0
- package/dist/plugin/vite-plugin-vendure-dashboard.js +3 -1
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +43 -34
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +2 -5
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_products/components/assign-facet-values-dialog.tsx +98 -0
- package/src/app/routes/_authenticated/_products/components/assign-to-channel-dialog.tsx +126 -0
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +268 -0
- package/src/app/routes/_authenticated/_products/products.graphql.ts +64 -0
- package/src/app/routes/_authenticated/_products/products.tsx +31 -2
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +14 -9
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +1 -1
- package/src/app/styles.css +3 -0
- package/src/lib/components/data-table/data-table-bulk-action-item.tsx +101 -0
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +89 -0
- package/src/lib/components/data-table/data-table-filter-badge.tsx +16 -8
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +4 -4
- package/src/lib/components/data-table/data-table-pagination.tsx +2 -2
- package/src/lib/components/data-table/data-table.tsx +50 -31
- package/src/lib/components/data-table/human-readable-operator.tsx +3 -3
- package/src/lib/components/shared/assigned-facet-values.tsx +1 -5
- package/src/lib/components/shared/paginated-list-data-table.tsx +47 -11
- package/src/lib/framework/data-table/data-table-extensions.ts +21 -0
- package/src/lib/framework/data-table/data-table-types.ts +25 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +11 -0
- package/src/lib/framework/extension-api/extension-api-types.ts +35 -0
- package/src/lib/framework/form-engine/use-generated-form.tsx +2 -5
- package/src/lib/framework/layout-engine/page-block-provider.tsx +6 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +43 -33
- package/src/lib/framework/page/list-page.tsx +6 -8
- package/src/lib/framework/registry/registry-types.ts +4 -2
- package/src/lib/hooks/use-page-block.tsx +10 -0
- package/src/lib/index.ts +8 -1
- package/vite/tests/barrel-exports.spec.ts +13 -9
- package/vite/vite-plugin-config.ts +1 -0
- package/vite/vite-plugin-dashboard-metadata.ts +1 -9
- package/vite/vite-plugin-tailwind-source.ts +65 -0
- package/vite/vite-plugin-vendure-dashboard.ts +5 -3
- /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 {
|
|
7
|
-
import {
|
|
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"
|
|
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({
|
|
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 './
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 (
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
218
|
+
header.column.columnDef.header,
|
|
219
|
+
header.getContext(),
|
|
220
|
+
)}
|
|
202
221
|
</TableHead>
|
|
203
222
|
);
|
|
204
223
|
})}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
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]
|
|
@@ -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(
|
|
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={
|
|
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>
|
|
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();
|