@nestledjs/data-browser 0.1.12 → 0.1.15
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/index.js +2 -0
- package/lib/components/RelationFieldWrapper.d.ts +9 -0
- package/lib/components/RelationFieldWrapper.js +63 -0
- package/lib/components/filters/DateRangeFilter.d.ts +1 -1
- package/lib/components/filters/DateRangeFilter.js +9 -9
- package/lib/components/filters/NumberRangeFilter.js +21 -21
- package/lib/components/filters/RelationComponents.d.ts +4 -4
- package/lib/components/filters/RelationComponents.js +16 -16
- package/lib/components/filters/RelationFilterField.js +16 -10
- package/lib/components/index.d.ts +1 -0
- package/lib/components/shared/AdminBreadcrumbs.js +3 -3
- package/lib/components/shared/AdminErrorStates.js +1 -1
- package/lib/components/shared/AdminStatusDisplay.js +1 -1
- package/lib/context/AdminDataContext.d.ts +1 -1
- package/lib/context/AdminDataContext.js +6 -5
- package/lib/hooks/useRelationData.d.ts +1 -1
- package/lib/hooks/useRelationData.js +3 -2
- package/lib/layouts/AdminDataLayout.js +53 -56
- package/lib/pages/AdminDataCreatePage.js +6 -5
- package/lib/pages/AdminDataEditPage.js +6 -5
- package/lib/pages/AdminDataIndexPage.js +14 -13
- package/lib/pages/AdminDataListPage.js +138 -99
- package/lib/utils/graphql-utils.d.ts +1 -1
- package/lib/utils/graphql-utils.js +132 -24
- package/lib/utils/string-utils.js +3 -3
- package/package.json +3 -3
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
1
3
|
import { FormFieldClass } from "@nestledjs/forms";
|
|
2
4
|
import { getPluralName } from "@nestledjs/helpers";
|
|
5
|
+
import { RelationFieldWrapper } from "../components/RelationFieldWrapper.js";
|
|
3
6
|
function getEnumValues(sdk, enumType) {
|
|
4
7
|
try {
|
|
5
8
|
const enumObject = sdk[enumType];
|
|
@@ -78,15 +81,36 @@ function getMutationName(model, operation) {
|
|
|
78
81
|
return modelName;
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
|
-
function
|
|
84
|
+
function toKebabCase(str) {
|
|
85
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
function toLowerCamelCase(str) {
|
|
88
|
+
if (!str) return "";
|
|
89
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
90
|
+
}
|
|
91
|
+
function findForeignKeyFieldName(relatedModel, currentModelName, relationName) {
|
|
92
|
+
var _a;
|
|
93
|
+
if (!relatedModel) return null;
|
|
94
|
+
const foreignKeyField = relatedModel.fields.find((f) => {
|
|
95
|
+
if (f.type !== currentModelName) return false;
|
|
96
|
+
if (!f.relationName) return false;
|
|
97
|
+
if (f.isList) return false;
|
|
98
|
+
if (relationName && f.relationName !== relationName) return false;
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
return ((_a = foreignKeyField == null ? void 0 : foreignKeyField.relationFromFields) == null ? void 0 : _a[0]) || null;
|
|
102
|
+
}
|
|
103
|
+
function buildFormFields(sdk, model, operation, currentItem, isSubmitting, basePath = "/admin/data", databaseModels) {
|
|
82
104
|
const editableFields = model.fields.filter((field) => {
|
|
83
105
|
if (operation === "create" && field.isId) return false;
|
|
84
106
|
if (field.isReadOnly || field.isGenerated) return false;
|
|
85
107
|
if (field.isUpdatedAt || field.name === "createdAt") return false;
|
|
86
|
-
if (field.relationName && field.isList) return false;
|
|
87
108
|
return true;
|
|
88
109
|
});
|
|
89
|
-
const
|
|
110
|
+
const regularFields = editableFields.filter((f) => !(f.relationName && f.isList));
|
|
111
|
+
const listRelationFields = editableFields.filter((f) => f.relationName && f.isList);
|
|
112
|
+
const formFields = [];
|
|
113
|
+
regularFields.forEach((field) => {
|
|
90
114
|
var _a;
|
|
91
115
|
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
92
116
|
let initialValue = currentItem && operation === "update" ? currentItem[field.name] : void 0;
|
|
@@ -124,44 +148,52 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
124
148
|
required: isRequired,
|
|
125
149
|
...initialValue !== void 0 && { value: initialValue }
|
|
126
150
|
};
|
|
151
|
+
let formField;
|
|
127
152
|
switch (field.type.toLowerCase()) {
|
|
128
153
|
case "string":
|
|
129
154
|
if (field.name.toLowerCase().includes("email")) {
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
155
|
+
formField = FormFieldClass.email(field.name, options);
|
|
156
|
+
} else if (field.name.toLowerCase().includes("description") || field.name.toLowerCase().includes("content") || field.name.toLowerCase().includes("notes")) {
|
|
157
|
+
formField = FormFieldClass.textArea(field.name, options);
|
|
158
|
+
} else {
|
|
159
|
+
formField = FormFieldClass.text(field.name, options);
|
|
134
160
|
}
|
|
135
|
-
|
|
161
|
+
break;
|
|
136
162
|
case "int":
|
|
137
163
|
case "bigint":
|
|
138
|
-
|
|
164
|
+
formField = FormFieldClass.text(field.name, options);
|
|
165
|
+
break;
|
|
139
166
|
case "float":
|
|
140
167
|
case "decimal":
|
|
141
|
-
|
|
168
|
+
formField = FormFieldClass.text(field.name, options);
|
|
169
|
+
break;
|
|
142
170
|
case "boolean": {
|
|
143
171
|
const booleanValue = currentItem && operation === "update" ? Boolean(currentItem[field.name]) : false;
|
|
144
|
-
|
|
172
|
+
formField = FormFieldClass.checkbox(field.name, {
|
|
145
173
|
...options,
|
|
146
174
|
required: false,
|
|
147
175
|
...operation === "update" && { value: booleanValue }
|
|
148
176
|
});
|
|
177
|
+
break;
|
|
149
178
|
}
|
|
150
179
|
case "datetime":
|
|
151
|
-
|
|
180
|
+
formField = FormFieldClass.dateTimePicker(field.name, options);
|
|
181
|
+
break;
|
|
152
182
|
case "date":
|
|
153
|
-
|
|
154
|
-
|
|
183
|
+
formField = FormFieldClass.datePicker(field.name, options);
|
|
184
|
+
break;
|
|
185
|
+
default: {
|
|
155
186
|
const enumValues = getEnumValues(sdk, field.type);
|
|
156
187
|
if (enumValues) {
|
|
157
188
|
const selectOptions = enumValues.map((value) => ({
|
|
158
189
|
value,
|
|
159
190
|
label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
|
|
160
191
|
}));
|
|
161
|
-
|
|
192
|
+
formField = FormFieldClass.select(field.name, {
|
|
162
193
|
...options,
|
|
163
194
|
options: selectOptions
|
|
164
195
|
});
|
|
196
|
+
break;
|
|
165
197
|
}
|
|
166
198
|
if (field.relationName && !field.isList) {
|
|
167
199
|
const relationFieldName = ((_a = field.relationFromFields) == null ? void 0 : _a[0]) || field.name;
|
|
@@ -172,8 +204,9 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
172
204
|
if (relationValue === null) {
|
|
173
205
|
relationValue = "";
|
|
174
206
|
}
|
|
175
|
-
const
|
|
176
|
-
const
|
|
207
|
+
const properPluralName = getPluralName(field.type);
|
|
208
|
+
const adminDocumentName = `__Admin${properPluralName}Document`;
|
|
209
|
+
const regularDocumentName = `${properPluralName}Document`;
|
|
177
210
|
const relationDocument = sdk[adminDocumentName] || sdk[regularDocumentName];
|
|
178
211
|
if (relationDocument) {
|
|
179
212
|
let searchField = "id";
|
|
@@ -199,12 +232,12 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
199
232
|
];
|
|
200
233
|
}
|
|
201
234
|
}
|
|
202
|
-
|
|
235
|
+
formField = FormFieldClass.searchSelectApollo(relationFieldName, {
|
|
203
236
|
label,
|
|
204
237
|
// Remove "ID" suffix - just use the field name
|
|
205
238
|
required: options.required,
|
|
206
|
-
dataType:
|
|
207
|
-
// e.g., Course → courses
|
|
239
|
+
dataType: properPluralName.charAt(0).toLowerCase() + properPluralName.slice(1),
|
|
240
|
+
// e.g., Course → courses (camelCase)
|
|
208
241
|
document: relationDocument,
|
|
209
242
|
searchFields: [searchField],
|
|
210
243
|
// For searching
|
|
@@ -223,18 +256,33 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
223
256
|
},
|
|
224
257
|
...initialOptions.length > 0 && { initialOptions },
|
|
225
258
|
// Provide initial options if available
|
|
226
|
-
...relationValue !== void 0 && { value: relationValue }
|
|
259
|
+
...relationValue !== void 0 && { value: relationValue },
|
|
260
|
+
// Add custom wrapper to show view record link
|
|
261
|
+
customWrapper: (fieldElement) => {
|
|
262
|
+
return React.createElement(
|
|
263
|
+
RelationFieldWrapper,
|
|
264
|
+
{
|
|
265
|
+
relationType: field.type,
|
|
266
|
+
initialValue: relationValue,
|
|
267
|
+
fieldName: relationFieldName,
|
|
268
|
+
basePath
|
|
269
|
+
},
|
|
270
|
+
fieldElement
|
|
271
|
+
);
|
|
272
|
+
}
|
|
227
273
|
});
|
|
274
|
+
break;
|
|
228
275
|
} else {
|
|
229
276
|
console.warn(
|
|
230
277
|
`GraphQL document ${adminDocumentName} or ${regularDocumentName} not found for relation ${field.type}. Using text input instead.`
|
|
231
278
|
);
|
|
232
|
-
|
|
279
|
+
formField = FormFieldClass.text(relationFieldName, {
|
|
233
280
|
label: `${label} ID`,
|
|
234
281
|
required: options.required,
|
|
235
282
|
helpText: "Enter the ID of the related record",
|
|
236
283
|
...relationValue !== void 0 && { value: relationValue }
|
|
237
284
|
});
|
|
285
|
+
break;
|
|
238
286
|
}
|
|
239
287
|
}
|
|
240
288
|
if (field.kind === "enum" && field.enumValues) {
|
|
@@ -242,12 +290,72 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
242
290
|
value,
|
|
243
291
|
label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
|
|
244
292
|
}));
|
|
245
|
-
|
|
293
|
+
formField = FormFieldClass.select(field.name, {
|
|
246
294
|
...options,
|
|
247
295
|
options: selectOptions
|
|
248
296
|
});
|
|
297
|
+
break;
|
|
249
298
|
}
|
|
250
|
-
|
|
299
|
+
formField = FormFieldClass.text(field.name, options);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
formFields.push(formField);
|
|
304
|
+
});
|
|
305
|
+
listRelationFields.forEach((field) => {
|
|
306
|
+
var _a;
|
|
307
|
+
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
308
|
+
if (operation === "update" && currentItem) {
|
|
309
|
+
const pluralModelName = getPluralName(field.type);
|
|
310
|
+
const relatedModelKebab = toKebabCase(pluralModelName);
|
|
311
|
+
const displayName = field.type.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
312
|
+
const pluralDisplayName = getPluralName(displayName);
|
|
313
|
+
const relatedModel = databaseModels == null ? void 0 : databaseModels.find((m) => m.name === field.type);
|
|
314
|
+
const foreignKeyFieldName = findForeignKeyFieldName(relatedModel, model.name, field.relationName) || `${toLowerCamelCase(model.name)}Id`;
|
|
315
|
+
const filterUrl = `${basePath}/${relatedModelKebab}?${foreignKeyFieldName}=${currentItem.id}`;
|
|
316
|
+
const countData = (_a = currentItem._count) == null ? void 0 : _a[field.name];
|
|
317
|
+
const countText = countData !== void 0 ? ` (${countData})` : "";
|
|
318
|
+
const formField = FormFieldClass.content(field.name, {
|
|
319
|
+
content: React.createElement("div", { className: "py-2" }, [
|
|
320
|
+
React.createElement(
|
|
321
|
+
"label",
|
|
322
|
+
{
|
|
323
|
+
key: "label",
|
|
324
|
+
className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
325
|
+
},
|
|
326
|
+
label
|
|
327
|
+
),
|
|
328
|
+
React.createElement(
|
|
329
|
+
Link,
|
|
330
|
+
{
|
|
331
|
+
key: "link",
|
|
332
|
+
to: filterUrl,
|
|
333
|
+
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"
|
|
334
|
+
},
|
|
335
|
+
[
|
|
336
|
+
React.createElement(
|
|
337
|
+
"svg",
|
|
338
|
+
{
|
|
339
|
+
key: "icon",
|
|
340
|
+
className: "h-4 w-4 mr-1",
|
|
341
|
+
fill: "none",
|
|
342
|
+
stroke: "currentColor",
|
|
343
|
+
viewBox: "0 0 24 24",
|
|
344
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
345
|
+
},
|
|
346
|
+
React.createElement("path", {
|
|
347
|
+
strokeLinecap: "round",
|
|
348
|
+
strokeLinejoin: "round",
|
|
349
|
+
strokeWidth: 2,
|
|
350
|
+
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"
|
|
351
|
+
})
|
|
352
|
+
),
|
|
353
|
+
`See Related ${pluralDisplayName}${countText}`
|
|
354
|
+
]
|
|
355
|
+
)
|
|
356
|
+
])
|
|
357
|
+
});
|
|
358
|
+
formFields.push(formField);
|
|
251
359
|
}
|
|
252
360
|
});
|
|
253
361
|
const loadingText = operation === "create" ? "Creating..." : "Updating...";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
function kebabCase(name) {
|
|
2
|
-
return name.
|
|
2
|
+
return name.replaceAll(/([a-z])([A-Z])/g, "$1-$2").replaceAll(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
3
3
|
}
|
|
4
4
|
function spacedWords(name) {
|
|
5
|
-
return name.
|
|
5
|
+
return name.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replaceAll(/([A-Z])([A-Z][a-z])/g, "$1 $2");
|
|
6
6
|
}
|
|
7
7
|
function formatFieldName(fieldName) {
|
|
8
|
-
return fieldName.
|
|
8
|
+
return fieldName.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
9
9
|
}
|
|
10
10
|
function normalizeModelNameForDocument(modelName) {
|
|
11
11
|
if (modelName === modelName.toUpperCase() && modelName.length > 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestledjs/data-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Universal admin data browser for Nestled framework projects with full CRUD operations",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
34
|
"url": "https://github.com/nestledjs/nestled_template.git",
|
|
35
|
-
"directory": "libs/
|
|
35
|
+
"directory": "libs/data-browser"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@apollo/client": "^4.0.0",
|
|
39
39
|
"@heroicons/react": "^2.0.0",
|
|
40
40
|
"@nestledjs/forms": "^0.5.0",
|
|
41
41
|
"@nestledjs/helpers": "^0.1.0",
|
|
42
|
-
"@nestledjs/shared-components": "^0.1.
|
|
42
|
+
"@nestledjs/shared-components": "^0.1.15",
|
|
43
43
|
"react": "^19.0.0",
|
|
44
44
|
"react-router": "^7.0.0"
|
|
45
45
|
},
|