@nestledjs/data-browser 1.0.15 → 1.0.16

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.
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Link } from "react-router";
3
- import { FormFieldClass } from "@nestledjs/forms";
3
+ import { FormFieldClass } from "@nestledjs/forms-core";
4
4
  import { RelationFieldWrapper } from "../components/RelationFieldWrapper.js";
5
5
  import { getPluralName } from "./get-plural-names.js";
6
6
  import { normalizeModelNameForDocument } from "./string-utils.js";
@@ -15,16 +15,17 @@ function getEnumValues(sdk, enumType) {
15
15
  }
16
16
  const values = Object.values(enumObject).filter((value) => typeof value === "string");
17
17
  if (values.length === 0) {
18
- const keys = Object.keys(enumObject).filter((key) => isNaN(Number(key)));
18
+ const keys = Object.keys(enumObject).filter((key) => Number.isNaN(Number(key)));
19
19
  return keys.length > 0 ? keys : null;
20
20
  }
21
- return values;
21
+ return values.filter((v) => typeof v === "string");
22
22
  } catch (error) {
23
+ console.error("Unexpected error:", error);
23
24
  return null;
24
25
  }
25
26
  }
26
27
  function getAdminDocuments(sdk, model) {
27
- if (!model || !model.name) {
28
+ if (!model?.name) {
28
29
  throw new Error("Invalid model provided to getAdminDocuments");
29
30
  }
30
31
  const normalizedModelName = normalizeModelNameForDocument(model.name);
@@ -34,14 +35,15 @@ function getAdminDocuments(sdk, model) {
34
35
  const updateDocumentName = `__AdminUpdate${normalizedModelName}`;
35
36
  const deleteDocumentName = `__AdminDelete${normalizedModelName}`;
36
37
  const createDocumentName = `__AdminCreate${normalizedModelName}`;
38
+ const sdkRecord = sdk;
37
39
  const documents = {
38
- query: sdk[singleQueryDocumentName],
40
+ query: sdkRecord[singleQueryDocumentName],
39
41
  // For single item
40
- listQuery: sdk[listQueryDocumentName],
42
+ listQuery: sdkRecord[listQueryDocumentName],
41
43
  // For lists
42
- update: sdk[updateDocumentName],
43
- delete: sdk[deleteDocumentName],
44
- create: sdk[createDocumentName]
44
+ update: sdkRecord[updateDocumentName],
45
+ delete: sdkRecord[deleteDocumentName],
46
+ create: sdkRecord[createDocumentName]
45
47
  };
46
48
  const missingDocuments = [];
47
49
  if (!documents.query) missingDocuments.push(singleQueryDocumentName);
@@ -70,7 +72,7 @@ function getMutationName(model, operation) {
70
72
  }
71
73
  }
72
74
  function toKebabCase(str) {
73
- return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
75
+ return str.replaceAll(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
74
76
  }
75
77
  function toLowerCamelCase(str) {
76
78
  if (!str) return "";
@@ -87,6 +89,262 @@ function findForeignKeyFieldName(relatedModel, currentModelName, relationName) {
87
89
  });
88
90
  return foreignKeyField?.relationFromFields?.[0] || null;
89
91
  }
92
+ function normalizeDateInitialValue(value, fieldType) {
93
+ if (!(value instanceof Date) && !(value && typeof value === "string")) return value;
94
+ try {
95
+ const dateValue = value instanceof Date ? value : new Date(String(value));
96
+ if (fieldType === "date") return dateValue.toISOString().split("T")[0];
97
+ return dateValue.toISOString().substring(0, 16);
98
+ } catch (e) {
99
+ console.error("Unexpected error:", e);
100
+ return "";
101
+ }
102
+ }
103
+ function getFieldInitialValue(field, currentItem, operation) {
104
+ let initialValue = currentItem && operation === "update" ? currentItem[field.name] : void 0;
105
+ const fieldTypeLower = field.type.toLowerCase();
106
+ if (fieldTypeLower === "datetime" || fieldTypeLower === "date") {
107
+ initialValue = normalizeDateInitialValue(initialValue, fieldTypeLower);
108
+ }
109
+ if (initialValue && typeof initialValue === "object" && !Array.isArray(initialValue) && field.relationName) {
110
+ const rel = initialValue;
111
+ initialValue = rel.id && typeof rel.id === "string" ? rel.id : "";
112
+ }
113
+ if (initialValue === null && fieldTypeLower !== "boolean") {
114
+ initialValue = "";
115
+ }
116
+ return initialValue;
117
+ }
118
+ function buildEnumSelectOptions(values) {
119
+ return values.map((value) => ({
120
+ value,
121
+ label: value.replaceAll("_", " ").toLowerCase().replace(/^./, (s) => s.toUpperCase())
122
+ }));
123
+ }
124
+ function mergeAndSortOptions(initialOptions, queryOptions) {
125
+ const merged = [...initialOptions];
126
+ for (const option of queryOptions) {
127
+ if (!merged.some((existing) => existing.value === option.value)) {
128
+ merged.push(option);
129
+ }
130
+ }
131
+ merged.sort((a, b) => a.label.localeCompare(b.label));
132
+ return merged;
133
+ }
134
+ function resolveRelationValue(field, currentItem, operation) {
135
+ if (!currentItem || operation !== "update") return void 0;
136
+ const relationFieldName = field.relationFromFields?.[0] || `${field.name}Id`;
137
+ let value = currentItem[relationFieldName];
138
+ if (value === void 0) {
139
+ const relObj = currentItem[field.name];
140
+ if (relObj && typeof relObj === "object" && relObj.id) value = relObj.id;
141
+ }
142
+ if (value && typeof value === "object" && value.id) {
143
+ value = value.id;
144
+ }
145
+ if (value === null) return "";
146
+ return value;
147
+ }
148
+ function buildInitialOptions(field, currentItem, operation, getDisplayLabel) {
149
+ if (!currentItem || operation !== "update") return [];
150
+ const currentRelationData = currentItem[field.name];
151
+ if (currentRelationData && typeof currentRelationData === "object" && currentRelationData.id) {
152
+ return [{ value: currentRelationData.id, label: getDisplayLabel(currentRelationData) }];
153
+ }
154
+ return [];
155
+ }
156
+ function buildRelationFormField(field, label, options, currentItem, operation, sdk, ctx) {
157
+ const { basePath, displayFieldConfig } = ctx;
158
+ const relationFieldName = field.relationFromFields?.[0] || `${field.name}Id`;
159
+ const relationValue = resolveRelationValue(field, currentItem, operation);
160
+ const properPluralName = getPluralName(field.type);
161
+ const adminDocumentName = `__Admin${properPluralName}`;
162
+ const regularDocumentName = `${properPluralName}`;
163
+ const relationDocument = sdk[adminDocumentName] || sdk[regularDocumentName];
164
+ if (!relationDocument) {
165
+ return FormFieldClass.text(relationFieldName, {
166
+ label: `${label} ID`,
167
+ required: options.required,
168
+ helpText: "Enter the ID of the related record",
169
+ ...relationValue !== void 0 && { value: relationValue }
170
+ });
171
+ }
172
+ const config = displayFieldConfig?.[field.type];
173
+ const displayFields = config?.display || ["name", "title"];
174
+ const searchFields = config?.search || displayFields;
175
+ const getDisplayLabel = (item) => {
176
+ const allValues = displayFields.map((f) => item[f]).filter((v) => v != null && v !== "");
177
+ return allValues.length > 0 ? allValues.join(" ") : item.id;
178
+ };
179
+ const initialOptions = buildInitialOptions(field, currentItem, operation, getDisplayLabel);
180
+ return FormFieldClass.searchSelectApollo(relationFieldName, {
181
+ label,
182
+ required: options.required,
183
+ dataType: properPluralName.charAt(0).toLowerCase() + properPluralName.slice(1),
184
+ document: relationDocument,
185
+ searchFields,
186
+ selectOptionsFunction: (items) => {
187
+ const queryOptions = items.map((item) => ({
188
+ value: item.id,
189
+ label: getDisplayLabel(item)
190
+ }));
191
+ return mergeAndSortOptions(initialOptions, queryOptions);
192
+ },
193
+ ...initialOptions.length > 0 && { initialOptions },
194
+ ...relationValue !== void 0 && { value: relationValue },
195
+ customWrapper: (fieldElement) => React.createElement(
196
+ RelationFieldWrapper,
197
+ {
198
+ relationType: field.type,
199
+ initialValue: relationValue,
200
+ fieldName: relationFieldName,
201
+ basePath
202
+ },
203
+ fieldElement
204
+ )
205
+ });
206
+ }
207
+ function fieldNameWords(name) {
208
+ return name.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s_-]+/).filter(Boolean);
209
+ }
210
+ function buildRegularFormField(field, sdk, currentItem, operation, basePath, displayFieldConfig) {
211
+ const label = field.name.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
212
+ const initialValue = getFieldInitialValue(field, currentItem, operation);
213
+ const isArrayField = Boolean(field.isList);
214
+ const isRequired = isArrayField ? false : !field.isOptional;
215
+ const options = {
216
+ label,
217
+ required: isRequired,
218
+ ...initialValue !== void 0 && { value: initialValue }
219
+ };
220
+ switch (field.type.toLowerCase()) {
221
+ case "string": {
222
+ const words = fieldNameWords(field.name);
223
+ const last = words[words.length - 1];
224
+ const secondLast = words[words.length - 2];
225
+ const isEmailField = last === "email" || last === "emailaddress" || secondLast === "email" && last === "address";
226
+ if (isEmailField) return FormFieldClass.email(field.name, options);
227
+ if (last === "description" || last === "content" || last === "notes")
228
+ return FormFieldClass.textArea(field.name, options);
229
+ return FormFieldClass.text(field.name, options);
230
+ }
231
+ case "int":
232
+ case "bigint":
233
+ case "float":
234
+ case "decimal":
235
+ return FormFieldClass.text(field.name, options);
236
+ case "boolean": {
237
+ const booleanValue = currentItem && operation === "update" ? Boolean(currentItem[field.name]) : false;
238
+ return FormFieldClass.checkbox(field.name, {
239
+ ...options,
240
+ required: false,
241
+ ...operation === "update" && { value: booleanValue }
242
+ });
243
+ }
244
+ case "datetime":
245
+ return FormFieldClass.dateTimePicker(field.name, options);
246
+ case "date":
247
+ return FormFieldClass.datePicker(field.name, options);
248
+ default:
249
+ return buildDefaultFormField(field, options, initialValue, sdk, currentItem, operation, {
250
+ basePath,
251
+ displayFieldConfig
252
+ });
253
+ }
254
+ }
255
+ function buildDefaultFormField(field, options, initialValue, sdk, currentItem, operation, ctx) {
256
+ const label = options.label;
257
+ const { basePath, displayFieldConfig } = ctx;
258
+ const enumValues = getEnumValues(sdk, field.type);
259
+ if (enumValues) {
260
+ if (field.isList) {
261
+ let defaultValue = "";
262
+ if (Array.isArray(initialValue) && initialValue.length > 0)
263
+ defaultValue = initialValue.join(",");
264
+ const checkboxOptions = enumValues.map((value) => ({
265
+ key: value,
266
+ value,
267
+ label: value.replaceAll("_", " ").toLowerCase().replace(/^./, (s) => s.toUpperCase())
268
+ }));
269
+ return FormFieldClass.checkboxGroup(field.name, {
270
+ label: options.label,
271
+ required: options.required,
272
+ checkboxOptions,
273
+ checkboxDirection: "column",
274
+ ...operation !== "update" && defaultValue && { defaultValue }
275
+ });
276
+ }
277
+ const selectOptions = buildEnumSelectOptions(enumValues);
278
+ const enumOptions = operation === "update" ? { label: options.label, required: options.required, options: selectOptions } : { ...options, options: selectOptions };
279
+ return FormFieldClass.select(field.name, enumOptions);
280
+ }
281
+ if (field.relationName && !field.isList) {
282
+ return buildRelationFormField(field, label, options, currentItem, operation, sdk, {
283
+ basePath,
284
+ displayFieldConfig
285
+ });
286
+ }
287
+ if (field.kind === "enum" && field.enumValues) {
288
+ const selectOptions = field.enumValues.map((value) => ({
289
+ value,
290
+ label: value.replaceAll("_", " ").toLowerCase().replace(/^./, (s) => s.toUpperCase())
291
+ }));
292
+ return FormFieldClass.select(field.name, { ...options, options: selectOptions });
293
+ }
294
+ return FormFieldClass.text(field.name, options);
295
+ }
296
+ function buildListRelationFormField(field, model, currentItem, databaseModels, basePath) {
297
+ const label = field.name.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
298
+ const pluralModelName = getPluralName(field.type);
299
+ const relatedModelKebab = toKebabCase(pluralModelName);
300
+ const displayName = field.type.replaceAll(/([a-z])([A-Z])/g, "$1 $2");
301
+ const pluralDisplayName = getPluralName(displayName);
302
+ const relatedModel = databaseModels?.find((m) => m.name === field.type);
303
+ const foreignKeyFieldName = findForeignKeyFieldName(relatedModel, model.name, field.relationName) || `${toLowerCamelCase(model.name)}Id`;
304
+ const filterUrl = `${basePath}/${relatedModelKebab}?${foreignKeyFieldName}=${currentItem.id}`;
305
+ const countData = currentItem._count?.[field.name];
306
+ const countText = countData === void 0 ? "" : ` (${countData})`;
307
+ return FormFieldClass.content(field.name, {
308
+ content: React.createElement("div", { className: "py-2" }, [
309
+ React.createElement(
310
+ "label",
311
+ {
312
+ key: "label",
313
+ className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
314
+ },
315
+ label
316
+ ),
317
+ React.createElement(
318
+ Link,
319
+ {
320
+ key: "link",
321
+ to: filterUrl,
322
+ className: "inline-flex items-center text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
323
+ },
324
+ [
325
+ React.createElement(
326
+ "svg",
327
+ {
328
+ key: "icon",
329
+ className: "h-4 w-4 mr-1",
330
+ fill: "none",
331
+ stroke: "currentColor",
332
+ viewBox: "0 0 24 24",
333
+ xmlns: "http://www.w3.org/2000/svg"
334
+ },
335
+ React.createElement("path", {
336
+ strokeLinecap: "round",
337
+ strokeLinejoin: "round",
338
+ strokeWidth: 2,
339
+ d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
340
+ })
341
+ ),
342
+ `See Related ${pluralDisplayName}${countText}`
343
+ ]
344
+ )
345
+ ])
346
+ });
347
+ }
90
348
  function buildFormFields(sdk, model, operation, options = {}) {
91
349
  const {
92
350
  currentItem,
@@ -117,272 +375,22 @@ function buildFormFields(sdk, model, operation, options = {}) {
117
375
  }
118
376
  }
119
377
  regularFields.forEach((field) => {
120
- const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
121
- let initialValue = currentItem && operation === "update" ? currentItem[field.name] : void 0;
122
- if (field.type.toLowerCase() === "datetime" || field.type.toLowerCase() === "date") {
123
- if (initialValue instanceof Date || initialValue && typeof initialValue === "string") {
124
- try {
125
- const dateValue = initialValue instanceof Date ? initialValue : new Date(initialValue);
126
- if (field.type.toLowerCase() === "date") {
127
- initialValue = dateValue.toISOString().split("T")[0];
128
- } else {
129
- const isoString = dateValue.toISOString();
130
- initialValue = isoString.substring(0, 16);
131
- }
132
- } catch (e) {
133
- initialValue = "";
134
- }
135
- }
136
- }
137
- if (initialValue && typeof initialValue === "object" && !Array.isArray(initialValue) && field.relationName) {
138
- const relationObj = initialValue;
139
- if (relationObj.id && typeof relationObj.id === "string") {
140
- initialValue = relationObj.id;
141
- } else {
142
- initialValue = "";
143
- }
144
- }
145
- if (initialValue === null && field.type.toLowerCase() !== "boolean") {
146
- initialValue = "";
147
- }
148
- const isArrayField = Boolean(field.isList);
149
- const isRequired = isArrayField ? false : !field.isOptional;
150
- const options2 = {
151
- label,
152
- required: isRequired,
153
- ...initialValue !== void 0 && { value: initialValue }
154
- };
155
- let formField;
156
- switch (field.type.toLowerCase()) {
157
- case "string":
158
- if (field.name.toLowerCase().includes("email")) {
159
- formField = FormFieldClass.email(field.name, options2);
160
- } else if (field.name.toLowerCase().includes("description") || field.name.toLowerCase().includes("content") || field.name.toLowerCase().includes("notes")) {
161
- formField = FormFieldClass.textArea(field.name, options2);
162
- } else {
163
- formField = FormFieldClass.text(field.name, options2);
164
- }
165
- break;
166
- case "int":
167
- case "bigint":
168
- formField = FormFieldClass.text(field.name, options2);
169
- break;
170
- case "float":
171
- case "decimal":
172
- formField = FormFieldClass.text(field.name, options2);
173
- break;
174
- case "boolean": {
175
- const booleanValue = currentItem && operation === "update" ? Boolean(currentItem[field.name]) : false;
176
- formField = FormFieldClass.checkbox(field.name, {
177
- ...options2,
178
- required: false,
179
- ...operation === "update" && { value: booleanValue }
180
- });
181
- break;
182
- }
183
- case "datetime":
184
- formField = FormFieldClass.dateTimePicker(field.name, options2);
185
- break;
186
- case "date":
187
- formField = FormFieldClass.datePicker(field.name, options2);
188
- break;
189
- default: {
190
- const enumValues = getEnumValues(sdk, field.type);
191
- if (enumValues) {
192
- if (field.isList) {
193
- let defaultValue = "";
194
- if (Array.isArray(initialValue) && initialValue.length > 0) {
195
- defaultValue = initialValue.join(",");
196
- }
197
- const checkboxOptions = enumValues.map((value) => ({
198
- key: value,
199
- value,
200
- label: value.replaceAll("_", " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
201
- }));
202
- formField = FormFieldClass.checkboxGroup(field.name, {
203
- label: options2.label,
204
- required: options2.required,
205
- checkboxOptions,
206
- checkboxDirection: "column",
207
- // Don't set value in field options for update operations
208
- ...operation !== "update" && defaultValue && { defaultValue }
209
- });
210
- break;
211
- }
212
- const selectOptions = enumValues.map((value) => ({
213
- value,
214
- label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
215
- }));
216
- const enumOptions = operation === "update" ? { label: options2.label, required: options2.required, options: selectOptions } : { ...options2, options: selectOptions };
217
- formField = FormFieldClass.select(field.name, enumOptions);
218
- break;
219
- }
220
- if (field.relationName && !field.isList) {
221
- const relationFieldName = field.relationFromFields?.[0] || `${field.name}Id`;
222
- let relationValue = currentItem && operation === "update" ? currentItem[relationFieldName] : void 0;
223
- if (relationValue === void 0 && currentItem && operation === "update") {
224
- const relationObject = currentItem[field.name];
225
- if (relationObject && typeof relationObject === "object" && relationObject.id) {
226
- relationValue = relationObject.id;
227
- }
228
- }
229
- if (relationValue && typeof relationValue === "object" && relationValue.id) {
230
- relationValue = relationValue.id;
231
- }
232
- if (relationValue === null) {
233
- relationValue = "";
234
- }
235
- const properPluralName = getPluralName(field.type);
236
- const adminDocumentName = `__Admin${properPluralName}`;
237
- const regularDocumentName = `${properPluralName}`;
238
- const relationDocument = sdk[adminDocumentName] || sdk[regularDocumentName];
239
- if (relationDocument) {
240
- const config = displayFieldConfig?.[field.type];
241
- const displayFields = config?.display || ["name", "title"];
242
- const searchFields = config?.search || displayFields;
243
- const getDisplayLabel = (item) => {
244
- const allValues = displayFields.map((field2) => item[field2]).filter((val) => val != null && val !== "");
245
- if (allValues.length > 0) {
246
- return allValues.join(" ");
247
- }
248
- return item.id;
249
- };
250
- let initialOptions = [];
251
- if (currentItem && operation === "update") {
252
- const currentRelationData = currentItem[field.name];
253
- if (currentRelationData && typeof currentRelationData === "object" && currentRelationData.id) {
254
- const displayLabel = getDisplayLabel(currentRelationData);
255
- initialOptions = [
256
- {
257
- value: currentRelationData.id,
258
- label: displayLabel
259
- }
260
- ];
261
- }
262
- }
263
- formField = FormFieldClass.searchSelectApollo(relationFieldName, {
264
- label,
265
- // Remove "ID" suffix - just use the field name
266
- required: options2.required,
267
- dataType: properPluralName.charAt(0).toLowerCase() + properPluralName.slice(1),
268
- // e.g., Course → courses (camelCase)
269
- document: relationDocument,
270
- searchFields,
271
- // For searching
272
- selectOptionsFunction: (items) => {
273
- const queryOptions = items.map((item) => ({
274
- value: item.id,
275
- label: getDisplayLabel(item)
276
- }));
277
- const allOptions = [...initialOptions];
278
- queryOptions.forEach((option) => {
279
- if (!allOptions.find((existing) => existing.value === option.value)) {
280
- allOptions.push(option);
281
- }
282
- });
283
- allOptions.sort((a, b) => a.label.localeCompare(b.label));
284
- return allOptions;
285
- },
286
- ...initialOptions.length > 0 && { initialOptions },
287
- // Provide initial options if available
288
- ...relationValue !== void 0 && { value: relationValue },
289
- // Add custom wrapper to show view record link
290
- customWrapper: (fieldElement) => {
291
- return React.createElement(
292
- RelationFieldWrapper,
293
- {
294
- relationType: field.type,
295
- initialValue: relationValue,
296
- fieldName: relationFieldName,
297
- basePath
298
- },
299
- fieldElement
300
- );
301
- }
302
- });
303
- break;
304
- } else {
305
- formField = FormFieldClass.text(relationFieldName, {
306
- label: `${label} ID`,
307
- required: options2.required,
308
- helpText: "Enter the ID of the related record",
309
- ...relationValue !== void 0 && { value: relationValue }
310
- });
311
- break;
312
- }
313
- }
314
- if (field.kind === "enum" && field.enumValues) {
315
- const selectOptions = field.enumValues.map((value) => ({
316
- value,
317
- label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
318
- }));
319
- formField = FormFieldClass.select(field.name, {
320
- ...options2,
321
- options: selectOptions
322
- });
323
- break;
324
- }
325
- formField = FormFieldClass.text(field.name, options2);
326
- break;
327
- }
328
- }
329
- formFields.push(formField);
330
- });
331
- listRelationFields.forEach((field) => {
332
- const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
333
- if (operation === "update" && currentItem) {
334
- const pluralModelName = getPluralName(field.type);
335
- const relatedModelKebab = toKebabCase(pluralModelName);
336
- const displayName = field.type.replace(/([a-z])([A-Z])/g, "$1 $2");
337
- const pluralDisplayName = getPluralName(displayName);
338
- const relatedModel = databaseModels?.find((m) => m.name === field.type);
339
- const foreignKeyFieldName = findForeignKeyFieldName(relatedModel, model.name, field.relationName) || `${toLowerCamelCase(model.name)}Id`;
340
- const filterUrl = `${basePath}/${relatedModelKebab}?${foreignKeyFieldName}=${currentItem.id}`;
341
- const countData = currentItem._count?.[field.name];
342
- const countText = countData !== void 0 ? ` (${countData})` : "";
343
- const formField = FormFieldClass.content(field.name, {
344
- content: React.createElement("div", { className: "py-2" }, [
345
- React.createElement(
346
- "label",
347
- {
348
- key: "label",
349
- className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
350
- },
351
- label
352
- ),
353
- React.createElement(
354
- Link,
355
- {
356
- key: "link",
357
- to: filterUrl,
358
- className: "inline-flex items-center text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
359
- },
360
- [
361
- React.createElement(
362
- "svg",
363
- {
364
- key: "icon",
365
- className: "h-4 w-4 mr-1",
366
- fill: "none",
367
- stroke: "currentColor",
368
- viewBox: "0 0 24 24",
369
- xmlns: "http://www.w3.org/2000/svg"
370
- },
371
- React.createElement("path", {
372
- strokeLinecap: "round",
373
- strokeLinejoin: "round",
374
- strokeWidth: 2,
375
- d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
376
- })
377
- ),
378
- `See Related ${pluralDisplayName}${countText}`
379
- ]
380
- )
381
- ])
382
- });
383
- formFields.push(formField);
384
- }
378
+ formFields.push(
379
+ buildRegularFormField(field, sdk, currentItem, operation, basePath, displayFieldConfig)
380
+ );
385
381
  });
382
+ if (operation === "update" && currentItem) {
383
+ listRelationFields.forEach((field) => {
384
+ const formField = buildListRelationFormField(
385
+ field,
386
+ model,
387
+ currentItem,
388
+ databaseModels,
389
+ basePath
390
+ );
391
+ if (formField) formFields.push(formField);
392
+ });
393
+ }
386
394
  const loadingText = operation === "create" ? "Creating..." : "Updating...";
387
395
  const defaultText = operation === "create" ? "Create" : "Update";
388
396
  const buttonText = isSubmitting ? loadingText : defaultText;
@@ -413,6 +421,37 @@ const SYSTEM_FIELDS = /* @__PURE__ */ new Set([
413
421
  function shouldSkipValue(key, value) {
414
422
  return SYSTEM_FIELDS.has(key) || value === void 0;
415
423
  }
424
+ function cleanEnumArrayValue(value) {
425
+ if (typeof value === "string") {
426
+ return value.split(",").filter((v) => v.trim() !== "");
427
+ }
428
+ if (Array.isArray(value)) return value;
429
+ return [];
430
+ }
431
+ function cleanObjectValue(obj, model) {
432
+ if (obj.value !== void 0 && obj.label !== void 0 && typeof obj.value === "string") {
433
+ return obj.value;
434
+ }
435
+ return processNestedObject(obj, model) ?? void 0;
436
+ }
437
+ function cleanInputEntry(key, value, booleanFields, enumArrayFields, requiredArrayFields, model) {
438
+ if (booleanFields.has(key) && value === void 0) return [key, false];
439
+ if (enumArrayFields.has(key)) return [key, cleanEnumArrayValue(value)];
440
+ if (requiredArrayFields.has(key) && (value === void 0 || value === null || value === "")) {
441
+ return [key, []];
442
+ }
443
+ if (shouldSkipValue(key, value)) return null;
444
+ if (typeof value === "string") {
445
+ const field = model?.fields?.find((f) => f.name === key);
446
+ return [key, convertStringValue(value, field)];
447
+ }
448
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
449
+ const cleaned = cleanObjectValue(value, model);
450
+ if (cleaned === void 0) return null;
451
+ return [key, cleaned];
452
+ }
453
+ return [key, value];
454
+ }
416
455
  function convertStringValue(value, field) {
417
456
  if (value === "") {
418
457
  return null;
@@ -421,7 +460,7 @@ function convertStringValue(value, field) {
421
460
  try {
422
461
  const date = new Date(value);
423
462
  return date.toISOString();
424
- } catch (e) {
463
+ } catch {
425
464
  return value;
426
465
  }
427
466
  }
@@ -433,7 +472,7 @@ function convertStringValue(value, field) {
433
472
  const numericPattern = /^-?\d+(\.\d+)?$/;
434
473
  if (numericPattern.test(value)) {
435
474
  const numericValue = Number(value);
436
- if (!isNaN(numericValue)) {
475
+ if (!Number.isNaN(numericValue)) {
437
476
  return numericValue;
438
477
  }
439
478
  }
@@ -456,55 +495,33 @@ function cleanFormInput(input, model) {
456
495
  model?.fields?.filter((field) => field.isList && field.kind === "enum")?.map((field) => field.name) || []
457
496
  );
458
497
  for (const [key, value] of Object.entries(input)) {
459
- if (booleanFields.has(key) && value === void 0) {
460
- cleaned[key] = false;
461
- continue;
462
- }
463
- if (enumArrayFields.has(key)) {
464
- if (typeof value === "string") {
465
- const arrayValue = value.split(",").filter((v) => v.trim() !== "");
466
- cleaned[key] = arrayValue;
467
- continue;
468
- } else if (Array.isArray(value)) {
469
- cleaned[key] = value;
470
- continue;
471
- } else if (value === void 0 || value === null || value === "") {
472
- cleaned[key] = [];
473
- continue;
474
- }
475
- }
476
- if (requiredArrayFields.has(key) && (value === void 0 || value === null || value === "")) {
477
- cleaned[key] = [];
478
- continue;
479
- }
480
- if (shouldSkipValue(key, value)) {
481
- continue;
482
- }
483
- if (typeof value === "string") {
484
- const field = model?.fields?.find((f) => f.name === key);
485
- const convertedValue = convertStringValue(value, field);
486
- cleaned[key] = convertedValue;
487
- continue;
488
- }
489
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
490
- const obj = value;
491
- if (obj.value !== void 0 && obj.label !== void 0 && typeof obj.value === "string") {
492
- cleaned[key] = obj.value;
493
- continue;
494
- }
495
- const processedNested = processNestedObject(obj, model);
496
- if (processedNested !== null) {
497
- cleaned[key] = processedNested;
498
- }
499
- continue;
498
+ const entry = cleanInputEntry(
499
+ key,
500
+ value,
501
+ booleanFields,
502
+ enumArrayFields,
503
+ requiredArrayFields,
504
+ model
505
+ );
506
+ if (entry !== null) {
507
+ cleaned[entry[0]] = entry[1];
500
508
  }
501
- cleaned[key] = value;
502
509
  }
503
510
  return cleaned;
504
511
  }
512
+ function toReadableText(text) {
513
+ return text.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
514
+ }
515
+ function sanitizeInput(input) {
516
+ if (!input || typeof input !== "string") return "";
517
+ return input.replaceAll(/[<>"'%;()&+]/g, "").replaceAll(/javascript:/gi, "").replaceAll(/on\w+\s*=/gi, "").trim().substring(0, 100);
518
+ }
505
519
  export {
506
520
  buildFormFields,
507
521
  cleanFormInput,
508
522
  getAdminDocuments,
509
- getMutationName
523
+ getMutationName,
524
+ sanitizeInput,
525
+ toKebabCase,
526
+ toReadableText
510
527
  };