@vendure/dashboard 3.3.8-master-202507260236 → 3.3.8-master-202507300243

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 (35) hide show
  1. package/package.json +4 -4
  2. package/src/app/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx +1 -1
  3. package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +11 -78
  4. package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +11 -81
  5. package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +10 -77
  6. package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +12 -87
  7. package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +12 -87
  8. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +10 -80
  9. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +10 -79
  10. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +8 -6
  11. package/src/lib/components/data-input/combination-mode-input.tsx +52 -0
  12. package/src/lib/components/data-input/configurable-operation-list-input.tsx +433 -0
  13. package/src/lib/components/data-input/custom-field-list-input.tsx +297 -0
  14. package/src/lib/components/data-input/datetime-input.tsx +5 -2
  15. package/src/lib/components/data-input/default-relation-input.tsx +599 -0
  16. package/src/lib/components/data-input/index.ts +6 -0
  17. package/src/lib/components/data-input/product-multi-selector.tsx +426 -0
  18. package/src/lib/components/data-input/relation-selector.tsx +7 -6
  19. package/src/lib/components/data-input/select-with-options.tsx +84 -0
  20. package/src/lib/components/data-input/struct-form-input.tsx +324 -0
  21. package/src/lib/components/shared/configurable-operation-arg-input.tsx +365 -21
  22. package/src/lib/components/shared/configurable-operation-input.tsx +81 -41
  23. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +260 -0
  24. package/src/lib/components/shared/configurable-operation-selector.tsx +156 -0
  25. package/src/lib/components/shared/custom-fields-form.tsx +207 -36
  26. package/src/lib/components/shared/multi-select.tsx +1 -1
  27. package/src/lib/components/ui/form.tsx +4 -4
  28. package/src/lib/framework/extension-api/input-component-extensions.tsx +5 -1
  29. package/src/lib/framework/form-engine/form-schema-tools.spec.ts +472 -0
  30. package/src/lib/framework/form-engine/form-schema-tools.ts +340 -5
  31. package/src/lib/framework/form-engine/use-generated-form.tsx +24 -8
  32. package/src/lib/framework/form-engine/utils.ts +3 -9
  33. package/src/lib/framework/layout-engine/page-layout.tsx +11 -3
  34. package/src/lib/framework/page/use-detail-page.ts +3 -3
  35. package/src/lib/lib/utils.ts +26 -24
