@vendure/dashboard 3.4.3-master-202509120226 → 3.4.3-master-202509190229

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.4.3-master-202509120226",
4
+ "version": "3.4.3-master-202509190229",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -94,14 +94,18 @@
94
94
  "@tanstack/react-table": "^8.21.2",
95
95
  "@tanstack/router-devtools": "^1.105.0",
96
96
  "@tanstack/router-plugin": "^1.105.0",
97
- "@tiptap/pm": "^2.11.5",
98
- "@tiptap/react": "^2.11.5",
99
- "@tiptap/starter-kit": "^2.11.5",
97
+ "@tiptap/extension-floating-menu": "^3.4.4",
98
+ "@tiptap/extension-image": "^3.4.4",
99
+ "@tiptap/extension-table": "^3.4.4",
100
+ "@tiptap/extension-text-style": "^3.4.4",
101
+ "@tiptap/pm": "^3.4.4",
102
+ "@tiptap/react": "^3.4.4",
103
+ "@tiptap/starter-kit": "^3.4.4",
100
104
  "@types/react": "^19.0.10",
101
105
  "@types/react-dom": "^19.0.4",
102
106
  "@uidotdev/usehooks": "^2.4.1",
103
- "@vendure/common": "^3.4.3-master-202509120226",
104
- "@vendure/core": "^3.4.3-master-202509120226",
107
+ "@vendure/common": "^3.4.3-master-202509190229",
108
+ "@vendure/core": "^3.4.3-master-202509190229",
105
109
  "@vitejs/plugin-react": "^4.3.4",
106
110
  "acorn": "^8.11.3",
107
111
  "acorn-walk": "^8.3.2",
@@ -152,5 +156,5 @@
152
156
  "lightningcss-linux-arm64-musl": "^1.29.3",
153
157
  "lightningcss-linux-x64-musl": "^1.29.1"
154
158
  },
155
- "gitHead": "690fb9e8ac0aa88be3555bb57a54386debc95bfe"
159
+ "gitHead": "1a94626a2e07d33d881a751ff394f8351c3521b4"
156
160
  }
@@ -1,4 +1,5 @@
1
1
  import { useMutation } from '@tanstack/react-query';
2
+ import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
2
3
  import { CopyIcon } from 'lucide-react';
3
4
  import { useState } from 'react';
4
5
  import { toast } from 'sonner';
@@ -8,13 +9,13 @@ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-ta
8
9
  import { api } from '@/vdb/graphql/api.js';
9
10
  import { duplicateEntityDocument } from '@/vdb/graphql/common-operations.js';
10
11
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
12
+ import { DuplicateEntityDialog } from './duplicate-entity-dialog.js';
11
13
 
