@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
@@ -1,51 +1,26 @@
1
- import { DataTableColumnHeader } from '@/vdb/components/data-table/data-table-column-header.js';
2
1
  import { DataTable, FacetedFilter } from '@/vdb/components/data-table/data-table.js';
3
2
  import {
4
- FieldInfo,
5
- getObjectPathToPaginatedList,
6
- getTypeFieldInfo,
3
+ getObjectPathToPaginatedList
7
4
  } from '@/vdb/framework/document-introspection/get-document-structure.js';
8
5
  import { useListQueryFields } from '@/vdb/framework/document-introspection/hooks.js';
9
6
  import { api } from '@/vdb/graphql/api.js';
10
- import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
7
+ import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query';
11
8
  import { useDebounce } from '@uidotdev/usehooks';
12
9
 
13
- import {
14
- AlertDialog,
15
- AlertDialogAction,
16
- AlertDialogCancel,
17
- AlertDialogContent,
18
- AlertDialogDescription,
19
- AlertDialogFooter,
20
- AlertDialogHeader,
21
- AlertDialogTitle,
22
- AlertDialogTrigger,
23
- } from '@/vdb/components/ui/alert-dialog.js';
24
- import {
25
- DropdownMenu,
26
- DropdownMenuContent,
27
- DropdownMenuItem,
28
- DropdownMenuTrigger,
29
- } from '@/vdb/components/ui/dropdown-menu.js';
30
- import { DisplayComponent } from '@/vdb/framework/component-registry/dynamic-component.js';
31
10
  import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
32
11
  import { ResultOf } from '@/vdb/graphql/graphql.js';
33
12
  import { useExtendedListQuery } from '@/vdb/hooks/use-extended-list-query.js';
34
- import { Trans, useLingui } from '@/vdb/lib/trans.js';
35
13
  import { TypedDocumentNode } from '@graphql-typed-document-node/core';
36
14
  import {
37
15
  ColumnFiltersState,
38
16
  ColumnSort,
39
- createColumnHelper,
40
17
  SortingState,
41
- Table,
18
+ Table
42
19
  } from '@tanstack/react-table';
43
- import { AccessorKeyColumnDef, ColumnDef, Row, TableOptions, VisibilityState } from '@tanstack/table-core';
44
- import { EllipsisIcon, TrashIcon } from 'lucide-react';
45
- import React, { useMemo } from 'react';
46
- import { toast } from 'sonner';
47
- import { Button } from '../ui/button.js';
48
- import { Checkbox } from '../ui/checkbox.js';
20
+ import { ColumnDef, Row, TableOptions, VisibilityState } from '@tanstack/table-core';
21
+ import React from 'react';
22
+ import { getColumnVisibility } from '../data-table/data-table-utils.js';
23
+ import { useGeneratedColumns } from '../data-table/use-generated-columns.js';
49
24
 
50
25
  // Type that identifies a paginated list structure (has items array and totalItems)
51
26
  type IsPaginatedList<T> = T extends { items: any[]; totalItems: number } ? true : false;
@@ -345,139 +320,14 @@ export function PaginatedListDataTable<
345
320
  listData = listData?.[path];
346
321
  }
347
322
 