@@ -0,0 +1,599 @@
1
+ import { graphql } from '@/vdb/graphql/graphql.js';
2
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
3
+ import { RelationCustomFieldConfig } from '@vendure/common/lib/generated-types';
4
+ import { ControllerRenderProps } from 'react-hook-form';
5
+ import { MultiRelationInput, SingleRelationInput } from './relation-input.js';
6
+ import { createRelationSelectorConfig } from './relation-selector.js';
7
+
8
+ interface PlaceholderIconProps {
9
+ letter: string;
10
+ className?: string;
11
+ rounded?: boolean;
12
+ }
13
+
14
+ function PlaceholderIcon({ letter, className = '', rounded = false }: Readonly<PlaceholderIconProps>) {
15
+ return (
16
+ <div
17
+ className={`w-full h-full bg-muted flex items-center justify-center border rounded text-muted-foreground ${rounded ? 'text-sm font-medium' : 'text-xs'} ${className}`}
18
+ >
19
+ {letter}
20
+ </div>
21
+ );
22
+ }
23
+
24
+ interface EntityLabelProps {
25
+ title: string;
26
+ subtitle: string;
27
+ imageUrl?: string;
28
+ placeholderLetter: string;
29
+ statusIndicator?: React.ReactNode;
30
+ rounded?: boolean;
31
+ tooltipText?: string;
32
+ }
33
+
34
+ interface StatusBadgeProps {
35
+ condition: boolean;
36
+ text: string;
37
+ variant?: 'orange' | 'green' | 'red' | 'blue';
38
+ }
39
+
40
+ function StatusBadge({ condition, text, variant = 'orange' }: Readonly<StatusBadgeProps>) {
41
+ if (!condition) return null;
42
+
43
+ const colorClasses = {
44
+ orange: 'text-orange-600',
45
+ green: 'bg-green-100 text-green-700',
46
+ red: 'bg-red-100 text-red-700',
47
+ blue: 'bg-blue-100 text-blue-700',
48
+ };
49
+
50
+ return (
51
+ <span
52
+ className={`ml-2 text-xs ${variant === 'orange' ? colorClasses.orange : `px-1.5 py-0.5 rounded-full ${colorClasses[variant]}`}`}
53
+ >
54
+ • {text}
55
+ </span>
56
+ );
57
+ }
58
+
59
+ function EntityLabel({
60
+ title,
61
+ subtitle,
62
+ imageUrl,
63
+ placeholderLetter,
64
+ statusIndicator,
65
+ rounded = false,
66
+ tooltipText,
67
+ }: Readonly<EntityLabelProps>) {
68
+ return (
69
+ <div className="flex items-center gap-3 w-full" title={tooltipText || `${title} (${subtitle})`}>
70
+ <div
71
+ className={`w-8 h-8 ${rounded ? 'rounded-full' : 'rounded overflow-hidden'} bg-muted flex-shrink-0`}
72
+ >
73
+ {imageUrl ? (
74
+ <img
75
+ src={imageUrl + '?preset=thumb'}
76
+ alt={title}
77
+ className="w-full h-full object-cover"
78
+ />
79
+ ) : (
80
+ <PlaceholderIcon letter={placeholderLetter} rounded={rounded} />
81
+ )}
82
+ </div>
83
+ <div className="flex-1 min-w-0">
84
+ <div className="font-medium truncate">
85
+ {title}
86
+ {statusIndicator}
87
+ </div>
88
+ <div className="text-sm text-muted-foreground truncate">{subtitle}</div>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ function createBaseEntityConfig(
95
+ entityName: string,
96
+ i18n: any,
97
+ labelKey: 'name' | 'code' | 'emailAddress' = 'name',
98
+ searchField: string = 'name',
99
+ ) {
100
+ return {
101
+ idKey: 'id',
102
+ labelKey,
103
+ placeholder: i18n.t(`Search ${entityName.toLowerCase()}s...`),
104
+ buildSearchFilter: (term: string) => ({
105
+ [searchField]: { contains: term },
106
+ }),
107
+ } as const;
108
+ }
109
+
110
+ function getOrderStateVariant(state: string): StatusBadgeProps['variant'] {
111
+ switch (state) {
112
+ case 'Delivered':
113
+ return 'green';
114
+ case 'Cancelled':
115
+ return 'red';
116
+ default:
117
+ return 'blue';
118
+ }
119
+ }
120
+
121
+ // Entity type mappings from the dev-config.ts - using functions to generate configs
122
+ const createEntityConfigs = (i18n: any) => ({
123
+ Product: createRelationSelectorConfig({
124
+ ...createBaseEntityConfig('Product', i18n),
125
+ listQuery: graphql(`
126
+ query GetProductsForRelationSelector($options: ProductListOptions) {
127
+ products(options: $options) {
128
+ items {
129
+ id
130
+ name
131
+ slug
132
+ enabled
133
+ featuredAsset {
134
+ id
135
+ preview
136
+ }
137
+ }
138
+ totalItems
139
+ }
140
+ }
141
+ `),
142
+ label: (item: any) => (
143
+ <EntityLabel
144
+ title={item.name}
145
+ subtitle={`${item.slug}${!item.enabled ? ' • Disabled' : ''}`}
146
+ imageUrl={item.featuredAsset?.preview}
147
+ placeholderLetter="P"
148
+ tooltipText={`${item.name} (${item.slug})`}
149
+ />
150
+ ),
151
+ }),
152
+
153
+ Customer: createRelationSelectorConfig({
154
+ ...createBaseEntityConfig('Customer', i18n, 'emailAddress', 'emailAddress'),
155
+ listQuery: graphql(`
156
+ query GetCustomersForRelationSelector($options: CustomerListOptions) {
157
+ customers(options: $options) {
158
+ items {
159
+ id
160
+ firstName
161
+ lastName
162
+ emailAddress
163
+ phoneNumber
164
+ user {
165
+ verified
166
+ }
167
+ }
168
+ totalItems
169
+ }
170
+ }
171
+ `),
172
+ label: (item: any) => (
173
+ <EntityLabel
174
+ title={`${item.firstName} ${item.lastName}`}
175
+ subtitle={[item.emailAddress, item.phoneNumber].filter(Boolean).join(' • ')}
176
+ placeholderLetter={
177
+ item.firstName?.[0]?.toUpperCase() || item.emailAddress?.[0]?.toUpperCase() || 'U'
178
+ }
179
+ rounded
180
+ statusIndicator={<StatusBadge condition={!item.user?.verified} text="Unverified" />}
181
+ tooltipText={`${item.firstName} ${item.lastName} (${item.emailAddress})`}
182
+ />
183
+ ),
184
+ }),
185
+
186
+ ProductVariant: createRelationSelectorConfig({
187
+ ...createBaseEntityConfig('Product Variant', i18n),
188
+ listQuery: graphql(`
189
+ query GetProductVariantsForRelationSelector($options: ProductVariantListOptions) {
190
+ productVariants(options: $options) {
191
+ items {
192
+ id
193
+ name
194
+ sku
195
+ enabled
196
+ stockOnHand
197
+ product {
198
+ name
199
+ featuredAsset {
200
+ id
201
+ preview
202
+ }
203
+ }
204
+ featuredAsset {
205
+ id
206
+ preview
207
+ }
208
+ }
209
+ totalItems
210
+ }
211
+ }
212
+ `),
213
+ label: (item: any) => (
214
+ <EntityLabel
215
+ title={`${item.product.name} - ${item.name}`}
216
+ subtitle={`SKU: ${item.sku} • Stock: ${item.stockOnHand ?? 0}`}
217
+ imageUrl={item.featuredAsset?.preview || item.product.featuredAsset?.preview}
218
+ placeholderLetter="V"
219
+ statusIndicator={<StatusBadge condition={!item.enabled} text="Disabled" />}
220
+ tooltipText={`${item.product.name} - ${item.name} (SKU: ${item.sku})`}
221
+ />
222
+ ),
223
+ }),
224
+
225
+ Collection: createRelationSelectorConfig({
226
+ ...createBaseEntityConfig('Collection', i18n),
227
+ listQuery: graphql(`
228
+ query GetCollectionsForRelationSelector($options: CollectionListOptions) {
229
+ collections(options: $options) {
230
+ items {
231
+ id
232
+ name
233
+ slug
234
+ isPrivate
235
+ position
236
+ productVariants {
237
+ totalItems
238
+ }
239
+ featuredAsset {
240
+ id
241
+ preview
242
+ }
243
+ }
244
+ totalItems
245
+ }
246
+ }
247
+ `),
248
+ label: (item: any) => (
249
+ <EntityLabel
250
+ title={item.name}
251
+ subtitle={`${item.slug} • ${item.productVariants?.totalItems || 0} products`}
252
+ imageUrl={item.featuredAsset?.preview}
253
+ placeholderLetter="C"
254
+ statusIndicator={<StatusBadge condition={item.isPrivate} text="Private" />}
255
+ tooltipText={`${item.name} (${item.slug})`}
256
+ />
257
+ ),
258
+ }),
259
+
260
+ Facet: createRelationSelectorConfig({
261
+ ...createBaseEntityConfig('Facet', i18n),
262
+ listQuery: graphql(`
263
+ query GetFacetsForRelationSelector($options: FacetListOptions) {
264
+ facets(options: $options) {
265
+ items {
266
+ id
267
+ name
268
+ code
269
+ isPrivate
270
+ valueList {
271
+ totalItems
272
+ }
273
+ }
274
+ totalItems
275
+ }
276
+ }
277
+ `),
278
+ label: (item: any) => (
279
+ <EntityLabel
280
+ title={item.name}
281
+ subtitle={`${item.code} • ${item.valueList?.totalItems || 0} values`}
282
+ placeholderLetter="F"
283
+ rounded
284
+ statusIndicator={<StatusBadge condition={item.isPrivate} text="Private" />}
285
+ tooltipText={`${item.name} (${item.code})`}
286
+ />
287
+ ),
288
+ }),
289
+
290
+ FacetValue: createRelationSelectorConfig({
291
+ ...createBaseEntityConfig('Facet Value', i18n),
292
+ listQuery: graphql(`
293
+ query GetFacetValuesForRelationSelector($options: FacetValueListOptions) {
294
+ facetValues(options: $options) {
295
+ items {
296
+ id
297
+ name
298
+ code
299
+ facet {
300
+ name
301
+ code
302
+ }
303
+ }
304
+ totalItems
305
+ }
306
+ }
307
+ `),
308
+ label: (item: any) => (
309
+ <EntityLabel
310
+ title={item.name}
311
+ subtitle={`${item.facet.name} • ${item.code}`}
312
+ placeholderLetter="FV"
313
+ rounded
314
+ tooltipText={`${item.facet.name}: ${item.name} (${item.code})`}
315
+ />
316
+ ),
317
+ }),
318
+
319
+ Asset: createRelationSelectorConfig({
320
+ ...createBaseEntityConfig('Asset', i18n),
321
+ listQuery: graphql(`
322
+ query GetAssetsForRelationSelector($options: AssetListOptions) {
323
+ assets(options: $options) {
324
+ items {
325
+ id
326
+ name
327
+ preview
328
+ source
329
+ mimeType
330
+ fileSize
331
+ width
332
+ height
333
+ }
334
+ totalItems
335
+ }
336
+ }
337
+ `),
338
+ label: (item: any) => {
339
+ const dimensions = item.width && item.height ? `${item.width}×${item.height}` : '';
340
+ const fileSize = item.fileSize ? `${Math.round(item.fileSize / 1024)}KB` : '';
341
+ const subtitle = [item.mimeType, dimensions, fileSize].filter(Boolean).join(' • ');
342
+ const tooltipDetails = [item.mimeType, dimensions, fileSize].filter(Boolean).join(', ');
343
+
344
+ return (
345
+ <EntityLabel
346
+ title={item.name}
347
+ subtitle={subtitle}
348
+ imageUrl={item.preview}
349
+ placeholderLetter="A"
350
+ tooltipText={`${item.name} (${tooltipDetails})`}
351
+ />
352
+ );
353
+ },
354
+ }),
355
+
356
+ Order: createRelationSelectorConfig({
357
+ ...createBaseEntityConfig('Order', i18n, 'code', 'code'),
358
+ listQuery: graphql(`
359
+ query GetOrdersForRelationSelector($options: OrderListOptions) {
360
+ orders(options: $options) {
361
+ items {
362
+ id
363
+ code
364
+ state
365
+ totalWithTax
366
+ currencyCode
367
+ orderPlacedAt
368
+ customer {
369
+ firstName
370
+ lastName
371
+ emailAddress
372
+ }
373
+ }
374
+ totalItems
375
+ }
376
+ }
377
+ `),
378
+ label: (item: any) => {
379
+ const stateVariant = getOrderStateVariant(item.state);
380
+ return (
381
+ <EntityLabel
382
+ title={item.code}
383
+ subtitle={`${item.customer?.firstName} ${item.customer?.lastName} • ${item.totalWithTax / 100} ${item.currencyCode}`}
384
+ placeholderLetter="O"
385
+ rounded
386
+ statusIndicator={
387
+ <StatusBadge condition={true} text={item.state} variant={stateVariant} />
388
+ }
389
+ tooltipText={`${item.code} - ${item.customer?.firstName} ${item.customer?.lastName} (${item.totalWithTax / 100} ${item.currencyCode})`}
390
+ />
391
+ );
392
+ },
393
+ }),
394
+
395
+ // OrderLine: Not available as a list query in the admin API, fallback to basic input
396
+
397
+ ShippingMethod: createRelationSelectorConfig({
398
+ ...createBaseEntityConfig('Shipping Method', i18n),
399
+ listQuery: graphql(`
400
+ query GetShippingMethodsForRelationSelector($options: ShippingMethodListOptions) {
401
+ shippingMethods(options: $options) {
402
+ items {
403
+ id
404
+ name
405
+ code
406
+ description
407
+ fulfillmentHandlerCode
408
+ }
409
+ totalItems
410
+ }
411
+ }
412
+ `),
413
+ label: (item: any) => (
414
+ <EntityLabel
415
+ title={item.name}
416
+ subtitle={`${item.code} • ${item.fulfillmentHandlerCode}`}
417
+ placeholderLetter="S"
418
+ rounded
419
+ tooltipText={`${item.name} (${item.code})`}
420
+ />
421
+ ),
422
+ }),
423
+
424
+ PaymentMethod: createRelationSelectorConfig({
425
+ ...createBaseEntityConfig('Payment Method', i18n),
426
+ listQuery: graphql(`
427
+ query GetPaymentMethodsForRelationSelector($options: PaymentMethodListOptions) {
428
+ paymentMethods(options: $options) {
429
+ items {
430
+ id
431
+ name
432
+ code
433
+ description
434
+ enabled
435
+ handler {
436
+ code
437
+ }
438
+ }
439
+ totalItems
440
+ }
441
+ }
442
+ `),
443
+ label: (item: any) => (
444
+ <EntityLabel
445
+ title={item.name}
446
+ subtitle={`${item.code} • ${item.handler?.code}`}
447
+ placeholderLetter="P"
448
+ rounded
449
+ statusIndicator={<StatusBadge condition={!item.enabled} text="Disabled" />}
450
+ tooltipText={`${item.name} (${item.code})`}
451
+ />
452
+ ),
453
+ }),
454
+
455
+ Channel: createRelationSelectorConfig({
456
+ ...createBaseEntityConfig('Channel', i18n, 'code', 'code'),
457
+ listQuery: graphql(`
458
+ query GetChannelsForRelationSelector($options: ChannelListOptions) {
459
+ channels(options: $options) {
460
+ items {
461
+ id
462
+ code
463
+ token
464
+ defaultLanguageCode
465
+ currencyCode
466
+ pricesIncludeTax
467
+ }
468
+ totalItems
469
+ }
470
+ }
471
+ `),
472
+ label: (item: any) => (
473
+ <EntityLabel
474
+ title={item.code}
475
+ subtitle={`${item.defaultLanguageCode} • ${item.currencyCode} • ${item.pricesIncludeTax ? 'Inc. Tax' : 'Ex. Tax'}`}
476
+ placeholderLetter="CH"
477
+ rounded
478
+ tooltipText={`${item.code} (${item.defaultLanguageCode}, ${item.currencyCode})`}
479
+ />
480
+ ),
481
+ }),
482
+
483
+ CustomerGroup: createRelationSelectorConfig({
484
+ ...createBaseEntityConfig('Customer Group', i18n),
485
+ listQuery: graphql(`
486
+ query GetCustomerGroupsForRelationSelector($options: CustomerGroupListOptions) {
487
+ customerGroups(options: $options) {
488
+ items {
489
+ id
490
+ name
491
+ customers {
492
+ totalItems
493
+ }
494
+ }
495
+ totalItems
496
+ }
497
+ }
498
+ `),
499
+ label: (item: any) => (
500
+ <EntityLabel
501
+ title={item.name}
502
+ subtitle={`${item.customers?.totalItems || 0} customers`}
503
+ placeholderLetter="CG"
504
+ rounded
505
+ tooltipText={`${item.name} (${item.customers?.totalItems || 0} customers)`}
506
+ />
507
+ ),
508
+ }),
509
+
510
+ Promotion: createRelationSelectorConfig({
511
+ ...createBaseEntityConfig('Promotion', i18n),
512
+ listQuery: graphql(`
513
+ query GetPromotionsForRelationSelector($options: PromotionListOptions) {
514
+ promotions(options: $options) {
515
+ items {
516
+ id
517
+ name
518
+ couponCode
519
+ enabled
520
+ startsAt
521
+ endsAt
522
+ }
523
+ totalItems
524
+ }
525
+ }
526
+ `),
527
+ label: (item: any) => {
528
+ const parts = [
529
+ item.couponCode,
530
+ item.startsAt && `Starts: ${new Date(item.startsAt).toLocaleDateString()}`,
531
+ item.endsAt && `Ends: ${new Date(item.endsAt).toLocaleDateString()}`,
532
+ ].filter(Boolean);
533
+
534
+ return (
535
+ <EntityLabel
536
+ title={item.name}
537
+ subtitle={parts.join(' • ')}
538
+ placeholderLetter="PR"
539
+ rounded
540
+ statusIndicator={<StatusBadge condition={!item.enabled} text="Disabled" />}
541
+ tooltipText={item.couponCode ? `${item.name} (${item.couponCode})` : item.name}
542
+ />
543
+ );
544
+ },
545
+ }),
546
+ });
547
+
548
+ interface DefaultRelationInputProps {
549
+ fieldDef: RelationCustomFieldConfig;
550
+ field: ControllerRenderProps<any, any>;
551
+ disabled?: boolean;
552
+ }
553
+
554
+ export function DefaultRelationInput({ fieldDef, field, disabled }: Readonly<DefaultRelationInputProps>) {
555
+ const { i18n } = useLingui();
556
+ const entityName = fieldDef.entity;
557
+ const ENTITY_CONFIGS = createEntityConfigs(i18n);
558
+ const config = ENTITY_CONFIGS[entityName as keyof typeof ENTITY_CONFIGS];
559
+
560
+ if (!config) {
561
+ // Fallback to plain input if entity type not found
562
+ console.warn(`No relation selector config found for entity: ${entityName}`);
563
+ return (
564
+ <input
565
+ value={field.value ?? ''}
566
+ onChange={e => field.onChange(e.target.value)}
567
+ onBlur={field.onBlur}
568
+ name={field.name}
569
+ disabled={disabled}
570
+ placeholder={`Enter ${entityName} ID`}
571
+ className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
572
+ />
573
+ );
574
+ }
575
+
576
+ const isList = fieldDef.list ?? false;
577
+
578
+ if (isList) {
579
+ return (
580
+ <MultiRelationInput
581
+ value={field.value ?? []}
582
+ onChange={field.onChange}
583
+ config={config}
584
+ disabled={disabled}
585
+ selectorLabel={<Trans>Select {entityName.toLowerCase()}s</Trans>}
586
+ />
587
+ );
588
+ } else {
589
+ return (
590
+ <SingleRelationInput
591
+ value={field.value ?? ''}
592
+ onChange={field.onChange}
593
+ config={config}
594
+ disabled={disabled}
595
+ selectorLabel={<Trans>Select {entityName.toLowerCase()}</Trans>}
596
+ />
597
+ );
598
+ }
599
+ }
@@ -5,6 +5,12 @@ export * from './datetime-input.js';
5
5
  export * from './facet-value-input.js';
6
6
  export * from './money-input.js';
7
7
  export * from './rich-text-input.js';
8
+ export * from './select-with-options.js';
9
+
10
+ // Enhanced configurable operation input components
11
+ export * from './configurable-operation-list-input.js';
12
+ export * from './customer-group-selector-input.js';
13
+ export * from './product-selector-input.js';
8
14
 
9
15
  // Relation selector components
10
16
  export * from './relation-input.js';