@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.
- package/README.md +43 -39
- package/index.js +4 -1
- package/lib/components/ExportButton.d.ts +7 -7
- package/lib/components/ExportButton.js +133 -36
- package/lib/components/RelationFieldWrapper.d.ts +1 -1
- package/lib/components/RelationFieldWrapper.js +1 -1
- package/lib/components/filters/DateRangeFilter.d.ts +1 -1
- package/lib/components/filters/EnumFilter.d.ts +1 -1
- package/lib/components/filters/NumberRangeFilter.d.ts +1 -1
- 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 +14 -6
- 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/useClickOutside.js +1 -1
- package/lib/hooks/useRelationData.js +1 -0
- package/lib/layouts/AdminDataLayout.js +84 -9
- package/lib/pages/AdminDataCreatePage.d.ts +1 -1
- package/lib/pages/AdminDataCreatePage.js +66 -46
- package/lib/pages/AdminDataEditPage.js +53 -19
- package/lib/pages/AdminDataListPage.js +87 -79
- package/lib/utils/graphql-utils.d.ts +7 -1
- package/lib/utils/graphql-utils.js +337 -320
- package/lib/utils/secure-storage.js +23 -14
- package/lib/utils/string-utils.js +31 -8
- package/package.json +3 -3
|
@@ -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,7 +72,7 @@ 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 "";
|
|
@@ -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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
};
|