12
14
  interface DuplicateBulkActionProps {
13
15
  entityType: 'Product' | 'Collection' | 'Facet' | 'Promotion';
14
16
  duplicatorCode: string;
15
- duplicatorArguments?: Array<{ name: string; value: string }>;
16
17
  requiredPermissions: string[];
17
- entityName: string; // For display purposes in error messages
18
+ entityName: string;
18
19
  onSuccess?: () => void;
19
20
  selection: any[];
20
21
  table: any;
@@ -23,23 +24,28 @@ interface DuplicateBulkActionProps {
23
24
  export function DuplicateBulkAction({
24
25
  entityType,
25
26
  duplicatorCode,
26
- duplicatorArguments = [],
27
27
  requiredPermissions,
28
28
  entityName,
29
29
  onSuccess,
30
30
  selection,
31
31
  table,
32
- }: DuplicateBulkActionProps) {
32
+ }: Readonly<DuplicateBulkActionProps>) {
33
33
  const { refetchPaginatedList } = usePaginatedList();
34
34
  const { i18n } = useLingui();
35
35
  const [isDuplicating, setIsDuplicating] = useState(false);
36
36
  const [progress, setProgress] = useState({ completed: 0, total: 0 });
37
+ const [dialogOpen, setDialogOpen] = useState(false);
37
38
 
38
39
  const { mutateAsync } = useMutation({
39
40
  mutationFn: api.mutate(duplicateEntityDocument),
40
41
  });
41
42
 
42
- const handleDuplicate = async () => {
43
+ const handleStartDuplication = () => {
44
+ if (isDuplicating) return;
45
+ setDialogOpen(true);
46
+ };
47
+
48
+ const handleConfirmDuplication = async (duplicatorInput: ConfigurableOperationInput) => {
43
49
  if (isDuplicating) return;
44
50
 
45
51
  setIsDuplicating(true);
@@ -61,10 +67,7 @@ export function DuplicateBulkAction({
61
67
  input: {
62
68
  entityName: entityType,
63
69
  entityId: entity.id,
64
- duplicatorInput: {
65
- code: duplicatorCode,
66
- arguments: duplicatorArguments,
67
- },
70
+ duplicatorInput,
68
71
  },
69
72
  });
70
73
 
@@ -116,19 +119,30 @@ export function DuplicateBulkAction({
116
119
  };
117
120
 
118
121
  return (
119
- <DataTableBulkActionItem
120
- requiresPermission={requiredPermissions}
121
- onClick={handleDuplicate}
122
- label={
123
- isDuplicating ? (
124
- <Trans>
125
- Duplicating... ({progress.completed}/{progress.total})
126
- </Trans>
127
- ) : (
128
- <Trans>Duplicate</Trans>
129
- )
130
- }
131
- icon={CopyIcon}
132
- />
122
+ <>
123
+ <DataTableBulkActionItem
124
+ requiresPermission={requiredPermissions}
125
+ onClick={handleStartDuplication}
126
+ label={
127
+ isDuplicating ? (
128
+ <Trans>
129
+ Duplicating... ({progress.completed}/{progress.total})
130
+ </Trans>
131
+ ) : (
132
+ <Trans>Duplicate</Trans>
133
+ )
134
+ }
135
+ icon={CopyIcon}
136
+ />
137
+ <DuplicateEntityDialog
138
+ open={dialogOpen}
139
+ onOpenChange={setDialogOpen}
140
+ entityType={entityType}
141
+ entityName={entityName}
142
+ entities={selection}
143
+ duplicatorCode={duplicatorCode}
144
+ onConfirm={handleConfirmDuplication}
145
+ />
146
+ </>
133
147
  );
134
148
  }
@@ -0,0 +1,117 @@
1
+ import { ConfigurableOperationInput as ConfigurableOperationInputComponent } from '@/vdb/components/shared/configurable-operation-input.js';
2
+ import { Button } from '@/vdb/components/ui/button.js';
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from '@/vdb/components/ui/dialog.js';
10
+ import { api } from '@/vdb/graphql/api.js';
11
+ import { getEntityDuplicatorsDocument } from '@/vdb/graphql/common-operations.js';
12
+ import { Trans } from '@/vdb/lib/trans.js';
13
+ import { useQuery } from '@tanstack/react-query';
14
+ import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
15
+ import React, { useState } from 'react';
16
+
17
+ interface DuplicateEntityDialogProps {
18
+ open: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ entityType: 'Product' | 'Collection' | 'Facet' | 'Promotion';
21
+ entityName: string;
22
+ entities: Array<{ id: string; name?: string }>;
23
+ duplicatorCode: string;
24
+ onConfirm: (duplicatorInput: ConfigurableOperationInput) => void;
25
+ }
26
+
27
+ export function DuplicateEntityDialog({
28
+ open,
29
+ onOpenChange,
30
+ entityType,
31
+ entityName,
32
+ duplicatorCode,
33
+ onConfirm,
34
+ }: Readonly<DuplicateEntityDialogProps>) {
35
+ const [selectedDuplicator, setSelectedDuplicator] = useState<ConfigurableOperationInput | undefined>();
36
+
37
+ const { data } = useQuery({
38
+ queryKey: ['entityDuplicators'],
39
+ queryFn: () => api.query(getEntityDuplicatorsDocument),
40
+ staleTime: 1000 * 60 * 60 * 5,
41
+ });
42
+
43
+ // Find the duplicator that matches the provided code and supports this entity type
44
+ const matchingDuplicator = data?.entityDuplicators?.find(
45
+ duplicator => duplicator.code === duplicatorCode && duplicator.forEntities.includes(entityType),
46
+ );
47
+
48
+ // Auto-initialize the duplicator when found
49
+ React.useEffect(() => {
50
+ if (matchingDuplicator && !selectedDuplicator) {
51
+ setSelectedDuplicator({
52
+ code: matchingDuplicator.code,
53
+ arguments:
54
+ matchingDuplicator.args?.map(arg => ({
55
+ name: arg.name,
56
+ value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
57
+ })) || [],
58
+ });
59
+ }
60
+ }, [matchingDuplicator, selectedDuplicator]);
61
+
62
+ const onDuplicatorValueChange = (newVal: ConfigurableOperationInput) => {
63
+ setSelectedDuplicator(newVal);
64
+ };
65
+
66
+ const handleConfirm = () => {
67
+ if (!selectedDuplicator) return;
68
+ onConfirm(selectedDuplicator);
69
+ onOpenChange(false);
70
+ setSelectedDuplicator(undefined);
71
+ };
72
+
73
+ const handleCancel = () => {
74
+ onOpenChange(false);
75
+ setSelectedDuplicator(undefined);
76
+ };
77
+
78
+ return (
79
+ <Dialog open={open} onOpenChange={onOpenChange}>
80
+ <DialogContent className="sm:max-w-lg">
81
+ <DialogHeader>
82
+ <DialogTitle>
83
+ <Trans>Duplicate {entityName.toLowerCase()}s</Trans>
84
+ </DialogTitle>
85
+ </DialogHeader>
86
+
87
+ <div className="space-y-4">
88
+ {selectedDuplicator && matchingDuplicator && (
89
+ <ConfigurableOperationInputComponent
90
+ operationDefinition={matchingDuplicator}
91
+ value={selectedDuplicator}
92
+ onChange={onDuplicatorValueChange}
93
+ removable={false}
94
+ />
95
+ )}
96
+
97
+ {!matchingDuplicator && (
98
+ <div className="text-sm text-muted-foreground">
99
+ <Trans>
100
+ No duplicator found with code "{duplicatorCode}" for {entityName}s
101
+ </Trans>
102
+ </div>
103
+ )}
104
+ </div>
105
+
106
+ <DialogFooter>
107
+ <Button variant="outline" onClick={handleCancel}>
108
+ <Trans>Cancel</Trans>
109
+ </Button>
110
+ <Button onClick={handleConfirm} disabled={!selectedDuplicator}>
111
+ <Trans>Duplicate</Trans>
112
+ </Button>
113
+ </DialogFooter>
114
+ </DialogContent>
115
+ </Dialog>
116
+ );
117
+ }
@@ -27,6 +27,7 @@ export const collectionListDocument = graphql(
27
27
  }
28
28
  children {
29
29
  id
30
+ name
30
31
  }
31
32
  position
32
33
  isPrivate
@@ -13,6 +13,7 @@ import { ResultOf } from 'gql.tada';
13
13
  import { Folder, FolderOpen, FolderTreeIcon, PlusIcon } from 'lucide-react';
14
14
  import { useState } from 'react';
15
15
 
16
+ import { Badge } from '@/vdb/components/ui/badge.js';
16
17
  import { collectionListDocument, deleteCollectionDocument } from './collections.graphql.js';
17
18
  import {
18
19
  AssignCollectionsToChannelBulkAction,
@@ -150,6 +151,26 @@ function CollectionListPage() {
150
151
  );
151
152
  },
152
153
  },
154
+ children: {
155
+ cell: ({ row }) => {
156
+ const children = row.original.children ?? [];
157
+ const count = children.length;
158
+ const maxDisplay = 5;
159
+ const leftOver = Math.max(count - maxDisplay, 0);
160
+ return (
161
+ <div className="flex flex-wrap gap-2">
162
+ {children.slice(0, maxDisplay).map(child => (
163
+ <Badge variant="outline">{child.name}</Badge>
164
+ ))}
165
+ {leftOver > 0 ? (
166
+ <Badge variant="outline">
167
+ <Trans>+ {leftOver} more</Trans>
168
+ </Badge>
169
+ ) : null}
170
+ </div>
171
+ );
172
+ },
173
+ },
153
174
  }}
154
175
  defaultColumnOrder={[
155
176
  'featuredAsset',
@@ -14,6 +14,7 @@ export const productVariantListDocument = graphql(
14
14
  }
15
15
  name
16
16
  sku
17
+ enabled
17
18
  currencyCode
18
19
  price
19
20
  priceWithTax
@@ -9,6 +9,12 @@ import { graphql } from '@/vdb/graphql/graphql.js';
9
9
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
10
10
  import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
11
11
  import { useState } from 'react';
12
+ import {
13
+ AssignFacetValuesToProductVariantsBulkAction,
14
+ AssignProductVariantsToChannelBulkAction,
15
+ DeleteProductVariantsBulkAction,
16
+ RemoveProductVariantsFromChannelBulkAction,
17
+ } from '../../_product-variants/components/product-variant-bulk-actions.js';
12
18
  import { productVariantListDocument } from '../products.graphql.js';
13
19
 
14
20
  export const deleteProductVariantDocument = graphql(`
@@ -47,9 +53,31 @@ export function ProductVariantsTable({
47
53
  productId,
48
54
  })}
49
55
  defaultVisibility={{
50
- id: false,
51
- currencyCode: false,
56
+ featuredAsset: true,
57
+ name: true,
58
+ enabled: true,
59
+ price: true,
60
+ priceWithTax: true,
61
+ stockLevels: true,
52
62
  }}
63
+ bulkActions={[
64
+ {
65
+ component: AssignProductVariantsToChannelBulkAction,
66
+ order: 100,
67
+ },
68
+ {
69
+ component: RemoveProductVariantsFromChannelBulkAction,
70
+ order: 200,
71
+ },
72
+ {
73
+ component: AssignFacetValuesToProductVariantsBulkAction,
74
+ order: 300,
75
+ },
76
+ {
77
+ component: DeleteProductVariantsBulkAction,
78
+ order: 400,
79
+ },
80
+ ]}
53
81
  customizeColumns={{
54
82
  name: {
55
83
  header: 'Variant name',
@@ -61,25 +61,34 @@ export const productDetailFragment = graphql(
61
61
  [assetFragment],
62
62
  );
63
63
 
64
- export const productVariantListDocument = graphql(`
65
- query ProductVariantList($options: ProductVariantListOptions, $productId: ID) {
66
- productVariants(options: $options, productId: $productId) {
67
- items {
68
- id
69
- name
70
- sku
71
- currencyCode
72
- price
73
- priceWithTax
74
- stockLevels {
75
- stockOnHand
76
- stockAllocated
64
+ export const productVariantListDocument = graphql(
65
+ `
66
+ query ProductVariantList($options: ProductVariantListOptions, $productId: ID) {
67
+ productVariants(options: $options, productId: $productId) {
68
+ items {
69
+ id
70
+ createdAt
71
+ updatedAt
72
+ featuredAsset {
73
+ ...Asset
74
+ }
75
+ name
76
+ sku
77
+ enabled
78
+ currencyCode
79
+ price
80
+ priceWithTax
81
+ stockLevels {
82
+ stockOnHand
83
+ stockAllocated
84
+ }
77
85
  }
86
+ totalItems
78
87
  }
79
- totalItems
80
88
  }
81
- }
82
- `);
89
+ `,
90
+ [assetFragment],
91
+ );
83
92
 
84
93
  export const productDetailDocument = graphql(
85
94
  `
@@ -1,26 +1,6 @@
1
1
  import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
2
2
  import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
3
- import TextStyle from '@tiptap/extension-text-style';
4
- import { BubbleMenu, Editor, EditorContent, useEditor } from '@tiptap/react';
5
- import StarterKit from '@tiptap/starter-kit';
6
- import { BoldIcon, ItalicIcon, StrikethroughIcon } from 'lucide-react';
7
- import { useLayoutEffect, useRef } from 'react';
8
- import { Button } from '../ui/button.js';
9
-
10
- // define your extension array
11
- const extensions = [
12
- TextStyle.configure(),
13
- StarterKit.configure({
14
- bulletList: {
15
- keepMarks: true,
16
- keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
17
- },
18
- orderedList: {
19
- keepMarks: true,
20
- keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
21
- },
22
- }),
23
- ];
3
+ import { RichTextEditor } from '../shared/rich-text-editor/rich-text-editor.js';
24
4
 
25
5
  /**
26
6
  * @description
@@ -31,99 +11,6 @@ const extensions = [
31
11
  */
32
12
  export function RichTextInput({ value, onChange, fieldDef }: Readonly<DashboardFormComponentProps>) {
33
13
  const readOnly = isReadonlyField(fieldDef);
34
- const isInternalUpdate = useRef(false);
35
-
36
- const editor = useEditor({
37
- parseOptions: {
38
- preserveWhitespace: 'full',
39
- },
40
- extensions: extensions,
41
- content: value,
42
- editable: !readOnly,
43
- onUpdate: ({ editor }) => {
44
- if (!readOnly) {
45
- isInternalUpdate.current = true;
46
- console.log('onUpdate');
47
- const newValue = editor.getHTML();
48
- if (value !== newValue) {
49
- onChange(newValue);
50
- }
51
- }
52
- },
53
- editorProps: {
54
- attributes: {
55
- class: `border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/10 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[500px] overflow-y-auto ${readOnly ? 'cursor-not-allowed opacity-50' : ''}`,
56
- },
57
- },
58
- });
59
-
60
- useLayoutEffect(() => {
61
- if (editor && !isInternalUpdate.current) {
62
- const currentContent = editor.getHTML();
63
- if (currentContent !== value) {
64
- const { from, to } = editor.state.selection;
65
- editor.commands.setContent(value, false);
66
- editor.commands.setTextSelection({ from, to });
67
- }
68
- }
69
- isInternalUpdate.current = false;
70
- }, [value, editor]);
71
-
72
- // Update editor's editable state when disabled prop changes
73
- useLayoutEffect(() => {
74
- if (editor) {
75
- editor.setEditable(!readOnly, false);
76
- }
77
- }, [readOnly, editor]);
78
-
79
- if (!editor) {
80
- return null;
81
- }
82
-
83
- return (
84
- <>
85
- <EditorContent editor={editor} />
86
- <CustomBubbleMenu editor={editor} disabled={readOnly} />
87
- </>
88
- );
89
- }
90
14
 
91
- function CustomBubbleMenu({ editor, disabled }: { editor: Editor | null; disabled?: boolean }) {
92
- if (!editor || disabled) return null;
93
- return (
94
- <BubbleMenu editor={editor}>
95
- <div className="flex items-center gap-2 bg-background p-2 rounded-md border">
96
- <Button
97
- type="button"
98
- variant="ghost"
99
- size="icon"
100
- onClick={() => editor.chain().focus().toggleBold().run()}
101
- className={editor.isActive('bold') ? 'bg-accent' : ''}
102
- disabled={disabled}
103
- >
104
- <BoldIcon className="w-4 h-4" />
105
- </Button>
106
- <Button
107
- type="button"
108
- variant="ghost"
109
- size="icon"
110
- onClick={() => editor.chain().focus().toggleItalic().run()}
111
- className={editor.isActive('italic') ? 'bg-accent' : ''}
112
- disabled={disabled}
113
- >
114
- <ItalicIcon className="w-4 h-4" />
115
- </Button>
116
- <Button
117
- type="button"
118
- variant="ghost"
119
- size="icon"
120
- onClick={() => editor.chain().focus().toggleStrike().run()}
121
- className={editor.isActive('strike') ? 'bg-accent' : ''}
122
- disabled={disabled}
123
- >
124
- <StrikethroughIcon className="w-4 h-4" />
125
- </Button>
126
- </div>
127
- </BubbleMenu>
128
- );
15
+ return <RichTextEditor value={value} onChange={onChange} disabled={readOnly} />;
129
16
  }