@vendure/dashboard 3.3.8-master-202507290247 → 3.3.8-master-202507310242
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 +4 -4
- package/src/app/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +11 -78
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +16 -8
- package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +11 -81
- package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +10 -77
- package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +12 -87
- package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +12 -87
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +10 -80
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +10 -79
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +8 -6
- package/src/app/routes/_authenticated/_system/components/payload-dialog.tsx +1 -1
- package/src/app/routes/_authenticated/_system/job-queue.graphql.ts +11 -0
- package/src/app/routes/_authenticated/_system/job-queue.tsx +99 -5
- package/src/lib/components/data-input/combination-mode-input.tsx +52 -0
- package/src/lib/components/data-input/configurable-operation-list-input.tsx +433 -0
- package/src/lib/components/data-input/custom-field-list-input.tsx +297 -0
- package/src/lib/components/data-input/datetime-input.tsx +5 -2
- package/src/lib/components/data-input/default-relation-input.tsx +599 -0
- package/src/lib/components/data-input/index.ts +6 -0
- package/src/lib/components/data-input/product-multi-selector.tsx +426 -0
- package/src/lib/components/data-input/relation-selector.tsx +7 -6
- package/src/lib/components/data-input/select-with-options.tsx +84 -0
- package/src/lib/components/data-input/struct-form-input.tsx +324 -0
- package/src/lib/components/shared/configurable-operation-arg-input.tsx +365 -21
- package/src/lib/components/shared/configurable-operation-input.tsx +81 -41
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +260 -0
- package/src/lib/components/shared/configurable-operation-selector.tsx +156 -0
- package/src/lib/components/shared/custom-fields-form.tsx +208 -37
- package/src/lib/components/shared/multi-select.tsx +1 -1
- package/src/lib/components/ui/form.tsx +4 -4
- package/src/lib/framework/extension-api/input-component-extensions.tsx +5 -1
- package/src/lib/framework/form-engine/form-schema-tools.spec.ts +472 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +340 -5
- package/src/lib/framework/form-engine/use-generated-form.tsx +24 -8
- package/src/lib/framework/form-engine/utils.ts +3 -9
- package/src/lib/framework/layout-engine/page-layout.tsx +11 -3
- package/src/lib/framework/page/list-page.tsx +9 -0
- package/src/lib/framework/page/use-detail-page.ts +3 -3
- package/src/lib/lib/utils.ts +26 -24
|
@@ -3,26 +3,348 @@ import {
|
|
|
3
3
|
isEnumType,
|
|
4
4
|
isScalarType,
|
|
5
5
|
} from '@/vdb/framework/document-introspection/get-document-structure.js';
|
|
6
|
+
import { StructCustomFieldConfig } from '@vendure/common/lib/generated-types';
|
|
7
|
+
import { ResultOf } from 'gql.tada';
|
|
6
8
|
import { z, ZodRawShape, ZodType, ZodTypeAny } from 'zod';
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
import { structCustomFieldFragment } from '../../providers/server-config.js';
|
|
11
|
+
|
|
12
|
+
type CustomFieldConfig = {
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
pattern?: string;
|
|
16
|
+
intMin?: number;
|
|
17
|
+
intMax?: number;
|
|
18
|
+
floatMin?: number;
|
|
19
|
+
floatMax?: number;
|
|
20
|
+
datetimeMin?: string;
|
|
21
|
+
datetimeMax?: string;
|
|
22
|
+
list?: boolean;
|
|
23
|
+
nullable?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type StructFieldConfig = ResultOf<typeof structCustomFieldFragment>['fields'][number];
|
|
27
|
+
|
|
28
|
+
function mapGraphQLCustomFieldToConfig(field: StructFieldConfig): CustomFieldConfig {
|
|
29
|
+
const baseConfig = {
|
|
30
|
+
name: field.name,
|
|
31
|
+
type: field.type,
|
|
32
|
+
list: field.list ?? false,
|
|
33
|
+
nullable: true, // Default to true since GraphQL fields are nullable by default
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
switch (field.__typename) {
|
|
37
|
+
case 'StringStructFieldConfig':
|
|
38
|
+
return {
|
|
39
|
+
...baseConfig,
|
|
40
|
+
pattern: field.pattern ?? undefined,
|
|
41
|
+
};
|
|
42
|
+
case 'IntStructFieldConfig':
|
|
43
|
+
return {
|
|
44
|
+
...baseConfig,
|
|
45
|
+
intMin: field.intMin ?? undefined,
|
|
46
|
+
intMax: field.intMax ?? undefined,
|
|
47
|
+
};
|
|
48
|
+
case 'FloatStructFieldConfig':
|
|
49
|
+
return {
|
|
50
|
+
...baseConfig,
|
|
51
|
+
floatMin: field.floatMin ?? undefined,
|
|
52
|
+
floatMax: field.floatMax ?? undefined,
|
|
53
|
+
};
|
|
54
|
+
case 'DateTimeStructFieldConfig':
|
|
55
|
+
return {
|
|
56
|
+
...baseConfig,
|
|
57
|
+
datetimeMin: field.datetimeMin ?? undefined,
|
|
58
|
+
datetimeMax: field.datetimeMax ?? undefined,
|
|
59
|
+
};
|
|
60
|
+
default:
|
|
61
|
+
return baseConfig;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Safely parses a date string into a Date object.
|
|
67
|
+
* Used for parsing datetime constraints in custom field validation.
|
|
68
|
+
*
|
|
69
|
+
* @param dateStr - The date string to parse
|
|
70
|
+
* @returns Parsed Date object or undefined if invalid
|
|
71
|
+
*/
|
|
72
|
+
function parseDate(dateStr: string | undefined | null): Date | undefined {
|
|
73
|
+
if (!dateStr) return undefined;
|
|
74
|
+
const date = new Date(dateStr);
|
|
75
|
+
return isNaN(date.getTime()) ? undefined : date;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a Zod validation schema for datetime fields with optional min/max constraints.
|
|
80
|
+
* Supports both string and Date inputs, which is common in form handling.
|
|
81
|
+
*
|
|
82
|
+
* @param minDate - Optional minimum date constraint
|
|
83
|
+
* @param maxDate - Optional maximum date constraint
|
|
84
|
+
* @returns Zod schema that validates date ranges
|
|
85
|
+
*/
|
|
86
|
+
function createDateValidationSchema(minDate: Date | undefined, maxDate: Date | undefined): ZodType {
|
|
87
|
+
const baseSchema = z.union([z.string(), z.date()]);
|
|
88
|
+
if (!minDate && !maxDate) return baseSchema;
|
|
89
|
+
|
|
90
|
+
const dateMinString = minDate?.toLocaleDateString() ?? '';
|
|
91
|
+
const dateMaxString = maxDate?.toLocaleDateString() ?? '';
|
|
92
|
+
const dateMinMessage = minDate ? `Date must be after ${dateMinString}` : '';
|
|
93
|
+
const dateMaxMessage = maxDate ? `Date must be before ${dateMaxString}` : '';
|
|
94
|
+
|
|
95
|
+
return baseSchema.refine(
|
|
96
|
+
val => {
|
|
97
|
+
if (!val) return true;
|
|
98
|
+
const date = val instanceof Date ? val : new Date(val);
|
|
99
|
+
if (minDate && date < minDate) return false;
|
|
100
|
+
if (maxDate && date > maxDate) return false;
|
|
101
|
+
return true;
|
|
102
|
+
},
|
|
103
|
+
val => {
|
|
104
|
+
const date = val instanceof Date ? val : new Date(val);
|
|
105
|
+
if (minDate && date < minDate) return { message: dateMinMessage };
|
|
106
|
+
if (maxDate && date > maxDate) return { message: dateMaxMessage };
|
|
107
|
+
return { message: '' };
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a Zod validation schema for string fields with optional regex pattern validation.
|
|
114
|
+
* Used for string-type custom fields that may have pattern constraints.
|
|
115
|
+
*
|
|
116
|
+
* @param pattern - Optional regex pattern string for validation
|
|
117
|
+
* @returns Zod string schema with optional pattern validation
|
|
118
|
+
*/
|
|
119
|
+
function createStringValidationSchema(pattern?: string): ZodType {
|
|
120
|
+
let schema = z.string();
|
|
121
|
+
if (pattern) {
|
|
122
|
+
schema = schema.regex(new RegExp(pattern), {
|
|
123
|
+
message: `Value must match pattern: ${pattern}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return schema;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Creates a Zod validation schema for integer fields with optional min/max constraints.
|
|
131
|
+
* Used for int-type custom fields that may have numeric range limits.
|
|
132
|
+
*
|
|
133
|
+
* @param min - Optional minimum value constraint
|
|
134
|
+
* @param max - Optional maximum value constraint
|
|
135
|
+
* @returns Zod number schema with optional range validation
|
|
136
|
+
*/
|
|
137
|
+
function createIntValidationSchema(min?: number, max?: number): ZodType {
|
|
138
|
+
let schema = z.number();
|
|
139
|
+
if (min !== undefined) {
|
|
140
|
+
schema = schema.min(min, {
|
|
141
|
+
message: `Value must be at least ${min}`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (max !== undefined) {
|
|
145
|
+
schema = schema.max(max, {
|
|
146
|
+
message: `Value must be at most ${max}`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return schema;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Creates a Zod validation schema for float fields with optional min/max constraints.
|
|
154
|
+
* Used for float-type custom fields that may have numeric range limits.
|
|
155
|
+
*
|
|
156
|
+
* @param min - Optional minimum value constraint
|
|
157
|
+
* @param max - Optional maximum value constraint
|
|
158
|
+
* @returns Zod number schema with optional range validation
|
|
159
|
+
*/
|
|
160
|
+
function createFloatValidationSchema(min?: number, max?: number): ZodType {
|
|
161
|
+
let schema = z.number();
|
|
162
|
+
if (min !== undefined) {
|
|
163
|
+
schema = schema.min(min, {
|
|
164
|
+
message: `Value must be at least ${min}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (max !== undefined) {
|
|
168
|
+
schema = schema.max(max, {
|
|
169
|
+
message: `Value must be at most ${max}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return schema;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Creates a Zod validation schema for a single custom field based on its type and constraints.
|
|
177
|
+
* This is the main dispatcher that routes different custom field types to their specific
|
|
178
|
+
* validation schema creators. Handles all standard custom field types in Vendure.
|
|
179
|
+
*
|
|
180
|
+
* @param customField - The custom field configuration object
|
|
181
|
+
* @returns Zod schema appropriate for the custom field type
|
|
182
|
+
*/
|
|
183
|
+
function createCustomFieldValidationSchema(customField: CustomFieldConfig): ZodType {
|
|
184
|
+
let zodType: ZodType;
|
|
185
|
+
|
|
186
|
+
switch (customField.type) {
|
|
187
|
+
case 'localeString':
|
|
188
|
+
case 'localeText':
|
|
189
|
+
case 'string':
|
|
190
|
+
zodType = createStringValidationSchema(customField.pattern);
|
|
191
|
+
break;
|
|
192
|
+
case 'int':
|
|
193
|
+
zodType = createIntValidationSchema(customField.intMin, customField.intMax);
|
|
194
|
+
break;
|
|
195
|
+
case 'float':
|
|
196
|
+
zodType = createFloatValidationSchema(customField.floatMin, customField.floatMax);
|
|
197
|
+
break;
|
|
198
|
+
case 'datetime': {
|
|
199
|
+
const minDate = parseDate(customField.datetimeMin);
|
|
200
|
+
const maxDate = parseDate(customField.datetimeMax);
|
|
201
|
+
zodType = createDateValidationSchema(minDate, maxDate);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'boolean':
|
|
205
|
+
zodType = z.boolean();
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
zodType = z.any();
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return zodType;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Creates a Zod validation schema for struct-type custom fields.
|
|
217
|
+
* Struct fields contain nested sub-fields, each with their own validation rules.
|
|
218
|
+
* This recursively processes each sub-field to create a nested object schema.
|
|
219
|
+
*
|
|
220
|
+
* @param structFieldConfig - The struct custom field configuration with nested fields
|
|
221
|
+
* @returns Zod object schema representing the struct with all sub-field validations
|
|
222
|
+
*/
|
|
223
|
+
function createStructFieldSchema(structFieldConfig: StructCustomFieldConfig): ZodType {
|
|
224
|
+
if (!structFieldConfig.fields || !Array.isArray(structFieldConfig.fields)) {
|
|
225
|
+
return z.object({}).passthrough();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const nestedSchema: ZodRawShape = {};
|
|
229
|
+
for (const structSubField of structFieldConfig.fields) {
|
|
230
|
+
const config = mapGraphQLCustomFieldToConfig(structSubField as StructFieldConfig);
|
|
231
|
+
let subFieldType = createCustomFieldValidationSchema(config);
|
|
232
|
+
|
|
233
|
+
// Handle list and nullable for struct sub-fields
|
|
234
|
+
if (config.list) {
|
|
235
|
+
subFieldType = z.array(subFieldType);
|
|
236
|
+
}
|
|
237
|
+
if (config.nullable) {
|
|
238
|
+
subFieldType = subFieldType.optional().nullable();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
nestedSchema[config.name] = subFieldType;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return z.object(nestedSchema);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Applies common list and nullable modifiers to a Zod schema based on custom field configuration.
|
|
249
|
+
* Many custom fields can be configured as lists (arrays) and/or nullable, so this helper
|
|
250
|
+
* centralizes that logic to avoid duplication.
|
|
251
|
+
*
|
|
252
|
+
* @param zodType - The base Zod schema to modify
|
|
253
|
+
* @param customField - Custom field config containing list/nullable flags
|
|
254
|
+
* @returns Modified Zod schema with list/nullable modifiers applied
|
|
255
|
+
*/
|
|
256
|
+
function applyListAndNullableModifiers(zodType: ZodType, customField: CustomFieldConfig): ZodType {
|
|
257
|
+
let modifiedType = zodType;
|
|
258
|
+
|
|
259
|
+
if (customField.list) {
|
|
260
|
+
modifiedType = z.array(modifiedType);
|
|
261
|
+
}
|
|
262
|
+
if (customField.nullable !== false) {
|
|
263
|
+
modifiedType = modifiedType.optional().nullable();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return modifiedType;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Processes all custom fields and creates a complete validation schema for the customFields object.
|
|
271
|
+
* Handles context-aware filtering (translation vs root context) and orchestrates the creation
|
|
272
|
+
* of validation schemas for all custom field types including complex struct fields.
|
|
273
|
+
*
|
|
274
|
+
* @param customFieldConfigs - Array of all custom field configurations
|
|
275
|
+
* @param isTranslationContext - Whether we're processing fields for translation forms
|
|
276
|
+
* @returns Zod schema shape for the entire customFields object
|
|
277
|
+
*/
|
|
278
|
+
function processCustomFieldsSchema(
|
|
279
|
+
customFieldConfigs: CustomFieldConfig[],
|
|
280
|
+
isTranslationContext: boolean,
|
|
281
|
+
): ZodRawShape {
|
|
282
|
+
const customFieldsSchema: ZodRawShape = {};
|
|
283
|
+
const translatableTypes = ['localeString', 'localeText'];
|
|
284
|
+
|
|
285
|
+
const filteredCustomFields = customFieldConfigs.filter(cf => {
|
|
286
|
+
if (isTranslationContext) {
|
|
287
|
+
return translatableTypes.includes(cf.type);
|
|
288
|
+
} else {
|
|
289
|
+
return !translatableTypes.includes(cf.type);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
for (const customField of filteredCustomFields) {
|
|
294
|
+
let zodType: ZodType;
|
|
295
|
+
|
|
296
|
+
if (customField.type === 'struct') {
|
|
297
|
+
zodType = createStructFieldSchema(customField as StructCustomFieldConfig);
|
|
298
|
+
} else {
|
|
299
|
+
zodType = createCustomFieldValidationSchema(customField);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
zodType = applyListAndNullableModifiers(zodType, customField);
|
|
303
|
+
const schemaPropertyName = getGraphQlInputName(customField);
|
|
304
|
+
customFieldsSchema[schemaPropertyName] = zodType;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return customFieldsSchema;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function createFormSchemaFromFields(
|
|
311
|
+
fields: FieldInfo[],
|
|
312
|
+
customFieldConfigs?: CustomFieldConfig[],
|
|
313
|
+
isTranslationContext = false,
|
|
314
|
+
) {
|
|
9
315
|
const schemaConfig: ZodRawShape = {};
|
|
316
|
+
|
|
10
317
|
for (const field of fields) {
|
|
11
318
|
const isScalar = isScalarType(field.type);
|
|
12
319
|
const isEnum = isEnumType(field.type);
|
|
13
|
-
|
|
14
|
-
|
|
320
|
+
|
|
321
|
+
if ((isScalar || isEnum) && field.name !== 'customFields') {
|
|
322
|
+
schemaConfig[field.name] = getZodTypeFromField(field, customFieldConfigs);
|
|
323
|
+
} else if (field.name === 'customFields') {
|
|
324
|
+
const customFieldsSchema =
|
|
325
|
+
customFieldConfigs && customFieldConfigs.length > 0
|
|
326
|
+
? processCustomFieldsSchema(customFieldConfigs, isTranslationContext)
|
|
327
|
+
: {};
|
|
328
|
+
schemaConfig[field.name] = z.object(customFieldsSchema).optional();
|
|
15
329
|
} else if (field.typeInfo) {
|
|
16
|
-
|
|
330
|
+
const isNestedTranslationContext = field.name === 'translations' || isTranslationContext;
|
|
331
|
+
let nestedType: ZodType = createFormSchemaFromFields(
|
|
332
|
+
field.typeInfo,
|
|
333
|
+
customFieldConfigs,
|
|
334
|
+
isNestedTranslationContext,
|
|
335
|
+
);
|
|
336
|
+
|
|
17
337
|
if (field.nullable) {
|
|
18
338
|
nestedType = nestedType.optional().nullable();
|
|
19
339
|
}
|
|
20
340
|
if (field.list) {
|
|
21
341
|
nestedType = z.array(nestedType);
|
|
22
342
|
}
|
|
343
|
+
|
|
23
344
|
schemaConfig[field.name] = nestedType;
|
|
24
345
|
}
|
|
25
346
|
}
|
|
347
|
+
|
|
26
348
|
return z.object(schemaConfig);
|
|
27
349
|
}
|
|
28
350
|
|
|
@@ -69,8 +391,12 @@ export function getDefaultValueFromField(field: FieldInfo, defaultLanguageCode?:
|
|
|
69
391
|
}
|
|
70
392
|
}
|
|
71
393
|
|
|
72
|
-
export function getZodTypeFromField(field: FieldInfo): ZodTypeAny {
|
|
394
|
+
export function getZodTypeFromField(field: FieldInfo, customFieldConfigs?: CustomFieldConfig[]): ZodTypeAny {
|
|
73
395
|
let zodType: ZodType;
|
|
396
|
+
|
|
397
|
+
// This function is only used for non-custom fields, so we don't need custom field logic here
|
|
398
|
+
// Custom fields are handled separately in createFormSchemaFromFields
|
|
399
|
+
|
|
74
400
|
switch (field.type) {
|
|
75
401
|
case 'String':
|
|
76
402
|
case 'ID':
|
|
@@ -88,6 +414,7 @@ export function getZodTypeFromField(field: FieldInfo): ZodTypeAny {
|
|
|
88
414
|
default:
|
|
89
415
|
zodType = z.any();
|
|
90
416
|
}
|
|
417
|
+
|
|
91
418
|
if (field.list) {
|
|
92
419
|
zodType = z.array(zodType);
|
|
93
420
|
}
|
|
@@ -96,3 +423,11 @@ export function getZodTypeFromField(field: FieldInfo): ZodTypeAny {
|
|
|
96
423
|
}
|
|
97
424
|
return zodType;
|
|
98
425
|
}
|
|
426
|
+
|
|
427
|
+
export function getGraphQlInputName(config: { name: string; type: string; list?: boolean }): string {
|
|
428
|
+
if (config.type === 'relation') {
|
|
429
|
+
return config.list === true ? `${config.name}Ids` : `${config.name}Id`;
|
|
430
|
+
} else {
|
|
431
|
+
return config.name;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -17,6 +17,7 @@ export interface GeneratedFormOptions<
|
|
|
17
17
|
document?: T;
|
|
18
18
|
varName?: VarName;
|
|
19
19
|
entity: E | null | undefined;
|
|
20
|
+
customFieldConfig?: any[]; // Add custom field config for validation
|
|
20
21
|
setValues: (
|
|
21
22
|
entity: NonNullable<E>,
|
|
22
23
|
) => VarName extends keyof VariablesOf<T> ? VariablesOf<T>[VarName] : VariablesOf<T>;
|
|
@@ -37,14 +38,20 @@ export function useGeneratedForm<
|
|
|
37
38
|
VarName extends keyof VariablesOf<T> | undefined,
|
|
38
39
|
E extends Record<string, any> = Record<string, any>,
|
|
39
40
|
>(options: GeneratedFormOptions<T, VarName, E>) {
|
|
40
|
-
const { document, entity, setValues, onSubmit, varName } = options;
|
|
41
|
+
const { document, entity, setValues, onSubmit, varName, customFieldConfig } = options;
|
|
41
42
|
const { activeChannel } = useChannel();
|
|
42
|
-
const
|
|
43
|
+
const serverConfig = useServerConfig();
|
|
44
|
+
const availableLanguages = serverConfig?.availableLanguages || [];
|
|
43
45
|
const updateFields = document ? getOperationVariablesFields(document, varName) : [];
|
|
44
|
-
|
|
46
|
+
|
|
47
|
+
const schema = createFormSchemaFromFields(updateFields, customFieldConfig);
|
|
45
48
|
const defaultValues = getDefaultValuesFromFields(updateFields, activeChannel?.defaultLanguageCode);
|
|
46
49
|
const processedEntity = ensureTranslationsForAllLanguages(entity, availableLanguages, defaultValues);
|
|
47
50
|
|
|
51
|
+
const values = processedEntity
|
|
52
|
+
? transformRelationFields(updateFields, setValues(processedEntity))
|
|
53
|
+
: defaultValues;
|
|
54
|
+
|
|
48
55
|
const form = useForm({
|
|
49
56
|
resolver: async (values, context, options) => {
|
|
50
57
|
const result = await zodResolver(schema)(values, context, options);
|
|
@@ -55,15 +62,24 @@ export function useGeneratedForm<
|
|
|
55
62
|
},
|
|
56
63
|
mode: 'onChange',
|
|
57
64
|
defaultValues,
|
|
58
|
-
values
|
|
59
|
-
? transformRelationFields(updateFields, setValues(processedEntity))
|
|
60
|
-
: defaultValues,
|
|
65
|
+
values,
|
|
61
66
|
});
|
|
62
|
-
let submitHandler = (event: FormEvent) => {
|
|
67
|
+
let submitHandler = (event: FormEvent): any => {
|
|
63
68
|
event.preventDefault();
|
|
64
69
|
};
|
|
65
70
|
if (onSubmit) {
|
|
66
|
-
submitHandler = (event: FormEvent) => {
|
|
71
|
+
submitHandler = async (event: FormEvent) => {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
|
|
74
|
+
// Trigger validation on ALL fields, not just dirty ones
|
|
75
|
+
const isValid = await form.trigger();
|
|
76
|
+
|
|
77
|
+
if (!isValid) {
|
|
78
|
+
console.log(`Form invalid!`);
|
|
79
|
+
event.stopPropagation();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
const onSubmitWrapper = (values: any) => {
|
|
68
84
|
onSubmit(removeEmptyIdFields(values, updateFields));
|
|
69
85
|
};
|
|
@@ -10,7 +10,7 @@ import { FieldInfo } from '../document-introspection/get-document-structure.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export function transformRelationFields<E extends Record<string, any>>(fields: FieldInfo[], entity: E): E {
|
|
12
12
|
// Create a shallow copy to avoid mutating the original entity
|
|
13
|
-
const processedEntity = { ...entity };
|
|
13
|
+
const processedEntity = { ...entity, customFields: { ...(entity.customFields ?? {}) } };
|
|
14
14
|
|
|
15
15
|
// Skip processing if there are no custom fields
|
|
16
16
|
if (!entity.customFields || !processedEntity.customFields) {
|
|
@@ -44,16 +44,10 @@ export function transformRelationFields<E extends Record<string, any>>(fields: F
|
|
|
44
44
|
// For single fields, the accessor is the field name without the "Id" suffix
|
|
45
45
|
const propertyAccessorKey = customField.name.replace(/Id$/, '');
|
|
46
46
|
const relationValue = entity.customFields[propertyAccessorKey];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const relationIdValue = relationValue.id;
|
|
50
|
-
if (relationIdValue) {
|
|
51
|
-
processedEntity.customFields[relationField] = relationIdValue;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
47
|
+
processedEntity.customFields[relationField] = relationValue?.id;
|
|
48
|
+
delete processedEntity.customFields[propertyAccessorKey];
|
|
54
49
|
}
|
|
55
50
|
}
|
|
56
|
-
|
|
57
51
|
return processedEntity;
|
|
58
52
|
}
|
|
59
53
|
|
|
@@ -242,7 +242,7 @@ export function PageLayout({ children, className }: Readonly<PageLayoutProps>) {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
export function DetailFormGrid({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
245
|
-
return <div className="
|
|
245
|
+
return <div className="grid @md:grid-cols-2 gap-6 items-start mb-6">{children}</div>;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
@@ -412,11 +412,19 @@ export function PageBlock({
|
|
|
412
412
|
blockId,
|
|
413
413
|
column,
|
|
414
414
|
}: Readonly<PageBlockProps>) {
|
|
415
|
-
const contextValue = useMemo(
|
|
415
|
+
const contextValue = useMemo(
|
|
416
|
+
() => ({
|
|
417
|
+
blockId,
|
|
418
|
+
title,
|
|
419
|
+
description,
|
|
420
|
+
column,
|
|
421
|
+
}),
|
|
422
|
+
[blockId, title, description, column],
|
|
423
|
+
);
|
|
416
424
|
return (
|
|
417
425
|
<PageBlockContext.Provider value={contextValue}>
|
|
418
426
|
<LocationWrapper>
|
|
419
|
-
<Card className={cn('w-full', className)}>
|
|
427
|
+
<Card className={cn('@container w-full', className)}>
|
|
420
428
|
{title || description ? (
|
|
421
429
|
<CardHeader>
|
|
422
430
|
{title && <CardTitle>{title}</CardTitle>}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ListQueryOptionsShape,
|
|
8
8
|
ListQueryShape,
|
|
9
9
|
PaginatedListDataTable,
|
|
10
|
+
PaginatedListRefresherRegisterFn,
|
|
10
11
|
RowAction,
|
|
11
12
|
} from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
12
13
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
@@ -53,6 +54,12 @@ export interface ListPageProps<
|
|
|
53
54
|
transformData?: (data: any[]) => any[];
|
|
54
55
|
setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
|
|
55
56
|
bulkActions?: BulkAction[];
|
|
57
|
+
/**
|
|
58
|
+
* Register a function that allows you to assign a refresh function for
|
|
59
|
+
* this list. The function can be assigned to a ref and then called when
|
|
60
|
+
* the list needs to be refreshed.
|
|
61
|
+
*/
|
|
62
|
+
registerRefresher?: PaginatedListRefresherRegisterFn;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
/**
|
|
@@ -90,6 +97,7 @@ export function ListPage<
|
|
|
90
97
|
transformData,
|
|
91
98
|
setTableOptions,
|
|
92
99
|
bulkActions,
|
|
100
|
+
registerRefresher,
|
|
93
101
|
}: Readonly<ListPageProps<T, U, V, AC>>) {
|
|
94
102
|
const route = typeof routeOrFn === 'function' ? routeOrFn() : routeOrFn;
|
|
95
103
|
const routeSearch = route.useSearch();
|
|
@@ -191,6 +199,7 @@ export function ListPage<
|
|
|
191
199
|
bulkActions={bulkActions}
|
|
192
200
|
setTableOptions={setTableOptions}
|
|
193
201
|
transformData={transformData}
|
|
202
|
+
registerRefresher={registerRefresher}
|
|
194
203
|
/>
|
|
195
204
|
</FullWidthPageBlock>
|
|
196
205
|
</PageLayout>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { removeReadonlyAndLocalizedCustomFields } from '@/vdb/lib/utils.js';
|
|
2
2
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
3
3
|
import {
|
|
4
4
|
DefinedInitialDataOptions,
|
|
@@ -307,10 +307,10 @@ export function useDetailPage<
|
|
|
307
307
|
document,
|
|
308
308
|
varName: 'input',
|
|
309
309
|
entity,
|
|
310
|
+
customFieldConfig,
|
|
310
311
|
setValues: setValuesForUpdate,
|
|
311
312
|
onSubmit(values: any) {
|
|
312
|
-
|
|
313
|
-
const filteredValues = removeReadonlyCustomFields(values, customFieldConfig || []);
|
|
313
|
+
const filteredValues = removeReadonlyAndLocalizedCustomFields(values, customFieldConfig || []);
|
|
314
314
|
|
|
315
315
|
if (isNew) {
|
|
316
316
|
const finalInput = transformCreateInput?.(filteredValues) ?? filteredValues;
|
package/src/lib/lib/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
2
2
|
import { twMerge } from 'tailwind-merge';
|
|
3
3
|
|
|
4
4
|
export function cn(...inputs: ClassValue[]) {
|
|
@@ -61,49 +61,51 @@ export function normalizeString(input: string, spaceReplacer = ' '): string {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Removes any readonly custom fields from form values before submission.
|
|
64
|
+
* Also removes localeString and localeText fields from the root customFields object
|
|
65
|
+
* since they should only exist in the translations array.
|
|
64
66
|
* This prevents errors when submitting readonly custom field values to mutations.
|
|
65
67
|
*
|
|
66
68
|
* @param values - The form values that may contain custom fields
|
|
67
69
|
* @param customFieldConfigs - Array of custom field configurations for the entity
|
|
68
|
-
* @returns The values with readonly custom fields removed
|
|
70
|
+
* @returns The values with readonly custom fields removed and locale fields properly placed
|
|
69
71
|
*/
|
|
70
|
-
export function
|
|
72
|
+
export function removeReadonlyAndLocalizedCustomFields<T extends Record<string, any>>(
|
|
71
73
|
values: T,
|
|
72
|
-
customFieldConfigs: Array<{ name: string; readonly?: boolean | null }> = [],
|
|
74
|
+
customFieldConfigs: Array<{ name: string; readonly?: boolean | null; type?: string }> = [],
|
|
73
75
|
): T {
|
|
74
76
|
if (!values || !customFieldConfigs?.length) {
|
|
75
77
|
return values;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
// Create a deep copy to avoid mutating the original values
|
|
79
80
|
const result = structuredClone(values);
|
|
80
|
-
|
|
81
|
-
// Get readonly field names
|
|
82
81
|
const readonlyFieldNames = customFieldConfigs
|
|
83
82
|
.filter(config => config.readonly === true)
|
|
84
83
|
.map(config => config.name);
|
|
84
|
+
const localeFieldNames = customFieldConfigs
|
|
85
|
+
.filter(config => config.type === 'localeString' || config.type === 'localeText')
|
|
86
|
+
.map(config => config.name);
|
|
87
|
+
const fieldsToRemoveFromRoot = [...readonlyFieldNames, ...localeFieldNames];
|
|
85
88
|
|
|
86
|
-
if (readonlyFieldNames.length === 0) {
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Remove readonly fields from main customFields
|
|
91
89
|
if (result.customFields && typeof result.customFields === 'object') {
|
|
92
|
-
|
|
90
|
+
fieldsToRemoveFromRoot.forEach(fieldName => {
|
|
93
91
|
delete result.customFields[fieldName];
|
|
94
|
-
}
|
|
92
|
+
});
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
}
|
|
95
|
+
removeReadonlyFromTranslations(result, readonlyFieldNames);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function removeReadonlyFromTranslations(entity: Record<string, any>, readonlyFieldNames: string[]): void {
|
|
100
|
+
if (!Array.isArray(entity.translations)) {
|
|
101
|
+
return;
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
|
|
104
|
+
entity.translations.forEach(translation => {
|
|
105
|
+
if (translation?.customFields && typeof translation.customFields === 'object') {
|
|
106
|
+
readonlyFieldNames.forEach(fieldName => {
|
|
107
|
+
delete translation.customFields[fieldName];
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
109
111
|
}
|