@nestledjs/data-browser 1.0.14 → 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.
Files changed (34) hide show
  1. package/README.md +43 -39
  2. package/index.js +6 -1
  3. package/lib/components/ExportButton.d.ts +14 -0
  4. package/lib/components/ExportButton.js +281 -0
  5. package/lib/components/FilterField.js +1 -2
  6. package/lib/components/RelationFieldWrapper.d.ts +1 -1
  7. package/lib/components/RelationFieldWrapper.js +2 -2
  8. package/lib/components/filters/DateRangeFilter.d.ts +1 -1
  9. package/lib/components/filters/DateRangeFilter.js +2 -2
  10. package/lib/components/filters/EnumFilter.d.ts +1 -1
  11. package/lib/components/filters/NumberRangeFilter.d.ts +1 -1
  12. package/lib/components/filters/NumberRangeFilter.js +2 -2
  13. package/lib/components/filters/RelationComponents.d.ts +5 -5
  14. package/lib/components/filters/RelationComponents.js +40 -13
  15. package/lib/components/filters/RelationFilterField.d.ts +1 -1
  16. package/lib/components/filters/RelationFilterField.js +18 -10
  17. package/lib/components/index.d.ts +1 -0
  18. package/lib/components/shared/AdminBreadcrumbs.js +2 -17
  19. package/lib/components/shared/AdminErrorStates.js +6 -1
  20. package/lib/components/shared/AdminStatusDisplay.js +7 -11
  21. package/lib/context/AdminDataContext.d.ts +2 -2
  22. package/lib/hooks/useAdminList.js +1 -1
  23. package/lib/hooks/useClickOutside.js +1 -1
  24. package/lib/hooks/useRelationData.js +2 -1
  25. package/lib/layouts/AdminDataLayout.js +87 -14
  26. package/lib/pages/AdminDataCreatePage.d.ts +1 -1
  27. package/lib/pages/AdminDataCreatePage.js +68 -45
  28. package/lib/pages/AdminDataEditPage.js +71 -38
  29. package/lib/pages/AdminDataListPage.js +116 -85
  30. package/lib/utils/graphql-utils.d.ts +7 -1
  31. package/lib/utils/graphql-utils.js +343 -331
  32. package/lib/utils/secure-storage.js +26 -20
  33. package/lib/utils/string-utils.js +31 -8
  34. package/package.json +2 -2
@@ -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,14 +72,13 @@ 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 "";
77
79
  return str.charAt(0).toLowerCase() + str.slice(1);
78
80
  }
79
81
  function findForeignKeyFieldName(relatedModel, currentModelName, relationName) {
80
- var _a;
81
82
  if (!relatedModel) return null;
82
83
  const foreignKeyField = relatedModel.fields.find((f) => {
83
84
  if (f.type !== currentModelName) return false;
@@ -86,7 +87,263 @@ function findForeignKeyFieldName(relatedModel, currentModelName, relationName) {
86
87
  if (relationName && f.relationName !== relationName) return false;
87
88
  return true;
88
89
  });
89
- return ((_a = foreignKeyField == null ? void 0 : foreignKeyField.relationFromFields) == null ? void 0 : _a[0]) || null;
90
+ return foreignKeyField?.relationFromFields?.[0] || null;
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
+ });
90
347
  }