348
- const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
349
-
350
- const { columns, customFieldColumnNames } = useMemo(() => {
351
- const columnConfigs: Array<{ fieldInfo: FieldInfo; isCustomField: boolean }> = [];
352
- const customFieldColumnNames: string[] = [];
353
-
354
- columnConfigs.push(
355
- ...fields // Filter out custom fields
356
- .filter(field => field.name !== 'customFields' && !field.type.endsWith('CustomFields'))
357
- .map(field => ({ fieldInfo: field, isCustomField: false })),
358
- );
359
-
360
- const customFieldColumn = fields.find(field => field.name === 'customFields');
361
- if (customFieldColumn && customFieldColumn.type !== 'JSON') {
362
- const customFieldFields = getTypeFieldInfo(customFieldColumn.type);
363
- columnConfigs.push(
364
- ...customFieldFields.map(field => ({ fieldInfo: field, isCustomField: true })),
365
- );
366
- customFieldColumnNames.push(...customFieldFields.map(field => field.name));
367
- }
368
-
369
- const queryBasedColumns = columnConfigs.map(({ fieldInfo, isCustomField }) => {
370
- const customConfig = customizeColumns?.[fieldInfo.name as unknown as AllItemFieldKeys<T>] ?? {};
371
- const { header, ...customConfigRest } = customConfig;
372
- const enableColumnFilter = fieldInfo.isScalar && !facetedFilters?.[fieldInfo.name];
373
-
374
- return columnHelper.accessor(fieldInfo.name as any, {
375
- id: fieldInfo.name,
376
- meta: { fieldInfo, isCustomField },
377
- enableColumnFilter,
378
- enableSorting: fieldInfo.isScalar,
379
- // Filtering is done on the server side, but we set this to 'equalsString' because
380
- // otherwise the TanStack Table with apply an "auto" function which somehow
381
- // prevents certain filters from working.
382
- filterFn: 'equalsString',
383
- cell: ({ cell, row }) => {
384
- const cellValue = cell.getValue();
385
- const value =
386
- cellValue ??
387
- (isCustomField ? row.original?.customFields?.[fieldInfo.name] : undefined);
388
-
389
- if (fieldInfo.list && Array.isArray(value)) {
390
- return value.join(', ');
391
- }
392
- if (
393
- (fieldInfo.type === 'DateTime' && typeof value === 'string') ||
394
- value instanceof Date
395
- ) {
396
- return <DisplayComponent id="vendure:dateTime" value={value} />;
397
- }
398
- if (fieldInfo.type === 'Boolean') {
399
- if (cell.column.id === 'enabled') {
400
- return <DisplayComponent id="vendure:booleanBadge" value={value} />;
401
- } else {
402
- return <DisplayComponent id="vendure:booleanCheckbox" value={value} />;
403
- }
404
- }
405
- if (fieldInfo.type === 'Asset') {
406
- return <DisplayComponent id="vendure:asset" value={value} />;
407
- }
408
- if (value !== null && typeof value === 'object') {
409
- return JSON.stringify(value);
410
- }
411
- return value;
412
- },
413
- header: headerContext => {
414
- return (
415
- <DataTableColumnHeader headerContext={headerContext} customConfig={customConfig} />
416
- );
417
- },
418
- ...customConfigRest,
419
- });
420
- });
421
-
422
- let finalColumns = [...queryBasedColumns];
423
-
424
- for (const [id, column] of Object.entries(additionalColumns ?? {})) {
425
- if (!id) {
426
- throw new Error('Column id is required');
427
- }
428
- finalColumns.push(columnHelper.accessor(id as any, { ...column, id }));
429
- }
430
-
431
- if (defaultColumnOrder) {
432
- // ensure the columns with ids matching the items in defaultColumnOrder
433
- // appear as the first columns in sequence, and leave the remainder in the
434
- // existing order
435
- const orderedColumns = finalColumns
436
- .filter(column => column.id && defaultColumnOrder.includes(column.id as any))
437
- .sort(
438
- (a, b) =>
439
- defaultColumnOrder.indexOf(a.id as any) - defaultColumnOrder.indexOf(b.id as any),
440
- );
441
- const remainingColumns = finalColumns.filter(
442
- column => !column.id || !defaultColumnOrder.includes(column.id as any),
443
- );
444
- finalColumns = [...orderedColumns, ...remainingColumns];
445
- }
446
-
447
- if (rowActions || deleteMutation) {
448
- const rowActionColumn = getRowActions(rowActions, deleteMutation);
449
- if (rowActionColumn) {
450
- finalColumns.push(rowActionColumn);
451
- }
452
- }
453
-
454
- // Add the row selection column
455
- finalColumns.unshift({
456
- id: 'selection',
457
- accessorKey: 'selection',
458
- header: ({ table }) => (
459
- <Checkbox
460
- className="mx-1"
461
- checked={table.getIsAllRowsSelected()}
462
- onCheckedChange={checked =>
463
- table.toggleAllRowsSelected(checked === 'indeterminate' ? undefined : checked)
464
- }
465
- />
466
- ),
467
- enableColumnFilter: false,
468
- cell: ({ row }) => {
469
- return (
470
- <Checkbox
471
- className="mx-1"
472
- checked={row.getIsSelected()}
473
- onCheckedChange={row.getToggleSelectedHandler()}
474
- />
475
- );
476
- },
477
- });
478
-
479
- return { columns: finalColumns, customFieldColumnNames };
480
- }, [fields, customizeColumns, rowActions]);
323
+ const { columns, customFieldColumnNames } = useGeneratedColumns({
324
+ fields,
325
+ customizeColumns,
326
+ rowActions,
327
+ deleteMutation,
328
+ additionalColumns,
329
+ defaultColumnOrder,
330
+ });
481
331
 
482
332
  const columnVisibility = getColumnVisibility(fields, defaultVisibility, customFieldColumnNames);
