@vendure/dashboard 3.3.6-master-202507090236 → 3.3.6-master-202507110238

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 (37) hide show
  1. package/dist/plugin/tests/barrel-exports.spec.js +1 -1
  2. package/package.json +4 -4
  3. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +30 -37
  4. package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +33 -53
  5. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +14 -7
  6. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +23 -12
  7. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +364 -0
  8. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +222 -0
  9. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +146 -85
  10. package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +268 -31
  11. package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +80 -0
  12. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +102 -0
  13. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +144 -0
  14. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +118 -2
  15. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +144 -52
  16. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +550 -0
  17. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +0 -17
  18. package/src/app/routes/_authenticated/_orders/utils/order-types.ts +5 -2
  19. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +4 -3
  20. package/src/app/routes/_authenticated/_products/products_.$id.tsx +0 -1
  21. package/src/lib/components/data-display/date-time.tsx +7 -1
  22. package/src/lib/components/data-input/relation-input.tsx +11 -0
  23. package/src/lib/components/data-input/relation-selector.tsx +9 -2
  24. package/src/lib/components/data-table/data-table-utils.ts +34 -0
  25. package/src/lib/components/data-table/data-table-view-options.tsx +2 -2
  26. package/src/lib/components/data-table/data-table.tsx +5 -2
  27. package/src/lib/components/data-table/use-generated-columns.tsx +307 -0
  28. package/src/lib/components/shared/paginated-list-data-table.tsx +15 -286
  29. package/src/lib/components/shared/product-variant-selector.tsx +28 -4
  30. package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
  31. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +321 -2
  32. package/src/lib/framework/document-introspection/get-document-structure.ts +149 -31
  33. package/src/lib/framework/extension-api/types/layout.ts +21 -6
  34. package/src/lib/framework/layout-engine/layout-extensions.ts +1 -4
  35. package/src/lib/framework/layout-engine/page-layout.tsx +61 -10
  36. package/src/lib/framework/page/use-detail-page.ts +10 -7
  37. package/vite/tests/barrel-exports.spec.ts +1 -1