91
348
  function buildFormFields(sdk, model, operation, options = {}) {
92
349
  const {
@@ -118,274 +375,22 @@ function buildFormFields(sdk, model, operation, options = {}) {
118
375
  }
119
376
  }
120
377
  regularFields.forEach((field) => {
121
- var _a;
122
- const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
123
- let initialValue = currentItem && operation === "update" ? currentItem[field.name] : void 0;
124
- if (field.type.toLowerCase() === "datetime" || field.type.toLowerCase() === "date") {
125
- if (initialValue instanceof Date || initialValue && typeof initialValue === "string") {
126
- try {
127
- const dateValue = initialValue instanceof Date ? initialValue : new Date(initialValue);
128
- if (field.type.toLowerCase() === "date") {
129
- initialValue = dateValue.toISOString().split("T")[0];
130
- } else {
131
- const isoString = dateValue.toISOString();
132
- initialValue = isoString.substring(0, 16);
133
- }
134
- } catch (e) {
135
- initialValue = "";
136
- }
137
- }
138
- }
139
- if (initialValue && typeof initialValue === "object" && !Array.isArray(initialValue) && field.relationName) {
140
- const relationObj = initialValue;
141
- if (relationObj.id && typeof relationObj.id === "string") {
142
- initialValue = relationObj.id;
143
- } else {
144
- initialValue = "";
145
- }
146
- }
147
- if (initialValue === null && field.type.toLowerCase() !== "boolean") {
148
- initialValue = "";
149
- }
150
- const isArrayField = Boolean(field.isList);
151
- const isRequired = isArrayField ? false : !field.isOptional;
152
- const options2 = {
153
- label,
154
- required: isRequired,
155
- ...initialValue !== void 0 && { value: initialValue }
156
- };
157
- let formField;
158
- switch (field.type.toLowerCase()) {
159
- case "string":
160
- if (field.name.toLowerCase().includes("email")) {
161
- formField = FormFieldClass.email(field.name, options2);
162
- } else if (field.name.toLowerCase().includes("description") || field.name.toLowerCase().includes("content") || field.name.toLowerCase().includes("notes")) {
163
- formField = FormFieldClass.textArea(field.name, options2);
164
- } else {
165
- formField = FormFieldClass.text(field.name, options2);
166
- }
167
- break;
168
- case "int":
169
- case "bigint":
170
- formField = FormFieldClass.text(field.name, options2);
171
- break;
172
- case "float":
173
- case "decimal":
174
- formField = FormFieldClass.text(field.name, options2);
175
- break;
176
- case "boolean": {
177
- const booleanValue = currentItem && operation === "update" ? Boolean(currentItem[field.name]) : false;
178
- formField = FormFieldClass.checkbox(field.name, {
179
- ...options2,
180
- required: false,
181
- ...operation === "update" && { value: booleanValue }
182
- });
183
- break;
184
- }
185
- case "datetime":
186
- formField = FormFieldClass.dateTimePicker(field.name, options2);
187
- break;
188
- case "date":
189
- formField = FormFieldClass.datePicker(field.name, options2);
190
- break;
191
- default: {
192
- const enumValues = getEnumValues(sdk, field.type);
193
- if (enumValues) {
194
- if (field.isList) {
195
- let defaultValue = "";
196
- if (Array.isArray(initialValue) && initialValue.length > 0) {
197
- defaultValue = initialValue.join(",");
198
- }
199
- const checkboxOptions = enumValues.map((value) => ({
200
- key: value,
201
- value,
202
- label: value.replaceAll("_", " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
203
- }));
204
- formField = FormFieldClass.checkboxGroup(field.name, {
205
- label: options2.label,
206
- required: options2.required,
207
- checkboxOptions,
208
- checkboxDirection: "column",
209
- // Don't set value in field options for update operations
210
- ...operation !== "update" && defaultValue && { defaultValue }
211
- });
212
- break;
213
- }
214
- const selectOptions = enumValues.map((value) => ({
215
- value,
216
- label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
217
- }));
218
- const enumOptions = operation === "update" ? { label: options2.label, required: options2.required, options: selectOptions } : { ...options2, options: selectOptions };
219
- formField = FormFieldClass.select(field.name, enumOptions);
220
- break;
221
- }
222
- if (field.relationName && !field.isList) {
223
- const relationFieldName = ((_a = field.relationFromFields) == null ? void 0 : _a[0]) || `${field.name}Id`;
224
- let relationValue = currentItem && operation === "update" ? currentItem[relationFieldName] : void 0;
225
- if (relationValue === void 0 && currentItem && operation === "update") {
226
- const relationObject = currentItem[field.name];
227
- if (relationObject && typeof relationObject === "object" && relationObject.id) {
228
- relationValue = relationObject.id;
229
- }
230
- }
231
- if (relationValue && typeof relationValue === "object" && relationValue.id) {
232
- relationValue = relationValue.id;
233
- }
234
- if (relationValue === null) {
235
- relationValue = "";
236
- }
237
- const properPluralName = getPluralName(field.type);
238
- const adminDocumentName = `__Admin${properPluralName}`;
239
- const regularDocumentName = `${properPluralName}`;
240
- const relationDocument = sdk[adminDocumentName] || sdk[regularDocumentName];
241
- if (relationDocument) {
242
- const config = displayFieldConfig == null ? void 0 : displayFieldConfig[field.type];
243
- const displayFields = (config == null ? void 0 : config.display) || ["name", "title"];
244
- const searchFields = (config == null ? void 0 : config.search) || displayFields;
245
- const getDisplayLabel = (item) => {
246
- const allValues = displayFields.map((field2) => item[field2]).filter((val) => val != null && val !== "");
247
- if (allValues.length > 0) {
248
- return allValues.join(" ");
249
- }
250
- return item.id;
251
- };
252
- let initialOptions = [];
253
- if (currentItem && operation === "update") {
254
- const currentRelationData = currentItem[field.name];
255
- if (currentRelationData && typeof currentRelationData === "object" && currentRelationData.id) {
256
- const displayLabel = getDisplayLabel(currentRelationData);
257
- initialOptions = [
258
- {
259
- value: currentRelationData.id,
260
- label: displayLabel
261
- }
262
- ];
263
- }
264
- }
265
- formField = FormFieldClass.searchSelectApollo(relationFieldName, {
266
- label,
267
- // Remove "ID" suffix - just use the field name
268
- required: options2.required,
269
- dataType: properPluralName.charAt(0).toLowerCase() + properPluralName.slice(1),
270
- // e.g., Course → courses (camelCase)
271
- document: relationDocument,
272
- searchFields,
273
- // For searching
274
- selectOptionsFunction: (items) => {
275
- const queryOptions = items.map((item) => ({
276
- value: item.id,
277
- label: getDisplayLabel(item)
278
- }));
279
- const allOptions = [...initialOptions];
280
- queryOptions.forEach((option) => {
281
- if (!allOptions.find((existing) => existing.value === option.value)) {
282
- allOptions.push(option);
283
- }
284
- });
285
- allOptions.sort((a, b) => a.label.localeCompare(b.label));
286
- return allOptions;
287
- },
288
- ...initialOptions.length > 0 && { initialOptions },
289
- // Provide initial options if available
290
- ...relationValue !== void 0 && { value: relationValue },
291
- // Add custom wrapper to show view record link
292
- customWrapper: (fieldElement) => {
293
- return React.createElement(
294
- RelationFieldWrapper,
295
- {
296
- relationType: field.type,
297
- initialValue: relationValue,
298
- fieldName: relationFieldName,
299
- basePath
300
- },
301
- fieldElement
302
- );
303
- }
304
- });
305
- break;
306
- } else {
307
- formField = FormFieldClass.text(relationFieldName, {
308
- label: `${label} ID`,
309
- required: options2.required,
310
- helpText: "Enter the ID of the related record",
311
- ...relationValue !== void 0 && { value: relationValue }
312
- });
313
- break;
314
- }
315
- }
316
- if (field.kind === "enum" && field.enumValues) {
317
- const selectOptions = field.enumValues.map((value) => ({
318
- value,
319
- label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
320
- }));
321
- formField = FormFieldClass.select(field.name, {
322
- ...options2,
323
- options: selectOptions
324
- });
325
- break;
326
- }
327
- formField = FormFieldClass.text(field.name, options2);
328
- break;
329
- }
330
- }
331
- formFields.push(formField);
332
- });
333
- listRelationFields.forEach((field) => {
334
- var _a;
335
- const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
336
- if (operation === "update" && currentItem) {
337
- const pluralModelName = getPluralName(field.type);
338
- const relatedModelKebab = toKebabCase(pluralModelName);
339
- const displayName = field.type.replace(/([a-z])([A-Z])/g, "$1 $2");
340
- const pluralDisplayName = getPluralName(displayName);
341
- const relatedModel = databaseModels == null ? void 0 : databaseModels.find((m) => m.name === field.type);
342
- const foreignKeyFieldName = findForeignKeyFieldName(relatedModel, model.name, field.relationName) || `${toLowerCamelCase(model.name)}Id`;
343
- const filterUrl = `${basePath}/${relatedModelKebab}?${foreignKeyFieldName}=${currentItem.id}`;
344
- const countData = (_a = currentItem._count) == null ? void 0 : _a[field.name];
345
- const countText = countData !== void 0 ? ` (${countData})` : "";
346
- const formField = FormFieldClass.content(field.name, {
347
- content: React.createElement("div", { className: "py-2" }, [
348
- React.createElement(
349
- "label",
350
- {
351
- key: "label",
352
- className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
353
- },
354
- label
355
- ),
356
- React.createElement(
357
- Link,
358
- {
359
- key: "link",
360
- to: filterUrl,
361
- 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"
362
- },
363
- [
364
- React.createElement(
365
- "svg",
366
- {
367
- key: "icon",
368
- className: "h-4 w-4 mr-1",
369
- fill: "none",
370
- stroke: "currentColor",
371
- viewBox: "0 0 24 24",
372
- xmlns: "http://www.w3.org/2000/svg"
373
- },
374
- React.createElement("path", {
375
- strokeLinecap: "round",
376
- strokeLinejoin: "round",
377
- strokeWidth: 2,
378
- 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"
379
- })
380
- ),
381
- `See Related ${pluralDisplayName}${countText}`
382
- ]
383
- )
384
- ])
385
- });
386
- formFields.push(formField);
387
- }
378
+ formFields.push(
379
+ buildRegularFormField(field, sdk, currentItem, operation, basePath, displayFieldConfig)
380
+ );
388
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
+ }
389
394
  const loadingText = operation === "create" ? "Creating..." : "Updating...";
390
395
  const defaultText = operation === "create" ? "Create" : "Update";
391
396
  const buttonText = isSubmitting ? loadingText : defaultText;
@@ -416,28 +421,58 @@ const SYSTEM_FIELDS = /* @__PURE__ */ new Set([
416
421
  function shouldSkipValue(key, value) {
417
422
  return SYSTEM_FIELDS.has(key) || value === void 0;
418
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
+ }
419
455
  function convertStringValue(value, field) {
420
- var _a;
421
456
  if (value === "") {
422
457
  return null;
423
458
  }
424
- if ((field == null ? void 0 : field.type.toLowerCase()) === "datetime" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) {
459
+ if (field?.type.toLowerCase() === "datetime" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) {
425
460
  try {
426
461
  const date = new Date(value);
427
462
  return date.toISOString();
428
- } catch (e) {
463
+ } catch {
429
464
  return value;
430
465
  }
431
466
  }
432
467
  if (value === "true") return true;
433
468
  if (value === "false") return false;
434
- const fieldType = (_a = field == null ? void 0 : field.type) == null ? void 0 : _a.toLowerCase();
469
+ const fieldType = field?.type?.toLowerCase();
435
470
  const isNumericField = fieldType && ["int", "bigint", "float", "decimal"].includes(fieldType);
436
471
  if (isNumericField) {
437
472
  const numericPattern = /^-?\d+(\.\d+)?$/;
438
473
  if (numericPattern.test(value)) {
439
474
  const numericValue = Number(value);
440
- if (!isNaN(numericValue)) {
475
+ if (!Number.isNaN(numericValue)) {
441
476
  return numericValue;
442
477
  }
443
478
  }
@@ -449,67 +484,44 @@ function processNestedObject(value, model) {
449
484
  return Object.keys(cleanedNested).length > 0 ? cleanedNested : null;
450
485
  }
451
486
  function cleanFormInput(input, model) {
452
- var _a, _b, _c, _d, _e, _f, _g;
453
487
  const cleaned = {};
454
488
  const booleanFields = new Set(
455
- ((_b = (_a = model == null ? void 0 : model.fields) == null ? void 0 : _a.filter((field) => field.type.toLowerCase() === "boolean")) == null ? void 0 : _b.map((field) => field.name)) || []
489
+ model?.fields?.filter((field) => field.type.toLowerCase() === "boolean")?.map((field) => field.name) || []
456
490
  );
457
491
  const requiredArrayFields = new Set(
458
- ((_d = (_c = model == null ? void 0 : model.fields) == null ? void 0 : _c.filter((field) => field.isList && !field.isOptional && !field.relationName)) == null ? void 0 : _d.map((field) => field.name)) || []
492
+ model?.fields?.filter((field) => field.isList && !field.isOptional && !field.relationName)?.map((field) => field.name) || []
459
493
  );
460
494
  const enumArrayFields = new Set(
461
- ((_f = (_e = model == null ? void 0 : model.fields) == null ? void 0 : _e.filter((field) => field.isList && field.kind === "enum")) == null ? void 0 : _f.map((field) => field.name)) || []
495
+ model?.fields?.filter((field) => field.isList && field.kind === "enum")?.map((field) => field.name) || []
462
496
  );
463
497
  for (const [key, value] of Object.entries(input)) {
464
- if (booleanFields.has(key) && value === void 0) {
465
- cleaned[key] = false;
466
- continue;
467
- }
468
- if (enumArrayFields.has(key)) {
469
- if (typeof value === "string") {
470
- const arrayValue = value.split(",").filter((v) => v.trim() !== "");
471
- cleaned[key] = arrayValue;
472
- continue;
473
- } else if (Array.isArray(value)) {
474
- cleaned[key] = value;
475
- continue;
476
- } else if (value === void 0 || value === null || value === "") {
477
- cleaned[key] = [];
478
- continue;
479
- }
480
- }
481
- if (requiredArrayFields.has(key) && (value === void 0 || value === null || value === "")) {
482
- cleaned[key] = [];
483
- continue;
484
- }
485
- if (shouldSkipValue(key, value)) {
486
- continue;
487
- }
488
- if (typeof value === "string") {
489
- const field = (_g = model == null ? void 0 : model.fields) == null ? void 0 : _g.find((f) => f.name === key);
490
- const convertedValue = convertStringValue(value, field);
491
- cleaned[key] = convertedValue;
492
- continue;
493
- }
494
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
495
- const obj = value;
496
- if (obj.value !== void 0 && obj.label !== void 0 && typeof obj.value === "string") {
497
- cleaned[key] = obj.value;
498
- continue;
499
- }
500
- const processedNested = processNestedObject(obj, model);
501
- if (processedNested !== null) {
502
- cleaned[key] = processedNested;
503
- }
504
- 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];
505
508
  }
506
- cleaned[key] = value;
507
509
  }
508
510
  return cleaned;
509
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
+ }
510
519
  export {
511
520
  buildFormFields,
512
521
  cleanFormInput,
513
522
  getAdminDocuments,
514
- getMutationName
523
+ getMutationName,
524
+ sanitizeInput,
525
+ toKebabCase,
526
+ toReadableText
515
527
  };