483
333
  const transformedData =
@@ -509,124 +359,3 @@ export function PaginatedListDataTable<
509
359
  );
510
360
  }
511
361
 
512
- function getRowActions(
513
- rowActions?: RowAction<any>[],
514
- deleteMutation?: TypedDocumentNode<any, any>,
515
- ): AccessorKeyColumnDef<any> | undefined {
516
- return {
517
- id: 'actions',
518
- accessorKey: 'actions',
519
- header: 'Actions',
520
- enableColumnFilter: false,
521
- cell: ({ row }) => {
522
- return (
523
- <DropdownMenu>
524
- <DropdownMenuTrigger asChild>
525
- <Button variant="ghost" size="icon">
526
- <EllipsisIcon />
527
- </Button>
528
- </DropdownMenuTrigger>
529
- <DropdownMenuContent>
530
- {rowActions?.map((action, index) => (
531
- <DropdownMenuItem onClick={() => action.onClick?.(row)} key={index}>
532
- {action.label}
533
- </DropdownMenuItem>
534
- ))}
535
- {deleteMutation && (
536
- <DeleteMutationRowAction deleteMutation={deleteMutation} row={row} />
537
- )}
538
- </DropdownMenuContent>
539
- </DropdownMenu>
540
- );
541
- },
542
- };
543
- }
544
-
545
- function DeleteMutationRowAction({
546
- deleteMutation,
547
- row,
548
- }: {
549
- deleteMutation: TypedDocumentNode<any, any>;
550
- row: Row<{ id: string }>;
551
- }) {
552
- const { refetchPaginatedList } = usePaginatedList();
553
- const { i18n } = useLingui();
554
- const { mutate: deleteMutationFn } = useMutation({
555
- mutationFn: api.mutate(deleteMutation),
556
- onSuccess: (result: { [key: string]: { result: 'DELETED' | 'NOT_DELETED'; message: string } }) => {
557
- const unwrappedResult = Object.values(result)[0];
558
- if (unwrappedResult.result === 'DELETED') {
559
- refetchPaginatedList();
560
- toast.success(i18n.t('Deleted successfully'));
561
- } else {
562
- toast.error(i18n.t('Failed to delete'), {
563
- description: unwrappedResult.message,
564
- });
565
- }
566
- },
567
- onError: (err: Error) => {
568
- toast.error(i18n.t('Failed to delete'), {
569
- description: err.message,
570
- });
571
- },
572
- });
573
- return (
574
- <AlertDialog>
575
- <AlertDialogTrigger asChild>
576
- <DropdownMenuItem onSelect={e => e.preventDefault()}>
577
- <div className="flex items-center gap-2 text-destructive">
578
- <TrashIcon className="w-4 h-4 text-destructive" />
579
- <Trans>Delete</Trans>
580
- </div>
581
- </DropdownMenuItem>
582
- </AlertDialogTrigger>
583
- <AlertDialogContent>
584
- <AlertDialogHeader>
585
- <AlertDialogTitle>
586
- <Trans>Confirm deletion</Trans>
587
- </AlertDialogTitle>
588
- <AlertDialogDescription>
589
- <Trans>
590
- Are you sure you want to delete this item? This action cannot be undone.
591
- </Trans>
592
- </AlertDialogDescription>
593
- </AlertDialogHeader>
594
- <AlertDialogFooter>
595
- <AlertDialogCancel>
596
- <Trans>Cancel</Trans>
597
- </AlertDialogCancel>
598
- <AlertDialogAction
599
- onClick={() => deleteMutationFn({ id: row.original.id })}
600
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
601
- >
602
- <Trans>Delete</Trans>
603
- </AlertDialogAction>
604
- </AlertDialogFooter>
605
- </AlertDialogContent>
606
- </AlertDialog>
607
- );
608
- }
609
-
610
- /**
611
- * Returns the default column visibility configuration.
612
- */
613
- function getColumnVisibility(
614
- fields: FieldInfo[],
615
- defaultVisibility?: Record<string, boolean | undefined>,
616
- customFieldColumnNames?: string[],
617
- ): Record<string, boolean> {
618
- const allDefaultsTrue = defaultVisibility && Object.values(defaultVisibility).every(v => v === true);
619
- const allDefaultsFalse = defaultVisibility && Object.values(defaultVisibility).every(v => v === false);
620
- return {
621
- id: false,
622
- createdAt: false,
623
- updatedAt: false,
624
- ...(allDefaultsTrue ? { ...Object.fromEntries(fields.map(f => [f.name, false])) } : {}),
625
- ...(allDefaultsFalse ? { ...Object.fromEntries(fields.map(f => [f.name, true])) } : {}),
626
- // Make custom fields hidden by default unless overridden
627
- ...(customFieldColumnNames
628
- ? { ...Object.fromEntries(customFieldColumnNames.map(f => [f, false])) }
629
- : {}),
630
- ...defaultVisibility,
631
- };
632
- }
@@ -8,7 +8,7 @@ import {
8
8
  } from '@/vdb/components/ui/command.js';
