@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.
- package/README.md +43 -39
- package/index.js +6 -1
- package/lib/components/ExportButton.d.ts +14 -0
- package/lib/components/ExportButton.js +281 -0
- package/lib/components/FilterField.js +1 -2
- package/lib/components/RelationFieldWrapper.d.ts +1 -1
- package/lib/components/RelationFieldWrapper.js +2 -2
- package/lib/components/filters/DateRangeFilter.d.ts +1 -1
- package/lib/components/filters/DateRangeFilter.js +2 -2
- package/lib/components/filters/EnumFilter.d.ts +1 -1
- package/lib/components/filters/NumberRangeFilter.d.ts +1 -1
- package/lib/components/filters/NumberRangeFilter.js +2 -2
- package/lib/components/filters/RelationComponents.d.ts +5 -5
- package/lib/components/filters/RelationComponents.js +40 -13
- package/lib/components/filters/RelationFilterField.d.ts +1 -1
- package/lib/components/filters/RelationFilterField.js +18 -10
- package/lib/components/index.d.ts +1 -0
- package/lib/components/shared/AdminBreadcrumbs.js +2 -17
- package/lib/components/shared/AdminErrorStates.js +6 -1
- package/lib/components/shared/AdminStatusDisplay.js +7 -11
- package/lib/context/AdminDataContext.d.ts +2 -2
- package/lib/hooks/useAdminList.js +1 -1
- package/lib/hooks/useClickOutside.js +1 -1
- package/lib/hooks/useRelationData.js +2 -1
- package/lib/layouts/AdminDataLayout.js +87 -14
- package/lib/pages/AdminDataCreatePage.d.ts +1 -1
- package/lib/pages/AdminDataCreatePage.js +68 -45
- package/lib/pages/AdminDataEditPage.js +71 -38
- package/lib/pages/AdminDataListPage.js +116 -85
- package/lib/utils/graphql-utils.d.ts +7 -1
- package/lib/utils/graphql-utils.js +343 -331
- package/lib/utils/secure-storage.js +26 -20
- package/lib/utils/string-utils.js +31 -8
- 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
|
|
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:
|
|
40
|
+
query: sdkRecord[singleQueryDocumentName],
|
|
39
41
|
// For single item
|
|
40
|
-
listQuery:
|
|
42
|
+
listQuery: sdkRecord[listQueryDocumentName],
|
|
41
43
|
// For lists
|
|
42
|
-
update:
|
|
43
|
-
delete:
|
|
44
|
-
create:
|
|
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.
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
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
|
-
|
|
489
|
+
model?.fields?.filter((field) => field.type.toLowerCase() === "boolean")?.map((field) => field.name) || []
|
|
456
490
|
);
|
|
457
491
|
const requiredArrayFields = new Set(
|
|
458
|
-
|
|
492
|
+
model?.fields?.filter((field) => field.isList && !field.isOptional && !field.relationName)?.map((field) => field.name) || []
|
|
459
493
|
);
|
|
460
494
|
const enumArrayFields = new Set(
|
|
461
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
};
|