@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.
Files changed (43) hide show
  1. package/package.json +3 -3
  2. package/src/app/common/duplicate-entity-dialog.tsx +2 -1
  3. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +2 -1
  4. package/src/app/routes/_authenticated/_products/components/generate-variants-panel.tsx +133 -32
  5. package/src/i18n/locales/ar.po +97 -78
  6. package/src/i18n/locales/bg.po +97 -78
  7. package/src/i18n/locales/cs.po +97 -78
  8. package/src/i18n/locales/de.po +97 -78
  9. package/src/i18n/locales/en.po +97 -78
  10. package/src/i18n/locales/es.po +97 -78
  11. package/src/i18n/locales/fa.po +97 -78
  12. package/src/i18n/locales/fr.po +97 -78
  13. package/src/i18n/locales/he.po +97 -78
  14. package/src/i18n/locales/hr.po +97 -78
  15. package/src/i18n/locales/hu.po +97 -78
  16. package/src/i18n/locales/it.po +97 -78
  17. package/src/i18n/locales/ja.po +97 -78
  18. package/src/i18n/locales/nb.po +97 -78
  19. package/src/i18n/locales/ne.po +97 -78
  20. package/src/i18n/locales/nl.po +97 -78
  21. package/src/i18n/locales/pl.po +97 -78
  22. package/src/i18n/locales/pt_BR.po +97 -78
  23. package/src/i18n/locales/pt_PT.po +97 -78
  24. package/src/i18n/locales/ro.po +97 -78
  25. package/src/i18n/locales/ru.po +97 -78
  26. package/src/i18n/locales/sv.po +97 -78
  27. package/src/i18n/locales/tr.po +97 -78
  28. package/src/i18n/locales/uk.po +97 -78
  29. package/src/i18n/locales/zh_Hans.po +97 -78
  30. package/src/i18n/locales/zh_Hant.po +97 -78
  31. package/src/lib/components/data-input/affixed-input.tsx +2 -0
  32. package/src/lib/components/data-input/default-relation-input.tsx +60 -0
  33. package/src/lib/components/data-input/select-with-options.tsx +12 -5
  34. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +2 -1
  35. package/src/lib/components/shared/configurable-operation-selector.tsx +2 -1
  36. package/src/lib/components/shared/configurable-operation-utils.spec.ts +49 -0
  37. package/src/lib/components/shared/configurable-operation-utils.ts +18 -0
  38. package/src/lib/framework/form-engine/form-schema-tools.spec.ts +39 -0
  39. package/src/lib/framework/form-engine/form-schema-tools.ts +72 -2
  40. package/src/lib/framework/form-engine/use-generated-form.tsx +13 -10
  41. package/src/lib/framework/form-engine/utils.spec.ts +50 -0
  42. package/src/lib/framework/form-engine/utils.ts +14 -0
  43. 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-master-202605290309",
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": "^3.6.4-master-202605290309",
141
- "@vendure/core": "^3.6.4-master-202605290309",
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.defaultValue != null ? arg.defaultValue.toString() : '',
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.defaultValue != null ? arg.defaultValue.toString() : '',
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 { Save } from 'lucide-react';
14
- import { useMemo } from 'react';
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
- {variants.length > 1 && (
269
+ {showVariantTools && (
195
270
  <TableHead className="w-12">
196
- <Trans>Create</Trans>
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
- {variants.length > 1 && (
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
- {variants.map(variant => (
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
- {variants.length > 1 && (
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
- {variants.length > 1 && (
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 {...field} placeholder="SKU" data-testid="variant-sku-input" />
246
- {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
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
- field.onChange(value.toString())
263
- }
264
- currency={
265
- activeChannel?.defaultCurrencyCode
266
- }
359
+ onChange={value => field.onChange(value.toString())}
360
+ currency={activeChannel?.defaultCurrencyCode}
267
361
  />
268
- {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
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 && <FieldError errors={[fieldState.error]} />}
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 && <Trans>Create variant</Trans>}
332
- {!createVariantsMutation.isPending && enabledCount !== 1 && <Trans>Create {enabledCount} variants</Trans>}
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>