@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
@@ -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
- export function createFormSchemaFromFields(fields: FieldInfo[]) {
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
- if (isScalar || isEnum) {
14
- schemaConfig[field.name] = getZodTypeFromField(field);
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
- let nestedType: ZodType = createFormSchemaFromFields(field.typeInfo);
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 availableLanguages = useServerConfig()?.availableLanguages || [];
43
+ const serverConfig = useServerConfig();
44
+ const availableLanguages = serverConfig?.availableLanguages || [];
43
45
  const updateFields = document ? getOperationVariablesFields(document, varName) : [];
44
- const schema = createFormSchemaFromFields(updateFields);
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: processedEntity
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
- if (relationValue) {
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="md:grid md:grid-cols-2 gap-4 items-start mb-4">{children}</div>;
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(() => ({ blockId, title, description, column }), [blockId, title, description, column]);
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>}
@@ -1,4 +1,4 @@
1
- import { removeReadonlyCustomFields } from '@/vdb/lib/utils.js';
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
- // Filter out readonly custom fields before submitting
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;
@@ -1,4 +1,4 @@
1
- import { clsx, type ClassValue } from 'clsx';
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 removeReadonlyCustomFields<T extends Record<string, any>>(
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
- for (const fieldName of readonlyFieldNames) {
90
+ fieldsToRemoveFromRoot.forEach(fieldName => {
93
91
  delete result.customFields[fieldName];
94
- }
92
+ });
95
93
  }
96
94
 
97
- // Remove readonly fields from translations customFields
98
- if (Array.isArray(result.translations)) {
99
- for (const translation of result.translations) {
100
- if (translation?.customFields && typeof translation.customFields === 'object') {
101
- for (const fieldName of readonlyFieldNames) {
102
- delete translation.customFields[fieldName];
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
- return result;
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
  }