@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
|
@@ -7,12 +7,15 @@ import {
|
|
|
7
7
|
FormMessage,
|
|
8
8
|
} from '@/components/ui/form.js';
|
|
9
9
|
import { Input } from '@/components/ui/input.js';
|
|
10
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs.js';
|
|
10
11
|
import { CustomFormComponent } from '@/framework/form-engine/custom-form-component.js';
|
|
11
12
|
import { useCustomFieldConfig } from '@/hooks/use-custom-field-config.js';
|
|
12
13
|
import { useUserSettings } from '@/hooks/use-user-settings.js';
|
|
14
|
+
import { useLingui } from '@/lib/trans.js';
|
|
13
15
|
import { customFieldConfigFragment } from '@/providers/server-config.js';
|
|
14
16
|
import { CustomFieldType } from '@vendure/common/lib/shared-types';
|
|
15
17
|
import { ResultOf } from 'gql.tada';
|
|
18
|
+
import React, { useMemo } from 'react';
|
|
16
19
|
import { Control, ControllerRenderProps } from 'react-hook-form';
|
|
17
20
|
import { Switch } from '../ui/switch.js';
|
|
18
21
|
import { TranslatableFormField } from './translatable-form-field.js';
|
|
@@ -29,6 +32,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Custom
|
|
|
29
32
|
const {
|
|
30
33
|
settings: { displayLanguage },
|
|
31
34
|
} = useUserSettings();
|
|
35
|
+
const { i18n } = useLingui();
|
|
32
36
|
|
|
33
37
|
const getTranslation = (input: Array<{ languageCode: string; value: string }> | null | undefined) => {
|
|
34
38
|
return input?.find(t => t.languageCode === displayLanguage)?.value;
|
|
@@ -42,18 +46,80 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Custom
|
|
|
42
46
|
: `customFields.${fieldDefName}`;
|
|
43
47
|
};
|
|
44
48
|
|
|
49
|
+
// Group custom fields by tabs
|
|
50
|
+
const groupedFields = useMemo(() => {
|
|
51
|
+
if (!customFields) return [];
|
|
52
|
+
|
|
53
|
+
const tabMap = new Map<string, CustomFieldConfig[]>();
|
|
54
|
+
const defaultTabName = '__default_tab__';
|
|
55
|
+
|
|
56
|
+
for (const field of customFields) {
|
|
57
|
+
const tabName = field.ui?.tab ?? defaultTabName;
|
|
58
|
+
if (tabMap.has(tabName)) {
|
|
59
|
+
tabMap.get(tabName)?.push(field);
|
|
60
|
+
} else {
|
|
61
|
+
tabMap.set(tabName, [field]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Array.from(tabMap.entries())
|
|
66
|
+
.sort((a, b) => (a[0] === defaultTabName ? -1 : 1))
|
|
67
|
+
.map(([tabName, customFields]) => ({
|
|
68
|
+
tabName: tabName === defaultTabName ? 'general' : tabName,
|
|
69
|
+
customFields,
|
|
70
|
+
}));
|
|
71
|
+
}, [customFields]);
|
|
72
|
+
|
|
73
|
+
// Check if we should show tabs (more than one tab or at least one field has a tab)
|
|
74
|
+
const shouldShowTabs = useMemo(() => {
|
|
75
|
+
if (!customFields) return false;
|
|
76
|
+
const hasTabbedFields = customFields.some(field => field.ui?.tab);
|
|
77
|
+
return hasTabbedFields || groupedFields.length > 1;
|
|
78
|
+
}, [customFields, groupedFields.length]);
|
|
79
|
+
|
|
80
|
+
if (!shouldShowTabs) {
|
|
81
|
+
// Single tab view - use the original grid layout
|
|
82
|
+
return (
|
|
83
|
+
<div className="grid grid-cols-2 gap-4">
|
|
84
|
+
{customFields?.map(fieldDef => (
|
|
85
|
+
<CustomFieldItem
|
|
86
|
+
key={fieldDef.name}
|
|
87
|
+
fieldDef={fieldDef}
|
|
88
|
+
control={control}
|
|
89
|
+
fieldName={getFieldName(fieldDef.name)}
|
|
90
|
+
getTranslation={getTranslation}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Tabbed view
|
|
45
98
|
return (
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
key={
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
<Tabs defaultValue={groupedFields[0]?.tabName} className="w-full">
|
|
100
|
+
<TabsList>
|
|
101
|
+
{groupedFields.map(group => (
|
|
102
|
+
<TabsTrigger key={group.tabName} value={group.tabName}>
|
|
103
|
+
{group.tabName === 'general' ? i18n.t('General') : group.tabName}
|
|
104
|
+
</TabsTrigger>
|
|
105
|
+
))}
|
|
106
|
+
</TabsList>
|
|
107
|
+
{groupedFields.map(group => (
|
|
108
|
+
<TabsContent key={group.tabName} value={group.tabName} className="mt-4">
|
|
109
|
+
<div className="grid grid-cols-2 gap-4">
|
|
110
|
+
{group.customFields.map(fieldDef => (
|
|
111
|
+
<CustomFieldItem
|
|
112
|
+
key={fieldDef.name}
|
|
113
|
+
fieldDef={fieldDef}
|
|
114
|
+
control={control}
|
|
115
|
+
fieldName={getFieldName(fieldDef.name)}
|
|
116
|
+
getTranslation={getTranslation}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</TabsContent>
|
|
55
121
|
))}
|
|
56
|
-
</
|
|
122
|
+
</Tabs>
|
|
57
123
|
);
|
|
58
124
|
}
|
|
59
125
|
|
|
@@ -69,83 +135,91 @@ interface CustomFieldItemProps {
|
|
|
69
135
|
function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: CustomFieldItemProps) {
|
|
70
136
|
const hasCustomFormComponent = fieldDef.ui && fieldDef.ui.component;
|
|
71
137
|
const isLocaleField = fieldDef.type === 'localeString' || fieldDef.type === 'localeText';
|
|
138
|
+
const shouldBeFullWidth = fieldDef.ui?.fullWidth === true;
|
|
139
|
+
const containerClassName = shouldBeFullWidth ? 'col-span-2' : '';
|
|
72
140
|
|
|
73
141
|
// For locale fields, always use TranslatableFormField regardless of custom components
|
|
74
142
|
if (isLocaleField) {
|
|
75
143
|
return (
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
144
|
+
<div className={containerClassName}>
|
|
145
|
+
<TranslatableFormField
|
|
146
|
+
control={control}
|
|
147
|
+
name={fieldName}
|
|
148
|
+
render={({ field, ...props }) => (
|
|
149
|
+
<FormItem>
|
|
150
|
+
<FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
|
|
151
|
+
<FormControl>
|
|
152
|
+
{hasCustomFormComponent ? (
|
|
153
|
+
<CustomFormComponent
|
|
154
|
+
fieldDef={fieldDef}
|
|
155
|
+
fieldProps={{
|
|
156
|
+
...props,
|
|
157
|
+
field: {
|
|
158
|
+
...field,
|
|
159
|
+
disabled: fieldDef.readonly ?? false,
|
|
160
|
+
},
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
) : (
|
|
164
|
+
<FormInputForType fieldDef={fieldDef} field={field} />
|
|
165
|
+
)}
|
|
166
|
+
</FormControl>
|
|
167
|
+
<FormDescription>{getTranslation(fieldDef.description)}</FormDescription>
|
|
168
|
+
<FormMessage />
|
|
169
|
+
</FormItem>
|
|
170
|
+
)}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
103
173
|
);
|
|
104
174
|
}
|
|
105
175
|
|
|
106
176
|
// For non-locale fields with custom components
|
|
107
177
|
if (hasCustomFormComponent) {
|
|
108
178
|
return (
|
|
179
|
+
<div className={containerClassName}>
|
|
180
|
+
<FormField
|
|
181
|
+
control={control}
|
|
182
|
+
name={fieldName}
|
|
183
|
+
render={fieldProps => (
|
|
184
|
+
<CustomFieldFormItem
|
|
185
|
+
fieldDef={fieldDef}
|
|
186
|
+
getTranslation={getTranslation}
|
|
187
|
+
fieldName={fieldProps.field.name}
|
|
188
|
+
>
|
|
189
|
+
<CustomFormComponent
|
|
190
|
+
fieldDef={fieldDef}
|
|
191
|
+
fieldProps={{
|
|
192
|
+
...fieldProps,
|
|
193
|
+
field: {
|
|
194
|
+
...fieldProps.field,
|
|
195
|
+
disabled: fieldDef.readonly ?? false,
|
|
196
|
+
},
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
</CustomFieldFormItem>
|
|
200
|
+
)}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// For regular fields without custom components
|
|
207
|
+
return (
|
|
208
|
+
<div className={containerClassName}>
|
|
109
209
|
<FormField
|
|
110
210
|
control={control}
|
|
111
211
|
name={fieldName}
|
|
112
|
-
render={
|
|
212
|
+
render={({ field }) => (
|
|
113
213
|
<CustomFieldFormItem
|
|
114
214
|
fieldDef={fieldDef}
|
|
115
215
|
getTranslation={getTranslation}
|
|
116
|
-
fieldName={
|
|
216
|
+
fieldName={field.name}
|
|
117
217
|
>
|
|
118
|
-
<
|
|
119
|
-
fieldDef={fieldDef}
|
|
120
|
-
fieldProps={{
|
|
121
|
-
...fieldProps,
|
|
122
|
-
field: {
|
|
123
|
-
...fieldProps.field,
|
|
124
|
-
disabled: fieldDef.readonly ?? false,
|
|
125
|
-
},
|
|
126
|
-
}}
|
|
127
|
-
/>
|
|
218
|
+
<FormInputForType fieldDef={fieldDef} field={field} />
|
|
128
219
|
</CustomFieldFormItem>
|
|
129
220
|
)}
|
|
130
221
|
/>
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// For regular fields without custom components
|
|
135
|
-
return (
|
|
136
|
-
<FormField
|
|
137
|
-
control={control}
|
|
138
|
-
name={fieldName}
|
|
139
|
-
render={({ field }) => (
|
|
140
|
-
<CustomFieldFormItem
|
|
141
|
-
fieldDef={fieldDef}
|
|
142
|
-
getTranslation={getTranslation}
|
|
143
|
-
fieldName={field.name}
|
|
144
|
-
>
|
|
145
|
-
<FormInputForType fieldDef={fieldDef} field={field} />
|
|
146
|
-
</CustomFieldFormItem>
|
|
147
|
-
)}
|
|
148
|
-
/>
|
|
222
|
+
</div>
|
|
149
223
|
);
|
|
150
224
|
}
|
|
151
225
|
|
|
@@ -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();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PageBlockProps } from '@/framework/layout-engine/page-layout.js';
|
|
2
|
+
import { createContext } from 'react';
|
|
3
|
+
|
|
4
|
+
export type PageBlockContextValue = Pick<PageBlockProps, 'blockId' | 'column' | 'title' | 'description'>;
|
|
5
|
+
|
|
6
|
+
export const PageBlockContext = createContext<PageBlockContextValue | undefined>(undefined);
|