@@ -40,6 +40,12 @@ export interface RelationSelectorConfig<T = any> {
40
40
 
41
41
  export interface RelationSelectorProps<T = any> {
42
42
  config: RelationSelectorConfig<T>;
43
+ /**
44
+ * @description
45
+ * The label for the selector trigger. Default is
46
+ * "Select item" for single select and "Select items" for multi select.
47
+ */
48
+ selectorLabel?: React.ReactNode;
43
49
  value?: string | string[];
44
50
  onChange: (value: string | string[]) => void;
45
51
  disabled?: boolean;
@@ -186,6 +192,7 @@ export function RelationSelector<T>({
186
192
  onChange,
187
193
  disabled,
188
194
  className,
195
+ selectorLabel,
189
196
  }: Readonly<RelationSelectorProps<T>>) {
190
197
  const [open, setOpen] = useState(false);
191
198
  const [selectedItemsCache, setSelectedItemsCache] = useState<T[]>([]);
@@ -396,10 +403,10 @@ export function RelationSelector<T>({
396
403
  {isMultiple
397
404
  ? selectedItems.length > 0
398
405
  ? `Add more (${selectedItems.length} selected)`
399
- : 'Select items'
406
+ : selectorLabel ?? <Trans>Select items</Trans>
400
407
  : selectedItems.length > 0
401
408
  ? 'Change selection'
402
- : 'Select item'}
409
+ : selectorLabel ?? <Trans>Select item</Trans>}
403
410
  </Trans>
404
411
  </Button>
405
412
  </PopoverTrigger>
@@ -0,0 +1,34 @@
1
+ import { FieldInfo } from '@/vdb/framework/document-introspection/get-document-structure.js';
2
+
3
+ /**
4
+ * Returns the default column visibility configuration.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const columnVisibility = getColumnVisibility(fields, {
9
+ * id: false,
10
+ * createdAt: false,
11
+ * updatedAt: false,
12
+ * });
13
+ * ```
14
+ */
15
+ export function getColumnVisibility(
16
+ fields: FieldInfo[],
17
+ defaultVisibility?: Record<string, boolean | undefined>,
18
+ customFieldColumnNames?: string[],
19
+ ): Record<string, boolean> {
20
+ const allDefaultsTrue = defaultVisibility && Object.values(defaultVisibility).every(v => v === true);
21
+ const allDefaultsFalse = defaultVisibility && Object.values(defaultVisibility).every(v => v === false);
22
+ return {
23
+ id: false,
24
+ createdAt: false,
25
+ updatedAt: false,
26
+ ...(allDefaultsTrue ? { ...Object.fromEntries(fields.map(f => [f.name, false])) } : {}),
27
+ ...(allDefaultsFalse ? { ...Object.fromEntries(fields.map(f => [f.name, true])) } : {}),
28
+ // Make custom fields hidden by default unless overridden
29
+ ...(customFieldColumnNames
30
+ ? { ...Object.fromEntries(customFieldColumnNames.map(f => [f, false])) }
31
+ : {}),
32
+ ...defaultVisibility,
33
+ };
34
+ }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { DndContext, closestCenter } from '@dnd-kit/core';
3
+ import { closestCenter, DndContext } from '@dnd-kit/core';
4
4
  import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
5
5
  import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
6
6
  import { CSS } from '@dnd-kit/utilities';
@@ -83,7 +83,7 @@ export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps
83
83
  <Trans>Columns</Trans>
84
84
  </Button>
85
85
  </DropdownMenuTrigger>
86
- <DropdownMenuContent align="end" className="w-[150px]">
86
+ <DropdownMenuContent align="end">
87
87
  <DndContext
88
88
  collisionDetection={closestCenter}
89
89
  onDragEnd={handleDragEnd}
@@ -36,6 +36,7 @@ export interface FacetedFilter {
36
36
  }
37
37
 
38
38
  interface DataTableProps<TData> {
39
+ children?: React.ReactNode;
39
40
  columns: ColumnDef<TData, any>[];
40
41
  data: TData[];
41
42
  totalItems: number;
@@ -62,6 +63,7 @@ interface DataTableProps<TData> {
62
63
  }
63
64
 
64
65
  export function DataTable<TData>({
66
+ children,
65
67
  columns,
66
68
  data,
67
69
  totalItems,
@@ -178,7 +180,7 @@ export function DataTable<TData>({
178
180
  />
179
181
  ))}
180
182
  </Suspense>
181
- <AddFilterMenu columns={table.getAllColumns()} />
183
+ {onFilterChange && <AddFilterMenu columns={table.getAllColumns()} />}
182
184
  </div>
183
185
  <div className="flex gap-1">
184
186
  {columnFilters
@@ -266,11 +268,12 @@ export function DataTable<TData>({
266
268
  </TableCell>
267
269
  </TableRow>
268
270
  )}
271
+ {children}
269
272
  </TableBody>
270
273
  </Table>
271
274
  <DataTableBulkActions bulkActions={bulkActions ?? []} table={table} />
272
275
  </div>
273
- <DataTablePagination table={table} />
276
+ {onPageChange && totalItems != null && <DataTablePagination table={table} />}
274
277
  </>
275
278
  );
276
279
  }
@@ -0,0 +1,307 @@
1
+ import { DisplayComponent } from '@/vdb/framework/component-registry/dynamic-component.js';
2
+ import { FieldInfo, getTypeFieldInfo } from '@/vdb/framework/document-introspection/get-document-structure.js';
3
+ import { api } from '@/vdb/graphql/api.js';
4
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
5
+ import { TypedDocumentNode } from '@graphql-typed-document-node/core';
6
+ import { useMutation } from '@tanstack/react-query';
7
+ import { AccessorKeyColumnDef, createColumnHelper, Row } from '@tanstack/react-table';
8
+ import { EllipsisIcon, TrashIcon } from 'lucide-react';
9
+ import { useMemo } from 'react';
10
+ import { toast } from 'sonner';
11
+ import {
12
+ AdditionalColumns,
13
+ AllItemFieldKeys,
14
+ CustomizeColumnConfig,
15
+ FacetedFilterConfig,
16
+ PaginatedListItemFields,
17
+ RowAction,
18
+ usePaginatedList,
19
+ } from '../shared/paginated-list-data-table.js';
20
+ import {
21
+ AlertDialog,
22
+ AlertDialogAction,
23
+ AlertDialogCancel,
24
+ AlertDialogContent,
25
+ AlertDialogDescription,
26
+ AlertDialogFooter,
27
+ AlertDialogHeader,
28
+ AlertDialogTitle,
29
+ AlertDialogTrigger,
30
+ } from '../ui/alert-dialog.js';
31
+ import { Button } from '../ui/button.js';
32
+ import { Checkbox } from '../ui/checkbox.js';
33
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu.js';
34
+ import { DataTableColumnHeader } from './data-table-column-header.js';
35
+
36
+ /**
37
+ * @description
38
+ * This hook is used to generate the columns for a data table, combining the fields
39
+ * from the query with the additional columns and the custom fields.
40
+ *
41
+ * It also
42
+ * - adds the row actions and the delete mutation.
43
+ * - adds the row selection column.
44
+ * - adds the custom field columns.
45
+ */
46
+ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
47
+ fields,
48
+ customizeColumns,
49
+ rowActions,
50
+ deleteMutation,
51
+ additionalColumns,
52
+ defaultColumnOrder,
53
+ facetedFilters,
54
+ includeSelectionColumn = true,
55
+ includeActionsColumn = true,
56
+ enableSorting = true,
57
+ }: Readonly<{
58
+ fields: FieldInfo[];
59
+ customizeColumns?: CustomizeColumnConfig<T>;
60
+ rowActions?: RowAction<PaginatedListItemFields<T>>[];
61
+ deleteMutation?: TypedDocumentNode<any, any>;
62
+ additionalColumns?: AdditionalColumns<T>;
63
+ defaultColumnOrder?: Array<string | number | symbol>;
64
+ facetedFilters?: FacetedFilterConfig<T>;
65
+ includeSelectionColumn?: boolean;
66
+ includeActionsColumn?: boolean;
67
+ enableSorting?: boolean;
68
+ }>) {
69
+ const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
70
+
71
+ const { columns, customFieldColumnNames } = useMemo(() => {
72
+ const columnConfigs: Array<{ fieldInfo: FieldInfo; isCustomField: boolean }> = [];
73
+ const customFieldColumnNames: string[] = [];
74
+
75
+ columnConfigs.push(
76
+ ...fields // Filter out custom fields
77
+ .filter(field => field.name !== 'customFields' && !field.type.endsWith('CustomFields'))
78
+ .map(field => ({ fieldInfo: field, isCustomField: false })),
79
+ );
80
+
81
+ const customFieldColumn = fields.find(field => field.name === 'customFields');
82
+ if (customFieldColumn && customFieldColumn.type !== 'JSON') {
83
+ const customFieldFields = getTypeFieldInfo(customFieldColumn.type);
84
+ columnConfigs.push(
85
+ ...customFieldFields.map(field => ({ fieldInfo: field, isCustomField: true })),
86
+ );
87
+ customFieldColumnNames.push(...customFieldFields.map(field => field.name));
88
+ }
89
+
90
+ const queryBasedColumns = columnConfigs.map(({ fieldInfo, isCustomField }) => {
91
+ const customConfig = customizeColumns?.[fieldInfo.name as unknown as AllItemFieldKeys<T>] ?? {};
92
+ const { header, ...customConfigRest } = customConfig;
93
+ const enableColumnFilter = fieldInfo.isScalar && !facetedFilters?.[fieldInfo.name];
94
+
95
+ return columnHelper.accessor(fieldInfo.name as any, {
96
+ id: fieldInfo.name,
97
+ meta: { fieldInfo, isCustomField },
98
+ enableColumnFilter,
99
+ enableSorting: fieldInfo.isScalar && enableSorting,
100
+ // Filtering is done on the server side, but we set this to 'equalsString' because
101
+ // otherwise the TanStack Table with apply an "auto" function which somehow
102
+ // prevents certain filters from working.
103
+ filterFn: 'equalsString',
104
+ cell: ({ cell, row }) => {
105
+ const cellValue = cell.getValue();
106
+ const value =
107
+ cellValue ??
108
+ (isCustomField ? row.original?.customFields?.[fieldInfo.name] : undefined);
109
+
110
+ if (fieldInfo.list && Array.isArray(value)) {
111
+ return value.join(', ');
112
+ }
113
+ if (
114
+ (fieldInfo.type === 'DateTime' && typeof value === 'string') ||
115
+ value instanceof Date
116
+ ) {
117
+ return <DisplayComponent id="vendure:dateTime" value={value} />;
118
+ }
119
+ if (fieldInfo.type === 'Boolean') {
120
+ if (cell.column.id === 'enabled') {
121
+ return <DisplayComponent id="vendure:booleanBadge" value={value} />;
122
+ } else {
123
+ return <DisplayComponent id="vendure:booleanCheckbox" value={value} />;
124
+ }
125
+ }
126
+ if (fieldInfo.type === 'Asset') {
127
+ return <DisplayComponent id="vendure:asset" value={value} />;
128
+ }
129
+ if (value !== null && typeof value === 'object') {
130
+ return JSON.stringify(value);
131
+ }
132
+ return value;
133
+ },
134
+ header: headerContext => {
135
+ return (
136
+ <DataTableColumnHeader headerContext={headerContext} customConfig={customConfig} />
137
+ );
138
+ },
139
+ ...customConfigRest,
140
+ });
141
+ });
142
+
143
+ let finalColumns = [...queryBasedColumns];
144
+
145
+ for (const [id, column] of Object.entries(additionalColumns ?? {})) {
146
+ if (!id) {
147
+ throw new Error('Column id is required');
148
+ }
149
+ finalColumns.push(columnHelper.accessor(id as any, { ...column, id }));
150
+ }
151
+
152
+ if (defaultColumnOrder) {
153
+ // ensure the columns with ids matching the items in defaultColumnOrder
154
+ // appear as the first columns in sequence, and leave the remainder in the
155
+ // existing order
156
+ const orderedColumns = finalColumns
157
+ .filter(column => column.id && defaultColumnOrder.includes(column.id as any))
158
+ .sort(
159
+ (a, b) =>
160
+ defaultColumnOrder.indexOf(a.id as any) - defaultColumnOrder.indexOf(b.id as any),
161
+ );
162
+ const remainingColumns = finalColumns.filter(
163
+ column => !column.id || !defaultColumnOrder.includes(column.id as any),
164
+ );
165
+ finalColumns = [...orderedColumns, ...remainingColumns];
166
+ }
167
+
168
+ if (includeActionsColumn && (rowActions || deleteMutation)) {
169
+ const rowActionColumn = getRowActions(rowActions, deleteMutation);
170
+ if (rowActionColumn) {
171
+ finalColumns.push(rowActionColumn);
172
+ }
173
+ }
174
+
175
+ if (includeSelectionColumn) {
176
+ // Add the row selection column
177
+ finalColumns.unshift({
178
+ id: 'selection',
179
+ accessorKey: 'selection',
180
+ header: ({ table }) => (
181
+ <Checkbox
182
+ className="mx-1"
183
+ checked={table.getIsAllRowsSelected()}
184
+ onCheckedChange={checked =>
185
+ table.toggleAllRowsSelected(checked === 'indeterminate' ? undefined : checked)
186
+ }
187
+ />
188
+ ),
189
+ enableColumnFilter: false,
190
+ cell: ({ row }) => {
191
+ return (
192
+ <Checkbox
193
+ className="mx-1"
194
+ checked={row.getIsSelected()}
195
+ onCheckedChange={row.getToggleSelectedHandler()}
196
+ />
197
+ );
198
+ },
199
+ });
200
+ }
201
+
202
+ return { columns: finalColumns, customFieldColumnNames };
203
+ }, [fields, customizeColumns, rowActions, deleteMutation, additionalColumns, defaultColumnOrder]);
204
+
205
+ return { columns, customFieldColumnNames };
206
+ }
207
+
208
+ function getRowActions(
209
+ rowActions?: RowAction<any>[],
210
+ deleteMutation?: TypedDocumentNode<any, any>,
211
+ ): AccessorKeyColumnDef<any> | undefined {
212
+ return {
213
+ id: 'actions',
214
+ accessorKey: 'actions',
215
+ header: () => <Trans>Actions</Trans>,
216
+ enableColumnFilter: false,
217
+ cell: ({ row }) => {
218
+ return (
219
+ <DropdownMenu>
220
+ <DropdownMenuTrigger asChild>
221
+ <Button variant="ghost" size="icon">
222
+ <EllipsisIcon />
223
+ </Button>
224
+ </DropdownMenuTrigger>
225
+ <DropdownMenuContent>
226
+ {rowActions?.map((action, index) => (
227
+ <DropdownMenuItem
228
+ onClick={() => action.onClick?.(row)}
229
+ key={`${action.label}-${index}`}
230
+ >
231
+ {action.label}
232
+ </DropdownMenuItem>
233
+ ))}
234
+ {deleteMutation && (
235
+ <DeleteMutationRowAction deleteMutation={deleteMutation} row={row} />
236
+ )}
237
+ </DropdownMenuContent>
238
+ </DropdownMenu>
239
+ );
240
+ },
241
+ };
242
+ }
243
+
244
+ function DeleteMutationRowAction({
245
+ deleteMutation,
246
+ row,
247
+ }: Readonly<{
248
+ deleteMutation: TypedDocumentNode<any, any>;
249
+ row: Row<{ id: string }>;
250
+ }>) {
251
+ const { refetchPaginatedList } = usePaginatedList();
252
+ const { i18n } = useLingui();
253
+ const { mutate: deleteMutationFn } = useMutation({
254
+ mutationFn: api.mutate(deleteMutation),
255
+ onSuccess: (result: { [key: string]: { result: 'DELETED' | 'NOT_DELETED'; message: string } }) => {
256
+ const unwrappedResult = Object.values(result)[0];
257
+ if (unwrappedResult.result === 'DELETED') {
258
+ refetchPaginatedList();
259
+ toast.success(i18n.t('Deleted successfully'));
260
+ } else {
261
+ toast.error(i18n.t('Failed to delete'), {
262
+ description: unwrappedResult.message,
263
+ });
264
+ }
265
+ },
266
+ onError: (err: Error) => {
267
+ toast.error(i18n.t('Failed to delete'), {
268
+ description: err.message,
269
+ });
270
+ },
271
+ });
272
+ return (
273
+ <AlertDialog>
274
+ <AlertDialogTrigger asChild>
275
+ <DropdownMenuItem onSelect={e => e.preventDefault()}>
276
+ <div className="flex items-center gap-2 text-destructive">
277
+ <TrashIcon className="w-4 h-4 text-destructive" />
278
+ <Trans>Delete</Trans>
279
+ </div>
280
+ </DropdownMenuItem>
281
+ </AlertDialogTrigger>
282
+ <AlertDialogContent>
283
+ <AlertDialogHeader>
284
+ <AlertDialogTitle>
285
+ <Trans>Confirm deletion</Trans>
286
+ </AlertDialogTitle>
287
+ <AlertDialogDescription>
288
+ <Trans>
289
+ Are you sure you want to delete this item? This action cannot be undone.
290
+ </Trans>
291
+ </AlertDialogDescription>
292
+ </AlertDialogHeader>
293
+ <AlertDialogFooter>
294
+ <AlertDialogCancel>
295
+ <Trans>Cancel</Trans>
296
+ </AlertDialogCancel>
297
+ <AlertDialogAction
298
+ onClick={() => deleteMutationFn({ id: row.original.id })}
299
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
300
+ >
301
+ <Trans>Delete</Trans>
302
+ </AlertDialogAction>
303
+ </AlertDialogFooter>
304
+ </AlertDialogContent>
305
+ </AlertDialog>
306
+ );
307
+ }