9
9
  import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
10
10
  import { api } from '@/vdb/graphql/api.js';
11
- import { assetFragment } from '@/vdb/graphql/fragments.js';
11
+ import { AssetFragment, assetFragment } from '@/vdb/graphql/fragments.js';
12
12
  import { graphql } from '@/vdb/graphql/graphql.js';
13
13
  import { useQuery } from '@tanstack/react-query';
14
14
  import { useDebounce } from '@uidotdev/usehooks';
@@ -28,6 +28,13 @@ const productVariantListDocument = graphql(
28
28
  featuredAsset {
29
29
  ...Asset
30
30
  }
31
+ price
32
+ priceWithTax
33
+ product {
34
+ featuredAsset {
35
+ ...Asset
36
+ }
37
+ }
31
38
  }
32
39
  totalItems
33
40
  }
@@ -37,10 +44,17 @@ const productVariantListDocument = graphql(
37
44
  );
38
45
 
39
46
  export interface ProductVariantSelectorProps {
40
- onProductVariantIdChange: (productVariantId: string) => void;
47
+ onProductVariantSelect: (variant: {
48
+ productVariantId: string;
49
+ productVariantName: string;
50
+ sku: string;
51
+ productAsset: AssetFragment | null;
52
+ price?: number;
53
+ priceWithTax?: number;
54
+ }) => void;
41
55
  }
42
56
 
43
- export function ProductVariantSelector({ onProductVariantIdChange }: Readonly<ProductVariantSelectorProps>) {
57
+ export function ProductVariantSelector({ onProductVariantSelect }: Readonly<ProductVariantSelectorProps>) {
44
58
  const [search, setSearch] = useState('');
45
59
  const [open, setOpen] = useState(false);
46
60
  const debouncedSearch = useDebounce(search, 500);
@@ -85,7 +99,17 @@ export function ProductVariantSelector({ onProductVariantIdChange }: Readonly<Pr
85
99
  key={variant.id}
86
100
  value={variant.id}
87
101
  onSelect={() => {
88
- onProductVariantIdChange(variant.id);
102
+ onProductVariantSelect({
103
+ productVariantId: variant.id,
104
+ productVariantName: variant.name,
105
+ sku: variant.sku,
106
+ productAsset:
107
+ variant.featuredAsset ??
108
+ variant.product.featuredAsset ??
109
+ null,
110
+ price: variant.price,
111
+ priceWithTax: variant.priceWithTax,
112
+ });
89
113
  setOpen(false);
90
114
  }}
91
115
  className="flex items-center gap-2 p-2"
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { COMPONENT_REGISTRY, useComponentRegistry } from "./component-registry.js";
2
+ import { useComponentRegistry } from "./component-registry.js";
3
3
 
4
4
  export type DisplayComponentProps<
5
5
  T extends keyof (typeof COMPONENT_REGISTRY)['dataDisplay'] | string,
@@ -33,7 +33,7 @@ export type InputComponentProps<
33
33
  */
34
34
  export function DisplayComponent<
35
35
  T extends keyof (typeof COMPONENT_REGISTRY)['dataDisplay'] | string,
36
- >(props: DisplayComponentProps<T>): React.ReactNode {
36
+ >(props: Readonly<DisplayComponentProps<T>>): React.ReactNode {
37
37
  const { getDisplayComponent } = useComponentRegistry();
38
38
  const Component = getDisplayComponent(props.id);
39
39
  if (!Component) {
@@ -45,7 +45,7 @@ export function DisplayComponent<
45
45
 
46
46
  export function InputComponent<
47
47
  T extends keyof (typeof COMPONENT_REGISTRY)['dataInput'] | string,
48
- >(props: InputComponentProps<T>): React.ReactNode {
48
+ >(props: Readonly<InputComponentProps<T>>): React.ReactNode {
49
49
  const { getInputComponent } = useComponentRegistry();
50
50
  const Component = getInputComponent(props.id);
51
51
  if (!Component) {