@vendure/dashboard 3.5.6-master-202603270307 → 3.5.6
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 +3 -3
- package/src/app/routes/_authenticated/_collections/collections.tsx +119 -38
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +3 -5
- package/src/app/routes/_authenticated/_collections/components/move-collections-dialog.tsx +2 -1
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +20 -2
- package/src/i18n/locales/ar.po +484 -392
- package/src/i18n/locales/bg.po +485 -393
- package/src/i18n/locales/cs.po +484 -392
- package/src/i18n/locales/de.po +484 -392
- package/src/i18n/locales/en.po +485 -393
- package/src/i18n/locales/es.po +484 -392
- package/src/i18n/locales/fa.po +484 -392
- package/src/i18n/locales/fr.po +485 -393
- package/src/i18n/locales/he.po +484 -392
- package/src/i18n/locales/hr.po +485 -393
- package/src/i18n/locales/hu.po +346 -276
- package/src/i18n/locales/it.po +484 -392
- package/src/i18n/locales/ja.po +484 -392
- package/src/i18n/locales/nb.po +484 -392
- package/src/i18n/locales/ne.po +485 -393
- package/src/i18n/locales/nl.po +1441 -732
- package/src/i18n/locales/pl.po +485 -393
- package/src/i18n/locales/pt_BR.po +484 -392
- package/src/i18n/locales/pt_PT.po +484 -392
- package/src/i18n/locales/ru.po +485 -393
- package/src/i18n/locales/sv.po +484 -392
- package/src/i18n/locales/tr.po +484 -392
- package/src/i18n/locales/uk.po +484 -392
- package/src/i18n/locales/zh_Hans.po +484 -392
- package/src/i18n/locales/zh_Hant.po +484 -392
- package/src/lib/components/data-input/money-input.tsx +16 -4
- package/src/lib/components/data-table/data-table-utils.ts +23 -25
- package/src/lib/framework/page/list-page.tsx +2 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.5.6
|
|
4
|
+
"version": "3.5.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -162,8 +162,8 @@
|
|
|
162
162
|
"@storybook/addon-vitest": "^10.0.0-beta.9",
|
|
163
163
|
"@storybook/react-vite": "^10.0.0-beta.9",
|
|
164
164
|
"@types/node": "^22.13.4",
|
|
165
|
-
"@vendure/common": "
|
|
166
|
-
"@vendure/core": "
|
|
165
|
+
"@vendure/common": "3.5.6",
|
|
166
|
+
"@vendure/core": "3.5.6",
|
|
167
167
|
"@vitest/browser": "^3.2.4",
|
|
168
168
|
"@vitest/coverage-v8": "^3.2.4",
|
|
169
169
|
"eslint": "^9.19.0",
|
|
@@ -6,12 +6,12 @@ import { ListPage } from '@/vdb/framework/page/list-page.js';
|
|
|
6
6
|
import { api } from '@/vdb/graphql/api.js';
|
|
7
7
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
8
8
|
import { FetchQueryOptions, useQueries, useQueryClient } from '@tanstack/react-query';
|
|
9
|
-
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
9
|
+
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
|
10
10
|
import { ExpandedState, getExpandedRowModel } from '@tanstack/react-table';
|
|
11
11
|
import { TableOptions } from '@tanstack/table-core';
|
|
12
12
|
import { ResultOf } from 'gql.tada';
|
|
13
13
|
import { Folder, FolderOpen, PlusIcon } from 'lucide-react';
|
|
14
|
-
import { useState } from 'react';
|
|
14
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
15
15
|
import { toast } from 'sonner';
|
|
16
16
|
|
|
17
17
|
import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
|
|
@@ -33,9 +33,29 @@ import {
|
|
|
33
33
|
import { CollectionContentsSheet } from './components/collection-contents-sheet.js';
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
function parseExpandedParam(expanded?: string): ExpandedState {
|
|
37
|
+
if (!expanded) return {};
|
|
38
|
+
const ids = expanded.split(',').filter(Boolean);
|
|
39
|
+
return Object.fromEntries(ids.map(id => [id, true]));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function serializeExpandedState(expanded: ExpandedState): string | undefined {
|
|
43
|
+
if (expanded === true) return undefined;
|
|
44
|
+
const ids = Object.entries(expanded)
|
|
45
|
+
.filter(([_, v]) => v)
|
|
46
|
+
.map(([id]) => id);
|
|
47
|
+
return ids.length > 0 ? ids.join(',') : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
export const Route = createFileRoute('/_authenticated/_collections/collections')({
|
|
37
51
|
component: CollectionListPage,
|
|
38
52
|
loader: () => ({ breadcrumb: () => <Trans>Collections</Trans> }),
|
|
53
|
+
validateSearch: (search: Record<string, unknown>) => {
|
|
54
|
+
return {
|
|
55
|
+
...search,
|
|
56
|
+
expanded: (search.expanded as string) || undefined,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
39
59
|
});
|
|
40
60
|
|
|
41
61
|
|
|
@@ -61,14 +81,36 @@ function isLoadMoreRow(row: CollectionOrLoadMore): row is LoadMoreRow {
|
|
|
61
81
|
function CollectionListPage() {
|
|
62
82
|
const { t } = useLingui();
|
|
63
83
|
const queryClient = useQueryClient();
|
|
64
|
-
const
|
|
84
|
+
const routeSearch = Route.useSearch();
|
|
85
|
+
const navigate = useNavigate({ from: Route.fullPath });
|
|
86
|
+
const [expanded, setExpandedState] = useState<ExpandedState>(() => parseExpandedParam(routeSearch.expanded));
|
|
65
87
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
66
88
|
const [accumulatedChildren, setAccumulatedChildren] = useState<
|
|
67
89
|
Record<string, { items: Collection[]; totalItems: number }>
|
|
68
90
|
>({});
|
|
69
91
|
const [nextPageToFetch, setNextPageToFetch] = useState<Record<string, number>>({});
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
const setExpanded = useCallback((updater: ExpandedState | ((prev: ExpandedState) => ExpandedState)) => {
|
|
94
|
+
setExpandedState(prev => {
|
|
95
|
+
const next = typeof updater === 'function' ? updater(prev) : updater;
|
|
96
|
+
navigate({
|
|
97
|
+
search: (old: Record<string, unknown>) => ({
|
|
98
|
+
...old,
|
|
99
|
+
expanded: serializeExpandedState(next),
|
|
100
|
+
}),
|
|
101
|
+
replace: true,
|
|
102
|
+
});
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
}, [navigate]);
|
|
106
|
+
|
|
107
|
+
// NOTE: queryFn must be pure (no setState side effects) because TanStack Query
|
|
108
|
+
// skips queryFn entirely when data is served from cache (staleTime: 5min). If we
|
|
109
|
+
// called setAccumulatedChildren inside queryFn, a re-mounted component would get
|
|
110
|
+
// cache hits but accumulatedChildren would never be populated, so children wouldn't
|
|
111
|
+
// render. Instead we sync via useEffect below, which fires for both cache hits and
|
|
112
|
+
// fresh fetches.
|
|
113
|
+
const firstPageChildQueries = useQueries({
|
|
72
114
|
queries: expanded === true ? [] : Object.entries(expanded)
|
|
73
115
|
.filter(([collectionId]) => !accumulatedChildren[collectionId])
|
|
74
116
|
.map(([collectionId]) => {
|
|
@@ -84,21 +126,35 @@ function CollectionListPage() {
|
|
|
84
126
|
skip: 0,
|
|
85
127
|
},
|
|
86
128
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
}));
|
|
94
|
-
return result;
|
|
129
|
+
return {
|
|
130
|
+
collectionId,
|
|
131
|
+
items: result.collections.items,
|
|
132
|
+
totalItems: result.collections.totalItems,
|
|
133
|
+
};
|
|
95
134
|
},
|
|
96
135
|
staleTime: 1000 * 60 * 5,
|
|
97
136
|
} satisfies FetchQueryOptions;
|
|
98
137
|
}),
|
|
99
138
|
});
|
|
100
139
|
|
|
101
|
-
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const newChildren: Record<string, { items: Collection[]; totalItems: number }> = {};
|
|
142
|
+
let hasNew = false;
|
|
143
|
+
for (const query of firstPageChildQueries) {
|
|
144
|
+
if (query.data && !accumulatedChildren[query.data.collectionId]) {
|
|
145
|
+
newChildren[query.data.collectionId] = {
|
|
146
|
+
items: query.data.items as Collection[],
|
|
147
|
+
totalItems: query.data.totalItems,
|
|
148
|
+
};
|
|
149
|
+
hasNew = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (hasNew) {
|
|
153
|
+
setAccumulatedChildren(prev => ({ ...prev, ...newChildren }));
|
|
154
|
+
}
|
|
155
|
+
}, [firstPageChildQueries]);
|
|
156
|
+
|
|
157
|
+
const pagedChildQueries = useQueries({
|
|
102
158
|
queries: Object.entries(nextPageToFetch)
|
|
103
159
|
.filter(([_, page]) => page > 0)
|
|
104
160
|
.map(([collectionId, page]) => {
|
|
@@ -114,28 +170,49 @@ function CollectionListPage() {
|
|
|
114
170
|
skip: page * CHILDREN_PAGE_SIZE,
|
|
115
171
|
},
|
|
116
172
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
[collectionId]: {
|
|
123
|
-
items: [...existing.items, ...result.collections.items],
|
|
124
|
-
totalItems: result.collections.totalItems,
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
});
|
|
128
|
-
setNextPageToFetch(prev => {
|
|
129
|
-
const { [collectionId]: _, ...rest } = prev;
|
|
130
|
-
return rest;
|
|
131
|
-
});
|
|
132
|
-
return result;
|
|
173
|
+
return {
|
|
174
|
+
collectionId,
|
|
175
|
+
items: result.collections.items,
|
|
176
|
+
totalItems: result.collections.totalItems,
|
|
177
|
+
};
|
|
133
178
|
},
|
|
134
179
|
staleTime: 1000 * 60 * 5,
|
|
135
180
|
} satisfies FetchQueryOptions;
|
|
136
181
|
}),
|
|
137
182
|
});
|
|
138
183
|
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
let hasUpdates = false;
|
|
186
|
+
const childUpdates: Record<string, { items: Collection[]; totalItems: number }> = {};
|
|
187
|
+
const fetchedPages: string[] = [];
|
|
188
|
+
for (const query of pagedChildQueries) {
|
|
189
|
+
if (!query.data) continue;
|
|
190
|
+
const { collectionId, items, totalItems } = query.data as {
|
|
191
|
+
collectionId: string;
|
|
192
|
+
items: Collection[];
|
|
193
|
+
totalItems: number;
|
|
194
|
+
};
|
|
195
|
+
if (accumulatedChildren[collectionId]) {
|
|
196
|
+
childUpdates[collectionId] = {
|
|
197
|
+
items: [...accumulatedChildren[collectionId].items, ...items],
|
|
198
|
+
totalItems,
|
|
199
|
+
};
|
|
200
|
+
fetchedPages.push(collectionId);
|
|
201
|
+
hasUpdates = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (hasUpdates) {
|
|
205
|
+
setAccumulatedChildren(prev => ({ ...prev, ...childUpdates }));
|
|
206
|
+
setNextPageToFetch(prev => {
|
|
207
|
+
const next = { ...prev };
|
|
208
|
+
for (const id of fetchedPages) {
|
|
209
|
+
delete next[id];
|
|
210
|
+
}
|
|
211
|
+
return next;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}, [pagedChildQueries]);
|
|
215
|
+
|
|
139
216
|
const addSubCollections = (data: Collection[]): CollectionOrLoadMore[] => {
|
|
140
217
|
const allRows: CollectionOrLoadMore[] = [];
|
|
141
218
|
const addSubRows = (row: Collection) => {
|
|
@@ -226,6 +303,13 @@ function CollectionListPage() {
|
|
|
226
303
|
},
|
|
227
304
|
});
|
|
228
305
|
|
|
306
|
+
// Remove query cache entries BEFORE clearing accumulated children
|
|
307
|
+
// to prevent stale cached data from being synced back by the useEffect.
|
|
308
|
+
queryClient.removeQueries({ queryKey: ['childCollections', sourceParentId] });
|
|
309
|
+
if (targetParentId !== sourceParentId) {
|
|
310
|
+
queryClient.removeQueries({ queryKey: ['childCollections', targetParentId] });
|
|
311
|
+
}
|
|
312
|
+
|
|
229
313
|
setAccumulatedChildren(prev => {
|
|
230
314
|
const newState = { ...prev };
|
|
231
315
|
delete newState[sourceParentId];
|
|
@@ -235,19 +319,11 @@ function CollectionListPage() {
|
|
|
235
319
|
return newState;
|
|
236
320
|
});
|
|
237
321
|
|
|
238
|
-
|
|
239
|
-
queryClient.invalidateQueries({ queryKey: ['childCollections', sourceParentId] }),
|
|
240
|
-
queryClient.invalidateQueries({ queryKey: ['PaginatedListDataTable'] }),
|
|
241
|
-
];
|
|
322
|
+
await queryClient.invalidateQueries({ queryKey: ['PaginatedListDataTable'] });
|
|
242
323
|
|
|
243
324
|
if (targetParentId === sourceParentId) {
|
|
244
|
-
await Promise.all(queriesToInvalidate);
|
|
245
325
|
toast.success(t`Collection position updated`);
|
|
246
326
|
} else {
|
|
247
|
-
queriesToInvalidate.push(
|
|
248
|
-
queryClient.invalidateQueries({ queryKey: ['childCollections', targetParentId] })
|
|
249
|
-
);
|
|
250
|
-
await Promise.all(queriesToInvalidate);
|
|
251
327
|
toast.success(t`Collection moved to new parent`);
|
|
252
328
|
}
|
|
253
329
|
} catch (error) {
|
|
@@ -378,6 +454,11 @@ function CollectionListPage() {
|
|
|
378
454
|
options.meta = {
|
|
379
455
|
...options.meta,
|
|
380
456
|
resetExpanded: () => setExpanded({}),
|
|
457
|
+
refreshChildCaches: () => {
|
|
458
|
+
queryClient.removeQueries({ queryKey: ['childCollections'] });
|
|
459
|
+
queryClient.removeQueries({ queryKey: ['PaginatedListDataTable'] });
|
|
460
|
+
setAccumulatedChildren({});
|
|
461
|
+
},
|
|
381
462
|
isUtilityRow: (row: { original: CollectionOrLoadMore }) => isLoadMoreRow(row.original),
|
|
382
463
|
renderUtilityRow: (row: { original: CollectionOrLoadMore }) => {
|
|
383
464
|
const original = row.original as LoadMoreRow;
|
|
@@ -94,19 +94,17 @@ export const DeleteCollectionsBulkAction: BulkActionComponent<any> = ({ selectio
|
|
|
94
94
|
|
|
95
95
|
export const MoveCollectionsBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
96
96
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
97
|
-
const queryClient = useQueryClient();
|
|
98
97
|
const { refetchPaginatedList } = usePaginatedList();
|
|
99
98
|
|
|
100
99
|
const handleSuccess = () => {
|
|
101
|
-
queryClient.invalidateQueries({ queryKey: ['childCollections'] });
|
|
102
100
|
refetchPaginatedList();
|
|
103
101
|
table.resetRowSelection();
|
|
104
102
|
};
|
|
105
103
|
|
|
106
104
|
const handleResetExpanded = () => {
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
|
|
105
|
+
const refreshChildCaches = (table.options.meta as { refreshChildCaches: () => void })?.refreshChildCaches;
|
|
106
|
+
if (refreshChildCaches) {
|
|
107
|
+
refreshChildCaches();
|
|
110
108
|
}
|
|
111
109
|
};
|
|
112
110
|
|
|
@@ -284,7 +284,8 @@ export function MoveCollectionsDialog({
|
|
|
284
284
|
toast.success(t`Collections moved successfully`);
|
|
285
285
|
queryClient.invalidateQueries({ queryKey: collectionForMoveKey });
|
|
286
286
|
queryClient.invalidateQueries({ queryKey: childCollectionsForMoveKey() });
|
|
287
|
-
|
|
287
|
+
// Remove child caches BEFORE invalidating the main list to prevent
|
|
288
|
+
// stale cached children from being synced back (same race as drag-reorder).
|
|
288
289
|
onResetExpanded?.();
|
|
289
290
|
onSuccess?.();
|
|
290
291
|
onOpenChange(false);
|
|
@@ -9,6 +9,7 @@ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js'
|
|
|
9
9
|
import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
|
|
10
10
|
import { TaxCategorySelector } from '@/vdb/components/shared/tax-category-selector.js';
|
|
11
11
|
import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
|
|
12
|
+
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
12
13
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
13
14
|
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@/vdb/components/ui/form.js';
|
|
14
15
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
@@ -33,9 +34,9 @@ import { api } from '@/vdb/graphql/api.js';
|
|
|
33
34
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
34
35
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
35
36
|
import { useQuery } from '@tanstack/react-query';
|
|
36
|
-
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
37
|
+
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
|
37
38
|
import { VariablesOf } from 'gql.tada';
|
|
38
|
-
import { Trash } from 'lucide-react';
|
|
39
|
+
import { Edit2, Trash } from 'lucide-react';
|
|
39
40
|
import { toast } from 'sonner';
|
|
40
41
|
|
|
41
42
|
import { AddCurrencyDropdown } from './components/add-currency-dropdown.js';
|
|
@@ -241,6 +242,23 @@ function ProductVariantDetailPage() {
|
|
|
241
242
|
)}
|
|
242
243
|
/>
|
|
243
244
|
</PageBlock>
|
|
245
|
+
{entity?.options && entity.options.length > 0 && (
|
|
246
|
+
<PageBlock column="side" blockId="options" title={<Trans>Options</Trans>}>
|
|
247
|
+
<div className="flex flex-wrap gap-1.5">
|
|
248
|
+
{entity.options.map(option => (
|
|
249
|
+
<Badge key={option.id} variant="secondary" className="text-xs" title={option.code}>
|
|
250
|
+
<span>{option.group.name}: {option.name}</span>
|
|
251
|
+
<Link
|
|
252
|
+
to={`/products/${entity.product.id}/option-groups/${option.group.id}`}
|
|
253
|
+
className="ml-1.5 inline-flex"
|
|
254
|
+
>
|
|
255
|
+
<Edit2 className="h-3 w-3" />
|
|
256
|
+
</Link>
|
|
257
|
+
</Badge>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
</PageBlock>
|
|
261
|
+
)}
|
|
244
262
|
<PageBlock column="main" blockId="main-form">
|
|
245
263
|
<DetailFormGrid>
|
|
246
264
|
<TranslatableFormFieldWrapper
|