@vendure/dashboard 3.3.8-master-202507290247 → 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,472 @@
1
+ import { FieldInfo } from '@/vdb/framework/document-introspection/get-document-structure.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createFormSchemaFromFields, getZodTypeFromField } from './form-schema-tools.js';
5
+
6
+ // Helper to create mock FieldInfo
7
+ const createMockField = (
8
+ name: string,
9
+ type: string,
10
+ nullable = false,
11
+ list = false,
12
+ typeInfo?: FieldInfo[],
13
+ ): FieldInfo => ({
14
+ name,
15
+ type,
16
+ nullable,
17
+ list,
18
+ typeInfo,
19
+ isPaginatedList: false,
20
+ isScalar: false,
21
+ });
22
+
23
+ // Helper to create mock CustomFieldConfig
24
+ const createMockCustomField = (
25
+ name: string,
26
+ type: string,
27
+ options: {
28
+ pattern?: string;
29
+ intMin?: number;
30
+ intMax?: number;
31
+ floatMin?: number;
32
+ floatMax?: number;
33
+ datetimeMin?: string;
34
+ datetimeMax?: string;
35
+ list?: boolean;
36
+ nullable?: boolean;
37
+ } = {},
38
+ ) => ({
39
+ name,
40
+ type,
41
+ ...options,
42
+ });
43
+
44
+ describe('form-schema-tools', () => {
45
+ describe('getZodTypeFromField', () => {
46
+ it('should create string type for String fields', () => {
47
+ const field = createMockField('name', 'String');
48
+ const schema = getZodTypeFromField(field);
49
+
50
+ expect(() => schema.parse('test')).not.toThrow();
51
+ expect(() => schema.parse(123)).toThrow();
52
+ });
53
+
54
+ it('should create number type for Int fields', () => {
55
+ const field = createMockField('age', 'Int');
56
+ const schema = getZodTypeFromField(field);
57
+
58
+ expect(() => schema.parse(25)).not.toThrow();
59
+ expect(() => schema.parse('25')).toThrow();
60
+ });
61
+
62
+ it('should create number type for Float fields', () => {
63
+ const field = createMockField('price', 'Float');
64
+ const schema = getZodTypeFromField(field);
65
+
66
+ expect(() => schema.parse(29.99)).not.toThrow();
67
+ expect(() => schema.parse('29.99')).toThrow();
68
+ });
69
+
70
+ it('should create boolean type for Boolean fields', () => {
71
+ const field = createMockField('active', 'Boolean');
72
+ const schema = getZodTypeFromField(field);
73
+
74
+ expect(() => schema.parse(true)).not.toThrow();
75
+ expect(() => schema.parse('true')).toThrow();
76
+ });
77
+
78
+ it('should handle nullable fields', () => {
79
+ const field = createMockField('optional', 'String', true);
80
+ const schema = getZodTypeFromField(field);
81
+
82
+ expect(() => schema.parse('test')).not.toThrow();
83
+ expect(() => schema.parse(null)).not.toThrow();
84
+ expect(() => schema.parse(undefined)).not.toThrow();
85
+ });
86
+
87
+ it('should handle list fields', () => {
88
+ const field = createMockField('tags', 'String', false, true);
89
+ const schema = getZodTypeFromField(field);
90
+
91
+ expect(() => schema.parse(['tag1', 'tag2'])).not.toThrow();
92
+ expect(() => schema.parse('tag1')).toThrow();
93
+ });
94
+ });
95
+
96
+ describe('createFormSchemaFromFields - basic functionality', () => {
97
+ it('should create schema for simple fields', () => {
98
+ const fields = [
99
+ createMockField('name', 'String'),
100
+ createMockField('age', 'Int'),
101
+ createMockField('active', 'Boolean'),
102
+ ];
103
+
104
+ const schema = createFormSchemaFromFields(fields);
105
+
106
+ const validData = { name: 'John', age: 25, active: true };
107
+ expect(() => schema.parse(validData)).not.toThrow();
108
+
109
+ const invalidData = { name: 123, age: 'twenty', active: 'yes' };
110
+ expect(() => schema.parse(invalidData)).toThrow();
111
+ });
112
+
113
+ it('should handle nested objects', () => {
114
+ const fields = [
115
+ createMockField('user', 'Object', false, false, [
116
+ createMockField('name', 'String'),
117
+ createMockField('email', 'String'),
118
+ ]),
119
+ ];
120
+
121
+ const schema = createFormSchemaFromFields(fields);
122
+
123
+ const validData = {
124
+ user: {
125
+ name: 'John',
126
+ email: 'john@example.com',
127
+ },
128
+ };
129
+ expect(() => schema.parse(validData)).not.toThrow();
130
+ });
131
+ });
132
+
133
+ describe('createFormSchemaFromFields - custom fields (root context)', () => {
134
+ it('should apply string pattern validation for root custom fields', () => {
135
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
136
+ const customFields = [createMockCustomField('sku', 'string', { pattern: '^[A-Z]{2}-\\d{4}$' })];
137
+
138
+ const schema = createFormSchemaFromFields(fields, customFields, false);
139
+
140
+ const validData = { customFields: { sku: 'AB-1234' } };
141
+ expect(() => schema.parse(validData)).not.toThrow();
142
+
143
+ const invalidData = { customFields: { sku: 'invalid-sku' } };
144
+ expect(() => schema.parse(invalidData)).toThrow();
145
+ });
146
+
147
+ it('should apply int min/max validation for root custom fields', () => {
148
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
149
+ const customFields = [createMockCustomField('quantity', 'int', { intMin: 1, intMax: 100 })];
150
+
151
+ const schema = createFormSchemaFromFields(fields, customFields, false);
152
+
153
+ const validData = { customFields: { quantity: 50 } };
154
+ expect(() => schema.parse(validData)).not.toThrow();
155
+
156
+ const belowMinData = { customFields: { quantity: 0 } };
157
+ expect(() => schema.parse(belowMinData)).toThrow();
158
+
159
+ const aboveMaxData = { customFields: { quantity: 101 } };
160
+ expect(() => schema.parse(aboveMaxData)).toThrow();
161
+ });
162
+
163
+ it('should apply float min/max validation for root custom fields', () => {
164
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
165
+ const customFields = [
166
+ createMockCustomField('weight', 'float', { floatMin: 0.1, floatMax: 999.9 }),
167
+ ];
168
+
169
+ const schema = createFormSchemaFromFields(fields, customFields, false);
170
+
171
+ const validData = { customFields: { weight: 10.5 } };
172
+ expect(() => schema.parse(validData)).not.toThrow();
173
+
174
+ const belowMinData = { customFields: { weight: 0.05 } };
175
+ expect(() => schema.parse(belowMinData)).toThrow();
176
+
177
+ const aboveMaxData = { customFields: { weight: 1000.0 } };
178
+ expect(() => schema.parse(aboveMaxData)).toThrow();
179
+ });
180
+
181
+ it('should apply datetime min/max validation for root custom fields', () => {
182
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
183
+ const customFields = [
184
+ createMockCustomField('releaseDate', 'datetime', {
185
+ datetimeMin: '2020-01-01T00:00:00.000Z',
186
+ datetimeMax: '2025-12-31T23:59:59.999Z',
187
+ }),
188
+ ];
189
+
190
+ const schema = createFormSchemaFromFields(fields, customFields, false);
191
+
192
+ // Test with string
193
+ const validDataString = { customFields: { releaseDate: '2023-06-15T12:00:00.000Z' } };
194
+ expect(() => schema.parse(validDataString)).not.toThrow();
195
+
196
+ // Test with Date object
197
+ const validDataDate = { customFields: { releaseDate: new Date('2023-06-15T12:00:00.000Z') } };
198
+ expect(() => schema.parse(validDataDate)).not.toThrow();
199
+
200
+ const beforeMinData = { customFields: { releaseDate: '2019-12-31T23:59:59.999Z' } };
201
+ expect(() => schema.parse(beforeMinData)).toThrow();
202
+
203
+ const afterMaxData = { customFields: { releaseDate: '2026-01-01T00:00:00.000Z' } };
204
+ expect(() => schema.parse(afterMaxData)).toThrow();
205
+ });
206
+
207
+ it('should handle boolean custom fields', () => {
208
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
209
+ const customFields = [createMockCustomField('featured', 'boolean')];
210
+
211
+ const schema = createFormSchemaFromFields(fields, customFields, false);
212
+
213
+ const validData = { customFields: { featured: true } };
214
+ expect(() => schema.parse(validData)).not.toThrow();
215
+
216
+ const invalidData = { customFields: { featured: 'yes' } };
217
+ expect(() => schema.parse(invalidData)).toThrow();
218
+ });
219
+
220
+ it('should handle list custom fields', () => {
221
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
222
+ const customFields = [createMockCustomField('tags', 'string', { list: true })];
223
+
224
+ const schema = createFormSchemaFromFields(fields, customFields, false);
225
+
226
+ const validData = { customFields: { tags: ['tag1', 'tag2'] } };
227
+ expect(() => schema.parse(validData)).not.toThrow();
228
+
229
+ const invalidData = { customFields: { tags: 'single-tag' } };
230
+ expect(() => schema.parse(invalidData)).toThrow();
231
+ });
232
+
233
+ it('should handle nullable custom fields', () => {
234
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
235
+ const customFields = [createMockCustomField('optionalField', 'string', { nullable: true })];
236
+
237
+ const schema = createFormSchemaFromFields(fields, customFields, false);
238
+
239
+ const validData = { customFields: { optionalField: 'value' } };
240
+ expect(() => schema.parse(validData)).not.toThrow();
241
+
242
+ const nullData = { customFields: { optionalField: null } };
243
+ expect(() => schema.parse(nullData)).not.toThrow();
244
+
245
+ const undefinedData = { customFields: { optionalField: undefined } };
246
+ expect(() => schema.parse(undefinedData)).not.toThrow();
247
+ });
248
+
249
+ it('should only include non-translatable fields in root context', () => {
250
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
251
+ const customFields = [
252
+ createMockCustomField('sku', 'string'), // Should be included
253
+ createMockCustomField('description', 'localeString'), // Should be excluded
254
+ createMockCustomField('content', 'localeText'), // Should be excluded
255
+ createMockCustomField('quantity', 'int'), // Should be included
256
+ ];
257
+
258
+ const schema = createFormSchemaFromFields(fields, customFields, false);
259
+
260
+ // Should accept non-translatable fields
261
+ const validData = { customFields: { sku: 'AB-123', quantity: 5 } };
262
+ expect(() => schema.parse(validData)).not.toThrow();
263
+
264
+ // Should reject translatable fields in root context
265
+ const withTranslatableData = {
266
+ customFields: {
267
+ sku: 'AB-123',
268
+ description: 'Some description', // This should cause validation to fail
269
+ },
270
+ };
271
+ // Note: This might not throw because Zod ignores extra properties by default
272
+ // The important thing is that the schema doesn't validate translatable fields
273
+ });
274
+ });
275
+
276
+ describe('createFormSchemaFromFields - custom fields (translation context)', () => {
277
+ it('should handle localeString custom fields in translation context', () => {
278
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
279
+ const customFields = [
280
+ createMockCustomField('description', 'localeString', { pattern: '^[A-Za-z\\s]+$' }),
281
+ ];
282
+
283
+ const schema = createFormSchemaFromFields(fields, customFields, true);
284
+
285
+ const validData = { customFields: { description: 'Valid Description' } };
286
+ expect(() => schema.parse(validData)).not.toThrow();
287
+
288
+ const invalidData = { customFields: { description: 'Invalid123' } };
289
+ expect(() => schema.parse(invalidData)).toThrow();
290
+ });
291
+
292
+ it('should handle localeText custom fields in translation context', () => {
293
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
294
+ const customFields = [createMockCustomField('content', 'localeText')];
295
+
296
+ const schema = createFormSchemaFromFields(fields, customFields, true);
297
+
298
+ const validData = { customFields: { content: 'Some long text content' } };
299
+ expect(() => schema.parse(validData)).not.toThrow();
300
+
301
+ const invalidData = { customFields: { content: 123 } };
302
+ expect(() => schema.parse(invalidData)).toThrow();
303
+ });
304
+
305
+ it('should only include translatable fields in translation context', () => {
306
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
307
+ const customFields = [
308
+ createMockCustomField('sku', 'string'), // Should be excluded
309
+ createMockCustomField('description', 'localeString'), // Should be included
310
+ createMockCustomField('content', 'localeText'), // Should be included
311
+ createMockCustomField('quantity', 'int'), // Should be excluded
312
+ ];
313
+
314
+ const schema = createFormSchemaFromFields(fields, customFields, true);
315
+
316
+ // Should accept translatable fields
317
+ const validData = {
318
+ customFields: {
319
+ description: 'Description',
320
+ content: 'Content',
321
+ },
322
+ };
323
+ expect(() => schema.parse(validData)).not.toThrow();
324
+ });
325
+ });
326
+
327
+ describe('createFormSchemaFromFields - translation handling', () => {
328
+ it('should handle translations field with custom fields', () => {
329
+ const fields = [
330
+ createMockField('name', 'String'),
331
+ createMockField('translations', 'Object', false, true, [
332
+ createMockField('id', 'ID'),
333
+ createMockField('languageCode', 'String'),
334
+ createMockField('name', 'String'),
335
+ createMockField('customFields', 'Object', false, false, []),
336
+ ]),
337
+ createMockField('customFields', 'Object', false, false, []),
338
+ ];
339
+ const customFields = [
340
+ createMockCustomField('sku', 'string'), // Root custom field
341
+ createMockCustomField('description', 'localeString'), // Translation custom field
342
+ ];
343
+
344
+ const schema = createFormSchemaFromFields(fields, customFields, false);
345
+
346
+ const validData = {
347
+ name: 'Product Name',
348
+ customFields: { sku: 'AB-123' }, // Root custom fields
349
+ translations: [
350
+ {
351
+ id: '1',
352
+ languageCode: 'en',
353
+ name: 'English Name',
354
+ customFields: { description: 'English description' }, // Translation custom fields
355
+ },
356
+ ],
357
+ };
358
+
359
+ expect(() => schema.parse(validData)).not.toThrow();
360
+ });
361
+ });
362
+
363
+ describe('createFormSchemaFromFields - error messages', () => {
364
+ it('should provide clear error messages for pattern validation', () => {
365
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
366
+ const customFields = [createMockCustomField('sku', 'string', { pattern: '^[A-Z]{2}-\\d{4}$' })];
367
+
368
+ const schema = createFormSchemaFromFields(fields, customFields, false);
369
+
370
+ try {
371
+ schema.parse({ customFields: { sku: 'invalid' } });
372
+ expect.fail('Should have thrown validation error');
373
+ } catch (error: any) {
374
+ expect(error.errors[0].message).toContain('Value must match pattern');
375
+ }
376
+ });
377
+
378
+ it('should provide clear error messages for min validation', () => {
379
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
380
+ const customFields = [createMockCustomField('quantity', 'int', { intMin: 1 })];
381
+
382
+ const schema = createFormSchemaFromFields(fields, customFields, false);
383
+
384
+ try {
385
+ schema.parse({ customFields: { quantity: 0 } });
386
+ expect.fail('Should have thrown validation error');
387
+ } catch (error: any) {
388
+ expect(error.errors[0].message).toContain('Value must be at least 1');
389
+ }
390
+ });
391
+
392
+ it('should provide clear error messages for max validation', () => {
393
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
394
+ const customFields = [createMockCustomField('quantity', 'int', { intMax: 100 })];
395
+
396
+ const schema = createFormSchemaFromFields(fields, customFields, false);
397
+
398
+ try {
399
+ schema.parse({ customFields: { quantity: 101 } });
400
+ expect.fail('Should have thrown validation error');
401
+ } catch (error: any) {
402
+ expect(error.errors[0].message).toContain('Value must be at most 100');
403
+ }
404
+ });
405
+
406
+ it('should provide clear error messages for datetime validation', () => {
407
+ const fields = [createMockField('customFields', 'Object', false, false, [])];
408
+ const customFields = [
409
+ createMockCustomField('releaseDate', 'datetime', {
410
+ datetimeMin: '2020-01-01T00:00:00.000Z',
411
+ }),
412
+ ];
413
+
414
+ const schema = createFormSchemaFromFields(fields, customFields, false);
415
+
416
+ try {
417
+ schema.parse({ customFields: { releaseDate: '2019-12-31T23:59:59.999Z' } });
418
+ expect.fail('Should have thrown validation error');
419
+ } catch (error: any) {
420
+ expect(error.issues).toBeDefined();
421
+ expect(error.issues.length).toBeGreaterThan(0);
422
+ expect(error.issues[0].message).toContain('Date must be after');
423
+ }
424
+
425
+ // Test with Date object as well
426
+ try {
427
+ schema.parse({ customFields: { releaseDate: new Date('2019-12-31T23:59:59.999Z') } });
428
+ expect.fail('Should have thrown validation error');
429
+ } catch (error: any) {
430
+ expect(error.issues).toBeDefined();
431
+ expect(error.issues.length).toBeGreaterThan(0);
432
+ expect(error.issues[0].message).toContain('Date must be after');
433
+ }
434
+ });
435
+ });
436
+
437
+ describe('createFormSchemaFromFields - edge cases', () => {
438
+ it('should handle empty custom field config', () => {
439
+ const fields = [
440
+ createMockField('name', 'String'),
441
+ createMockField('customFields', 'Object', false, false, []),
442
+ ];
443
+
444
+ const schema = createFormSchemaFromFields(fields, []);
445
+
446
+ const validData = { name: 'Test', customFields: {} };
447
+ expect(() => schema.parse(validData)).not.toThrow();
448
+ });
449
+
450
+ it('should handle no custom field config', () => {
451
+ const fields = [
452
+ createMockField('name', 'String'),
453
+ createMockField('customFields', 'Object', false, false, []),
454
+ ];
455
+
456
+ const schema = createFormSchemaFromFields(fields);
457
+
458
+ const validData = { name: 'Test' };
459
+ expect(() => schema.parse(validData)).not.toThrow();
460
+ });
461
+
462
+ it('should handle fields without customFields', () => {
463
+ const fields = [createMockField('name', 'String'), createMockField('age', 'Int')];
464
+ const customFields = [createMockCustomField('sku', 'string')];
465
+
466
+ const schema = createFormSchemaFromFields(fields, customFields);
467
+
468
+ const validData = { name: 'Test', age: 25 };
469
+ expect(() => schema.parse(validData)).not.toThrow();
470
+ });
471
+ });
472
+ });