@vendure/dashboard 3.6.4-master-202605290309 → 3.6.4
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/common/duplicate-entity-dialog.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +2 -1
- package/src/app/routes/_authenticated/_products/components/generate-variants-panel.tsx +133 -32
- package/src/i18n/locales/ar.po +97 -78
- package/src/i18n/locales/bg.po +97 -78
- package/src/i18n/locales/cs.po +97 -78
- package/src/i18n/locales/de.po +97 -78
- package/src/i18n/locales/en.po +97 -78
- package/src/i18n/locales/es.po +97 -78
- package/src/i18n/locales/fa.po +97 -78
- package/src/i18n/locales/fr.po +97 -78
- package/src/i18n/locales/he.po +97 -78
- package/src/i18n/locales/hr.po +97 -78
- package/src/i18n/locales/hu.po +97 -78
- package/src/i18n/locales/it.po +97 -78
- package/src/i18n/locales/ja.po +97 -78
- package/src/i18n/locales/nb.po +97 -78
- package/src/i18n/locales/ne.po +97 -78
- package/src/i18n/locales/nl.po +97 -78
- package/src/i18n/locales/pl.po +97 -78
- package/src/i18n/locales/pt_BR.po +97 -78
- package/src/i18n/locales/pt_PT.po +97 -78
- package/src/i18n/locales/ro.po +97 -78
- package/src/i18n/locales/ru.po +97 -78
- package/src/i18n/locales/sv.po +97 -78
- package/src/i18n/locales/tr.po +97 -78
- package/src/i18n/locales/uk.po +97 -78
- package/src/i18n/locales/zh_Hans.po +97 -78
- package/src/i18n/locales/zh_Hant.po +97 -78
- package/src/lib/components/data-input/affixed-input.tsx +2 -0
- package/src/lib/components/data-input/default-relation-input.tsx +60 -0
- package/src/lib/components/data-input/select-with-options.tsx +12 -5
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +2 -1
- package/src/lib/components/shared/configurable-operation-selector.tsx +2 -1
- package/src/lib/components/shared/configurable-operation-utils.spec.ts +49 -0
- package/src/lib/components/shared/configurable-operation-utils.ts +18 -0
- package/src/lib/framework/form-engine/form-schema-tools.spec.ts +39 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +72 -2
- package/src/lib/framework/form-engine/use-generated-form.tsx +13 -10
- package/src/lib/framework/form-engine/utils.spec.ts +50 -0
- package/src/lib/framework/form-engine/utils.ts +14 -0
- package/src/lib/providers/auth.tsx +52 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.6.4
|
|
4
|
+
"version": "3.6.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -137,8 +137,8 @@
|
|
|
137
137
|
"@storybook/addon-vitest": "^10.3.1",
|
|
138
138
|
"@storybook/react-vite": "^10.3.1",
|
|
139
139
|
"@types/node": "^22.19.0",
|
|
140
|
-
"@vendure/common": "
|
|
141
|
-
"@vendure/core": "
|
|
140
|
+
"@vendure/common": "3.6.4",
|
|
141
|
+
"@vendure/core": "3.6.4",
|
|
142
142
|
"@vitest/browser": "^3.2.4",
|
|
143
143
|
"@vitest/coverage-v8": "^3.2.4",
|
|
144
144
|
"eslint": "^9.39.0",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConfigurableOperationInput as ConfigurableOperationInputComponent } from '@/vdb/components/shared/configurable-operation-input.js';
|
|
2
|
+
import { getInitialConfigArgValue } from '@/vdb/components/shared/configurable-operation-utils.js';
|
|
2
3
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
4
|
import {
|
|
4
5
|
Dialog,
|
|
@@ -54,7 +55,7 @@ export function DuplicateEntityDialog({
|
|
|
54
55
|
arguments:
|
|
55
56
|
matchingDuplicator.args?.map(arg => ({
|
|
56
57
|
name: arg.name,
|
|
57
|
-
value: arg
|
|
58
|
+
value: getInitialConfigArgValue(arg),
|
|
58
59
|
})) || [],
|
|
59
60
|
});
|
|
60
61
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConfigurableOperationInput } from '@/vdb/components/shared/configurable-operation-input.js';
|
|
2
|
+
import { getInitialConfigArgValue } from '@/vdb/components/shared/configurable-operation-utils.js';
|
|
2
3
|
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
3
4
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
4
5
|
import {
|
|
@@ -120,7 +121,7 @@ export function FulfillOrderDialog({ order, onSuccess }: Readonly<FulfillOrderDi
|
|
|
120
121
|
code: defaultHandler.code,
|
|
121
122
|
arguments: defaultHandler.args.map(arg => ({
|
|
122
123
|
name: arg.name,
|
|
123
|
-
value: arg
|
|
124
|
+
value: getInitialConfigArgValue(arg),
|
|
124
125
|
})),
|
|
125
126
|
});
|
|
126
127
|
}
|
|
@@ -3,6 +3,7 @@ import { ConfirmationDialog } from '@/vdb/components/shared/confirmation-dialog.
|
|
|
3
3
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
4
4
|
import { Checkbox } from '@/vdb/components/ui/checkbox.js';
|
|
5
5
|
import { Field, FieldError } from '@/vdb/components/ui/field.js';
|
|
6
|
+
import { Form } from '@/vdb/components/ui/form.js';
|
|
6
7
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
7
8
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/vdb/components/ui/table.js';
|
|
8
9
|
import { api } from '@/vdb/graphql/api.js';
|
|
@@ -10,10 +11,10 @@ import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
|
10
11
|
import { z, zodResolver } from '@/vdb/lib/zod.js';
|
|
11
12
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
12
13
|
import { useMutation } from '@tanstack/react-query';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { useDebounce } from '@uidotdev/usehooks';
|
|
15
|
+
import { Save, Search } from 'lucide-react';
|
|
16
|
+
import { useMemo, useState } from 'react';
|
|
15
17
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
|
16
|
-
import { Form } from '@/vdb/components/ui/form.js';
|
|
17
18
|
import { toast } from 'sonner';
|
|
18
19
|
import { createProductVariantsDocument } from '../products.graphql.js';
|
|
19
20
|
|
|
@@ -126,14 +127,20 @@ export function GenerateVariantsPanel({
|
|
|
126
127
|
|
|
127
128
|
const variants = useMemo(() => generateVariantCombinations(optionGroups), [optionGroups]);
|
|
128
129
|
|
|
130
|
+
// For small products (few option-group combinations) the historical
|
|
131
|
+
// "all variants enabled by default" workflow is convenient. For products
|
|
132
|
+
// built on a shared option group with many values (the reporter's case in
|
|
133
|
+
// OSS-531 — 129 colors), defaulting every variant on forces the user to
|
|
134
|
+
// uncheck almost everything. Above the threshold we flip the default off
|
|
135
|
+
// and let them check only the ones they want; the filter + master toggle
|
|
136
|
+
// above the table make that workflow practical.
|
|
137
|
+
const enableByDefault = variants.length <= 20;
|
|
138
|
+
|
|
129
139
|
const form = useForm<VariantFormValues>({
|
|
130
140
|
resolver: zodResolver(formSchema),
|
|
131
141
|
defaultValues: {
|
|
132
142
|
variants: Object.fromEntries(
|
|
133
|
-
variants.map(v => [
|
|
134
|
-
v.id,
|
|
135
|
-
{ enabled: true, sku: '', price: '', stock: '' },
|
|
136
|
-
]),
|
|
143
|
+
variants.map(v => [v.id, { enabled: enableByDefault, sku: '', price: '', stock: '' }]),
|
|
137
144
|
),
|
|
138
145
|
},
|
|
139
146
|
mode: 'onChange',
|
|
@@ -150,9 +157,7 @@ export function GenerateVariantsPanel({
|
|
|
150
157
|
.filter(v => formValues.variants[v.id]?.enabled)
|
|
151
158
|
.map(v => {
|
|
152
159
|
const data = formValues.variants[v.id];
|
|
153
|
-
const name = v.optionNames.length
|
|
154
|
-
? `${productName} ${v.optionNames.join(' ')}`
|
|
155
|
-
: productName;
|
|
160
|
+
const name = v.optionNames.length ? `${productName} ${v.optionNames.join(' ')}` : productName;
|
|
156
161
|
|
|
157
162
|
return {
|
|
158
163
|
productId,
|
|
@@ -185,18 +190,95 @@ export function GenerateVariantsPanel({
|
|
|
185
190
|
const watchedVariants = useWatch({ control: form.control, name: 'variants' });
|
|
186
191
|
const enabledCount = variants.filter(v => watchedVariants?.[v.id]?.enabled).length;
|
|
187
192
|
|
|
193
|
+
const [filter, setFilter] = useState('');
|
|
194
|
+
const debouncedFilter = useDebounce(filter, 300);
|
|
195
|
+
const filteredVariants = useMemo(() => {
|
|
196
|
+
if (!debouncedFilter) return variants;
|
|
197
|
+
// Rows render `optionNames.join(' / ')`, so accept that exact shape
|
|
198
|
+
// ("Red / M") as well as the space-joined source name.
|
|
199
|
+
const normalize = (s: string) =>
|
|
200
|
+
s
|
|
201
|
+
.toLowerCase()
|
|
202
|
+
.replace(/\s*\/\s*/g, ' ')
|
|
203
|
+
.replace(/\s+/g, ' ')
|
|
204
|
+
.trim();
|
|
205
|
+
const q = normalize(debouncedFilter);
|
|
206
|
+
if (!q) return variants;
|
|
207
|
+
return variants.filter(v => normalize(v.name).includes(q));
|
|
208
|
+
}, [variants, debouncedFilter]);
|
|
209
|
+
|
|
210
|
+
// Master toggle drives every variant currently visible after the filter,
|
|
211
|
+
// not the whole list — so a user can scope a check/uncheck-all to e.g.
|
|
212
|
+
// "Red" without losing their selections in other rows.
|
|
213
|
+
const visibleEnabledCount = filteredVariants.filter(v => watchedVariants?.[v.id]?.enabled).length;
|
|
214
|
+
const allVisibleEnabled = filteredVariants.length > 0 && visibleEnabledCount === filteredVariants.length;
|
|
215
|
+
const someVisibleEnabled = visibleEnabledCount > 0;
|
|
216
|
+
const handleToggleVisible = () => {
|
|
217
|
+
const shouldEnable = !allVisibleEnabled;
|
|
218
|
+
// Single setValue with the full record avoids N RHF subscriber updates
|
|
219
|
+
// (the table has 129+ rows for shared option groups).
|
|
220
|
+
const next = { ...(form.getValues('variants') ?? {}) };
|
|
221
|
+
for (const v of filteredVariants) {
|
|
222
|
+
next[v.id] = { ...next[v.id], enabled: shouldEnable };
|
|
223
|
+
}
|
|
224
|
+
form.setValue('variants', next, { shouldDirty: true });
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const showVariantTools = variants.length > 1;
|
|
228
|
+
const isFiltered = debouncedFilter.length > 0;
|
|
229
|
+
|
|
188
230
|
return (
|
|
189
231
|
<Form {...form}>
|
|
190
232
|
<div className="space-y-4">
|
|
233
|
+
{showVariantTools && (
|
|
234
|
+
<div className="flex items-center gap-3">
|
|
235
|
+
<div className="relative w-full max-w-sm">
|
|
236
|
+
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
237
|
+
<Input
|
|
238
|
+
value={filter}
|
|
239
|
+
onChange={e => setFilter(e.target.value)}
|
|
240
|
+
placeholder={t`Filter variants...`}
|
|
241
|
+
className="pl-8"
|
|
242
|
+
data-testid="variant-filter-input"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="text-sm text-muted-foreground">
|
|
246
|
+
{(() => {
|
|
247
|
+
// Hoist locals so Lingui extracts named placeholders
|
|
248
|
+
// (`{shown}`, `{total}`, `{selected}`) in the .po
|
|
249
|
+
// catalog instead of positional `{0}`, `{1}`, `{2}`.
|
|
250
|
+
const shown = filteredVariants.length;
|
|
251
|
+
const total = variants.length;
|
|
252
|
+
const selected = enabledCount;
|
|
253
|
+
return isFiltered ? (
|
|
254
|
+
<Trans>
|
|
255
|
+
Showing {shown} of {total} • {selected} selected
|
|
256
|
+
</Trans>
|
|
257
|
+
) : (
|
|
258
|
+
<Trans>
|
|
259
|
+
{selected} of {total} selected
|
|
260
|
+
</Trans>
|
|
261
|
+
);
|
|
262
|
+
})()}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
191
266
|
<Table>
|
|
192
267
|
<TableHeader>
|
|
193
268
|
<TableRow>
|
|
194
|
-
{
|
|
269
|
+
{showVariantTools && (
|
|
195
270
|
<TableHead className="w-12">
|
|
196
|
-
<
|
|
271
|
+
<Checkbox
|
|
272
|
+
checked={allVisibleEnabled || someVisibleEnabled}
|
|
273
|
+
indeterminate={someVisibleEnabled && !allVisibleEnabled}
|
|
274
|
+
onCheckedChange={handleToggleVisible}
|
|
275
|
+
disabled={filteredVariants.length === 0}
|
|
276
|
+
aria-label={t`Toggle all visible variants`}
|
|
277
|
+
data-testid="variant-toggle-all"
|
|
278
|
+
/>
|
|
197
279
|
</TableHead>
|
|
198
280
|
)}
|
|
199
|
-
{
|
|
281
|
+
{showVariantTools && (
|
|
200
282
|
<TableHead>
|
|
201
283
|
<Trans>Variant</Trans>
|
|
202
284
|
</TableHead>
|
|
@@ -213,9 +295,19 @@ export function GenerateVariantsPanel({
|
|
|
213
295
|
</TableRow>
|
|
214
296
|
</TableHeader>
|
|
215
297
|
<TableBody>
|
|
216
|
-
{
|
|
298
|
+
{filteredVariants.length === 0 && (
|
|
299
|
+
<TableRow>
|
|
300
|
+
<TableCell
|
|
301
|
+
colSpan={showVariantTools ? 5 : 3}
|
|
302
|
+
className="text-center text-muted-foreground py-8"
|
|
303
|
+
>
|
|
304
|
+
<Trans>No variants match the current filter.</Trans>
|
|
305
|
+
</TableCell>
|
|
306
|
+
</TableRow>
|
|
307
|
+
)}
|
|
308
|
+
{filteredVariants.map(variant => (
|
|
217
309
|
<TableRow key={variant.id}>
|
|
218
|
-
{
|
|
310
|
+
{showVariantTools && (
|
|
219
311
|
<TableCell>
|
|
220
312
|
<Controller
|
|
221
313
|
control={form.control}
|
|
@@ -230,7 +322,7 @@ export function GenerateVariantsPanel({
|
|
|
230
322
|
</TableCell>
|
|
231
323
|
)}
|
|
232
324
|
|
|
233
|
-
{
|
|
325
|
+
{showVariantTools && (
|
|
234
326
|
<TableCell className="font-medium">
|
|
235
327
|
{variant.optionNames.join(' / ')}
|
|
236
328
|
</TableCell>
|
|
@@ -242,8 +334,14 @@ export function GenerateVariantsPanel({
|
|
|
242
334
|
name={`variants.${variant.id}.sku`}
|
|
243
335
|
render={({ field, fieldState }) => (
|
|
244
336
|
<Field data-invalid={fieldState.invalid || undefined}>
|
|
245
|
-
<Input
|
|
246
|
-
|
|
337
|
+
<Input
|
|
338
|
+
{...field}
|
|
339
|
+
placeholder="SKU"
|
|
340
|
+
data-testid="variant-sku-input"
|
|
341
|
+
/>
|
|
342
|
+
{fieldState.invalid && (
|
|
343
|
+
<FieldError errors={[fieldState.error]} />
|
|
344
|
+
)}
|
|
247
345
|
</Field>
|
|
248
346
|
)}
|
|
249
347
|
/>
|
|
@@ -258,14 +356,12 @@ export function GenerateVariantsPanel({
|
|
|
258
356
|
<MoneyInput
|
|
259
357
|
{...field}
|
|
260
358
|
value={Number(field.value) || 0}
|
|
261
|
-
onChange={value =>
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
currency={
|
|
265
|
-
activeChannel?.defaultCurrencyCode
|
|
266
|
-
}
|
|
359
|
+
onChange={value => field.onChange(value.toString())}
|
|
360
|
+
currency={activeChannel?.defaultCurrencyCode}
|
|
267
361
|
/>
|
|
268
|
-
{fieldState.invalid &&
|
|
362
|
+
{fieldState.invalid && (
|
|
363
|
+
<FieldError errors={[fieldState.error]} />
|
|
364
|
+
)}
|
|
269
365
|
</Field>
|
|
270
366
|
)}
|
|
271
367
|
/>
|
|
@@ -284,7 +380,9 @@ export function GenerateVariantsPanel({
|
|
|
284
380
|
step="1"
|
|
285
381
|
data-testid="variant-stock-input"
|
|
286
382
|
/>
|
|
287
|
-
{fieldState.invalid &&
|
|
383
|
+
{fieldState.invalid && (
|
|
384
|
+
<FieldError errors={[fieldState.error]} />
|
|
385
|
+
)}
|
|
288
386
|
</Field>
|
|
289
387
|
)}
|
|
290
388
|
/>
|
|
@@ -296,8 +394,8 @@ export function GenerateVariantsPanel({
|
|
|
296
394
|
|
|
297
395
|
<div className="flex justify-between items-center">
|
|
298
396
|
<div>
|
|
299
|
-
{onBack &&
|
|
300
|
-
onBack.confirmation ? (
|
|
397
|
+
{onBack &&
|
|
398
|
+
(onBack.confirmation ? (
|
|
301
399
|
<ConfirmationDialog
|
|
302
400
|
title={onBack.confirmation.title}
|
|
303
401
|
description={onBack.confirmation.description}
|
|
@@ -318,8 +416,7 @@ export function GenerateVariantsPanel({
|
|
|
318
416
|
>
|
|
319
417
|
← <Trans>Back</Trans>
|
|
320
418
|
</button>
|
|
321
|
-
)
|
|
322
|
-
)}
|
|
419
|
+
))}
|
|
323
420
|
</div>
|
|
324
421
|
<Button
|
|
325
422
|
type="button"
|
|
@@ -328,8 +425,12 @@ export function GenerateVariantsPanel({
|
|
|
328
425
|
>
|
|
329
426
|
<Save className="mr-2 h-4 w-4" />
|
|
330
427
|
{createVariantsMutation.isPending && <Trans>Creating...</Trans>}
|
|
331
|
-
{!createVariantsMutation.isPending && enabledCount === 1 &&
|
|
332
|
-
|
|
428
|
+
{!createVariantsMutation.isPending && enabledCount === 1 && (
|
|
429
|
+
<Trans>Create variant</Trans>
|
|
430
|
+
)}
|
|
431
|
+
{!createVariantsMutation.isPending && enabledCount !== 1 && (
|
|
432
|
+
<Trans>Create {enabledCount} variants</Trans>
|
|
433
|
+
)}
|
|
333
434
|
</Button>
|
|
334
435
|
</div>
|
|
335
436
|
</div>
|