@vendure/dashboard 3.3.5-master-202506250727 → 3.3.5-master-202506251318
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/_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 +3 -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/custom-fields-form.tsx +141 -67
- 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
|
@@ -119,3 +119,67 @@ export const deleteProductDocument = graphql(`
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
`);
|
|
122
|
+
|
|
123
|
+
export const deleteProductsDocument = graphql(`
|
|
124
|
+
mutation DeleteProducts($ids: [ID!]!) {
|
|
125
|
+
deleteProducts(ids: $ids) {
|
|
126
|
+
result
|
|
127
|
+
message
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
export const assignProductsToChannelDocument = graphql(`
|
|
133
|
+
mutation AssignProductsToChannel($input: AssignProductsToChannelInput!) {
|
|
134
|
+
assignProductsToChannel(input: $input) {
|
|
135
|
+
id
|
|
136
|
+
channels {
|
|
137
|
+
id
|
|
138
|
+
code
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`);
|
|
143
|
+
|
|
144
|
+
export const removeProductsFromChannelDocument = graphql(`
|
|
145
|
+
mutation RemoveProductsFromChannel($input: RemoveProductsFromChannelInput!) {
|
|
146
|
+
removeProductsFromChannel(input: $input) {
|
|
147
|
+
id
|
|
148
|
+
channels {
|
|
149
|
+
id
|
|
150
|
+
code
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
`);
|
|
155
|
+
|
|
156
|
+
export const updateProductsDocument = graphql(`
|
|
157
|
+
mutation UpdateProducts($input: [UpdateProductInput!]!) {
|
|
158
|
+
updateProducts(input: $input) {
|
|
159
|
+
id
|
|
160
|
+
name
|
|
161
|
+
facetValues {
|
|
162
|
+
id
|
|
163
|
+
name
|
|
164
|
+
code
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
export const duplicateEntityDocument = graphql(`
|
|
171
|
+
mutation DuplicateEntity($input: DuplicateEntityInput!) {
|
|
172
|
+
duplicateEntity(input: $input) {
|
|
173
|
+
... on DuplicateEntitySuccess {
|
|
174
|
+
newEntityId
|
|
175
|
+
}
|
|
176
|
+
... on ErrorResult {
|
|
177
|
+
errorCode
|
|
178
|
+
message
|
|
179
|
+
}
|
|
180
|
+
... on DuplicateEntityError {
|
|
181
|
+
duplicationError
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
`);
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { DetailPageButton } from '@/components/shared/detail-page-button.js';
|
|
2
2
|
import { PermissionGuard } from '@/components/shared/permission-guard.js';
|
|
3
3
|
import { Button } from '@/components/ui/button.js';
|
|
4
|
-
import {
|
|
4
|
+
import { PageActionBarRight } from '@/framework/layout-engine/page-layout.js';
|
|
5
5
|
import { ListPage } from '@/framework/page/list-page.js';
|
|
6
6
|
import { Trans } from '@/lib/trans.js';
|
|
7
7
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
8
|
-
import { PlusIcon
|
|
8
|
+
import { PlusIcon } from 'lucide-react';
|
|
9
|
+
import {
|
|
10
|
+
AssignFacetValuesToProductsBulkAction,
|
|
11
|
+
AssignProductsToChannelBulkAction,
|
|
12
|
+
DeleteProductsBulkAction,
|
|
13
|
+
DuplicateProductsBulkAction,
|
|
14
|
+
RemoveProductsFromChannelBulkAction,
|
|
15
|
+
} from './components/product-bulk-actions.js';
|
|
9
16
|
import { deleteProductDocument, productListDocument } from './products.graphql.js';
|
|
10
17
|
|
|
11
18
|
export const Route = createFileRoute('/_authenticated/_products/products')({
|
|
@@ -32,6 +39,28 @@ function ProductListPage() {
|
|
|
32
39
|
};
|
|
33
40
|
}}
|
|
34
41
|
route={Route}
|
|
42
|
+
bulkActions={[
|
|
43
|
+
{
|
|
44
|
+
component: AssignProductsToChannelBulkAction,
|
|
45
|
+
order: 100,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
component: RemoveProductsFromChannelBulkAction,
|
|
49
|
+
order: 200,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
component: AssignFacetValuesToProductsBulkAction,
|
|
53
|
+
order: 300,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
component: DuplicateProductsBulkAction,
|
|
57
|
+
order: 400,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
component: DeleteProductsBulkAction,
|
|
61
|
+
order: 500,
|
|
62
|
+
},
|
|
63
|
+
]}
|
|
35
64
|
>
|
|
36
65
|
<PageActionBarRight>
|
|
37
66
|
<PermissionGuard requires={['CreateProduct', 'CreateCatalog']}>
|
|
@@ -50,9 +50,11 @@ function ProductDetailPage() {
|
|
|
50
50
|
const navigate = useNavigate();
|
|
51
51
|
const creatingNewEntity = params.id === NEW_ENTITY_PATH;
|
|
52
52
|
const { i18n } = useLingui();
|
|
53
|
-
const refreshRef = useRef<() => void>(() => {
|
|
53
|
+
const refreshRef = useRef<() => void>(() => {
|
|
54
|
+
});
|
|
54
55
|
|
|
55
56
|
const { form, submitHandler, entity, isPending, refreshEntity, resetForm } = useDetailPage({
|
|
57
|
+
entityName: 'Product',
|
|
56
58
|
queryDocument: productDetailDocument,
|
|
57
59
|
createDocument: createProductDocument,
|
|
58
60
|
updateDocument: updateProductDocument,
|
package/src/app/styles.css
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
@custom-variant dark (&:is(.dark *));
|
|
6
6
|
|
|
7
|
+
/* @source rules from extensions will be added here by the dashboardTailwindSourcePlugin */
|
|
8
|
+
|
|
7
9
|
/*
|
|
8
10
|
* Important: This is not an actual import. We are pre-processing this CSS file
|
|
9
11
|
* to inject the theme variables into the CSS. This import will be replaced
|
|
@@ -64,6 +66,7 @@
|
|
|
64
66
|
* {
|
|
65
67
|
@apply border-border outline-ring/50;
|
|
66
68
|
}
|
|
69
|
+
|
|
67
70
|
body {
|
|
68
71
|
@apply bg-background text-foreground;
|
|
69
72
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { usePermissions } from '@/hooks/use-permissions.js';
|
|
2
|
+
import { Trans } from '@/lib/trans.js';
|
|
3
|
+
import { cn } from '@/lib/utils.js';
|
|
4
|
+
import { LucideIcon } from 'lucide-react';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
AlertDialog,
|
|
8
|
+
AlertDialogAction,
|
|
9
|
+
AlertDialogCancel,
|
|
10
|
+
AlertDialogContent,
|
|
11
|
+
AlertDialogDescription,
|
|
12
|
+
AlertDialogFooter,
|
|
13
|
+
AlertDialogHeader,
|
|
14
|
+
AlertDialogTitle,
|
|
15
|
+
AlertDialogTrigger,
|
|
16
|
+
} from '../ui/alert-dialog.js';
|
|
17
|
+
import { DropdownMenuItem } from '../ui/dropdown-menu.js';
|
|
18
|
+
|
|
19
|
+
export interface DataTableBulkActionItemProps {
|
|
20
|
+
label: React.ReactNode;
|
|
21
|
+
icon?: LucideIcon;
|
|
22
|
+
confirmationText?: React.ReactNode;
|
|
23
|
+
onClick: () => void;
|
|
24
|
+
className?: string;
|
|
25
|
+
requiresPermission?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function DataTableBulkActionItem({
|
|
29
|
+
label,
|
|
30
|
+
icon: Icon,
|
|
31
|
+
confirmationText,
|
|
32
|
+
className,
|
|
33
|
+
onClick,
|
|
34
|
+
requiresPermission,
|
|
35
|
+
}: DataTableBulkActionItemProps) {
|
|
36
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
37
|
+
const { hasPermissions } = usePermissions();
|
|
38
|
+
const userHasPermission = hasPermissions(requiresPermission ?? []);
|
|
39
|
+
|
|
40
|
+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
if (!userHasPermission) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (confirmationText) {
|
|
47
|
+
setIsOpen(true);
|
|
48
|
+
} else {
|
|
49
|
+
onClick?.();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleConfirm = () => {
|
|
54
|
+
setIsOpen(false);
|
|
55
|
+
onClick?.();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleCancel = () => {
|
|
59
|
+
setIsOpen(false);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (confirmationText) {
|
|
63
|
+
return (
|
|
64
|
+
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
|
65
|
+
<AlertDialogTrigger asChild>
|
|
66
|
+
<DropdownMenuItem onClick={handleClick} disabled={!userHasPermission}>
|
|
67
|
+
{Icon && <Icon className={cn('mr-1 h-4 w-4', className)} />}
|
|
68
|
+
<span className={cn('text-sm', className)}>
|
|
69
|
+
<Trans>{label}</Trans>
|
|
70
|
+
</span>
|
|
71
|
+
</DropdownMenuItem>
|
|
72
|
+
</AlertDialogTrigger>
|
|
73
|
+
<AlertDialogContent>
|
|
74
|
+
<AlertDialogHeader>
|
|
75
|
+
<AlertDialogTitle>
|
|
76
|
+
<Trans>Confirm Action</Trans>
|
|
77
|
+
</AlertDialogTitle>
|
|
78
|
+
<AlertDialogDescription>{confirmationText}</AlertDialogDescription>
|
|
79
|
+
</AlertDialogHeader>
|
|
80
|
+
<AlertDialogFooter>
|
|
81
|
+
<AlertDialogCancel onClick={handleCancel}>
|
|
82
|
+
<Trans>Cancel</Trans>
|
|
83
|
+
</AlertDialogCancel>
|
|
84
|
+
<AlertDialogAction onClick={handleConfirm}>
|
|
85
|
+
<Trans>Continue</Trans>
|
|
86
|
+
</AlertDialogAction>
|
|
87
|
+
</AlertDialogFooter>
|
|
88
|
+
</AlertDialogContent>
|
|
89
|
+
</AlertDialog>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<DropdownMenuItem onClick={handleClick}>
|
|
95
|
+
{Icon && <Icon className={cn('mr-1 h-4 w-4', className)} />}
|
|
96
|
+
<span className={cn('text-sm', className)}>
|
|
97
|
+
<Trans>{label}</Trans>
|
|
98
|
+
</span>
|
|
99
|
+
</DropdownMenuItem>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -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]
|