@nestledjs/data-browser 0.1.11 → 0.1.13
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 +46 -83
- package/lib/components/filters/NumberRangeFilter.js +58 -99
- package/lib/components/filters/RelationComponents.d.ts +4 -4
- package/lib/components/filters/RelationComponents.js +97 -215
- package/lib/components/filters/RelationFilterField.js +50 -91
- package/lib/components/index.d.ts +1 -0
- package/lib/components/shared/AdminBreadcrumbs.js +40 -88
- package/lib/components/shared/AdminErrorStates.js +87 -141
- package/lib/components/shared/AdminStatusDisplay.js +28 -61
- package/lib/context/AdminDataContext.d.ts +1 -1
- package/lib/context/AdminDataContext.js +13 -9
- package/lib/hooks/useRelationData.d.ts +1 -1
- package/lib/hooks/useRelationData.js +3 -2
- package/lib/layouts/AdminDataLayout.js +184 -280
- package/lib/pages/AdminDataCreatePage.js +256 -403
- package/lib/pages/AdminDataEditPage.js +443 -702
- package/lib/pages/AdminDataIndexPage.js +53 -80
- package/lib/pages/AdminDataListPage.js +608 -1111
- package/lib/utils/graphql-utils.d.ts +1 -1
- package/lib/utils/graphql-utils.js +119 -24
- package/lib/utils/string-utils.js +3 -3
- package/package.json +3 -3
|
@@ -17,7 +17,7 @@ export declare function getMutationName(model: DatabaseModel, operation: 'create
|
|
|
17
17
|
/**
|
|
18
18
|
* Build form fields for a model and operation
|
|
19
19
|
*/
|
|
20
|
-
export declare function buildFormFields(sdk: any, model: DatabaseModel, operation: 'create' | 'update', currentItem?: any, isSubmitting?: boolean): FormField[];
|
|
20
|
+
export declare function buildFormFields(sdk: any, model: DatabaseModel, operation: 'create' | 'update', currentItem?: any, isSubmitting?: boolean, basePath?: string): FormField[];
|
|
21
21
|
/**
|
|
22
22
|
* Clean form input data for GraphQL mutations
|
|
23
23
|
* Removes Apollo metadata and system fields
|
|
@@ -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,24 @@ 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 buildFormFields(sdk, model, operation, currentItem, isSubmitting, basePath = "/admin/data") {
|
|
82
92
|
const editableFields = model.fields.filter((field) => {
|
|
83
93
|
if (operation === "create" && field.isId) return false;
|
|
84
94
|
if (field.isReadOnly || field.isGenerated) return false;
|
|
85
95
|
if (field.isUpdatedAt || field.name === "createdAt") return false;
|
|
86
|
-
if (field.relationName && field.isList) return false;
|
|
87
96
|
return true;
|
|
88
97
|
});
|
|
89
|
-
const
|
|
98
|
+
const regularFields = editableFields.filter((f) => !(f.relationName && f.isList));
|
|
99
|
+
const listRelationFields = editableFields.filter((f) => f.relationName && f.isList);
|
|
100
|
+
const formFields = [];
|
|
101
|
+
regularFields.forEach((field) => {
|
|
90
102
|
var _a;
|
|
91
103
|
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
92
104
|
let initialValue = currentItem && operation === "update" ? currentItem[field.name] : void 0;
|
|
@@ -124,44 +136,52 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
124
136
|
required: isRequired,
|
|
125
137
|
...initialValue !== void 0 && { value: initialValue }
|
|
126
138
|
};
|
|
139
|
+
let formField;
|
|
127
140
|
switch (field.type.toLowerCase()) {
|
|
128
141
|
case "string":
|
|
129
142
|
if (field.name.toLowerCase().includes("email")) {
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
formField = FormFieldClass.email(field.name, options);
|
|
144
|
+
} else if (field.name.toLowerCase().includes("description") || field.name.toLowerCase().includes("content") || field.name.toLowerCase().includes("notes")) {
|
|
145
|
+
formField = FormFieldClass.textArea(field.name, options);
|
|
146
|
+
} else {
|
|
147
|
+
formField = FormFieldClass.text(field.name, options);
|
|
134
148
|
}
|
|
135
|
-
|
|
149
|
+
break;
|
|
136
150
|
case "int":
|
|
137
151
|
case "bigint":
|
|
138
|
-
|
|
152
|
+
formField = FormFieldClass.text(field.name, options);
|
|
153
|
+
break;
|
|
139
154
|
case "float":
|
|
140
155
|
case "decimal":
|
|
141
|
-
|
|
156
|
+
formField = FormFieldClass.text(field.name, options);
|
|
157
|
+
break;
|
|
142
158
|
case "boolean": {
|
|
143
159
|
const booleanValue = currentItem && operation === "update" ? Boolean(currentItem[field.name]) : false;
|
|
144
|
-
|
|
160
|
+
formField = FormFieldClass.checkbox(field.name, {
|
|
145
161
|
...options,
|
|
146
162
|
required: false,
|
|
147
163
|
...operation === "update" && { value: booleanValue }
|
|
148
164
|
});
|
|
165
|
+
break;
|
|
149
166
|
}
|
|
150
167
|
case "datetime":
|
|
151
|
-
|
|
168
|
+
formField = FormFieldClass.dateTimePicker(field.name, options);
|
|
169
|
+
break;
|
|
152
170
|
case "date":
|
|
153
|
-
|
|
154
|
-
|
|
171
|
+
formField = FormFieldClass.datePicker(field.name, options);
|
|
172
|
+
break;
|
|
173
|
+
default: {
|
|
155
174
|
const enumValues = getEnumValues(sdk, field.type);
|
|
156
175
|
if (enumValues) {
|
|
157
176
|
const selectOptions = enumValues.map((value) => ({
|
|
158
177
|
value,
|
|
159
178
|
label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
|
|
160
179
|
}));
|
|
161
|
-
|
|
180
|
+
formField = FormFieldClass.select(field.name, {
|
|
162
181
|
...options,
|
|
163
182
|
options: selectOptions
|
|
164
183
|
});
|
|
184
|
+
break;
|
|
165
185
|
}
|
|
166
186
|
if (field.relationName && !field.isList) {
|
|
167
187
|
const relationFieldName = ((_a = field.relationFromFields) == null ? void 0 : _a[0]) || field.name;
|
|
@@ -172,8 +192,9 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
172
192
|
if (relationValue === null) {
|
|
173
193
|
relationValue = "";
|
|
174
194
|
}
|
|
175
|
-
const
|
|
176
|
-
const
|
|
195
|
+
const properPluralName = getPluralName(field.type);
|
|
196
|
+
const adminDocumentName = `__Admin${properPluralName}Document`;
|
|
197
|
+
const regularDocumentName = `${properPluralName}Document`;
|
|
177
198
|
const relationDocument = sdk[adminDocumentName] || sdk[regularDocumentName];
|
|
178
199
|
if (relationDocument) {
|
|
179
200
|
let searchField = "id";
|
|
@@ -199,12 +220,12 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
199
220
|
];
|
|
200
221
|
}
|
|
201
222
|
}
|
|
202
|
-
|
|
223
|
+
formField = FormFieldClass.searchSelectApollo(relationFieldName, {
|
|
203
224
|
label,
|
|
204
225
|
// Remove "ID" suffix - just use the field name
|
|
205
226
|
required: options.required,
|
|
206
|
-
dataType:
|
|
207
|
-
// e.g., Course → courses
|
|
227
|
+
dataType: properPluralName.charAt(0).toLowerCase() + properPluralName.slice(1),
|
|
228
|
+
// e.g., Course → courses (camelCase)
|
|
208
229
|
document: relationDocument,
|
|
209
230
|
searchFields: [searchField],
|
|
210
231
|
// For searching
|
|
@@ -223,18 +244,33 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
223
244
|
},
|
|
224
245
|
...initialOptions.length > 0 && { initialOptions },
|
|
225
246
|
// Provide initial options if available
|
|
226
|
-
...relationValue !== void 0 && { value: relationValue }
|
|
247
|
+
...relationValue !== void 0 && { value: relationValue },
|
|
248
|
+
// Add custom wrapper to show view record link
|
|
249
|
+
customWrapper: (fieldElement) => {
|
|
250
|
+
return React.createElement(
|
|
251
|
+
RelationFieldWrapper,
|
|
252
|
+
{
|
|
253
|
+
relationType: field.type,
|
|
254
|
+
initialValue: relationValue,
|
|
255
|
+
fieldName: relationFieldName,
|
|
256
|
+
basePath
|
|
257
|
+
},
|
|
258
|
+
fieldElement
|
|
259
|
+
);
|
|
260
|
+
}
|
|
227
261
|
});
|
|
262
|
+
break;
|
|
228
263
|
} else {
|
|
229
264
|
console.warn(
|
|
230
265
|
`GraphQL document ${adminDocumentName} or ${regularDocumentName} not found for relation ${field.type}. Using text input instead.`
|
|
231
266
|
);
|
|
232
|
-
|
|
267
|
+
formField = FormFieldClass.text(relationFieldName, {
|
|
233
268
|
label: `${label} ID`,
|
|
234
269
|
required: options.required,
|
|
235
270
|
helpText: "Enter the ID of the related record",
|
|
236
271
|
...relationValue !== void 0 && { value: relationValue }
|
|
237
272
|
});
|
|
273
|
+
break;
|
|
238
274
|
}
|
|
239
275
|
}
|
|
240
276
|
if (field.kind === "enum" && field.enumValues) {
|
|
@@ -242,12 +278,71 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
|
|
|
242
278
|
value,
|
|
243
279
|
label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
|
|
244
280
|
}));
|
|
245
|
-
|
|
281
|
+
formField = FormFieldClass.select(field.name, {
|
|
246
282
|
...options,
|
|
247
283
|
options: selectOptions
|
|
248
284
|
});
|
|
285
|
+
break;
|
|
249
286
|
}
|
|
250
|
-
|
|
287
|
+
formField = FormFieldClass.text(field.name, options);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
formFields.push(formField);
|
|
292
|
+
});
|
|
293
|
+
listRelationFields.forEach((field) => {
|
|
294
|
+
var _a, _b;
|
|
295
|
+
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
296
|
+
if (operation === "update" && currentItem) {
|
|
297
|
+
const pluralModelName = getPluralName(field.type);
|
|
298
|
+
const relatedModelKebab = toKebabCase(pluralModelName);
|
|
299
|
+
const displayName = field.type.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
300
|
+
const pluralDisplayName = getPluralName(displayName);
|
|
301
|
+
const foreignKeyFieldName = ((_a = field.relationToFields) == null ? void 0 : _a[0]) || `${toLowerCamelCase(model.name)}Id`;
|
|
302
|
+
const filterUrl = `${basePath}/${relatedModelKebab}?${foreignKeyFieldName}=${currentItem.id}`;
|
|
303
|
+
const countData = (_b = currentItem._count) == null ? void 0 : _b[field.name];
|
|
304
|
+
const countText = countData !== void 0 ? ` (${countData})` : "";
|
|
305
|
+
const formField = FormFieldClass.content(field.name, {
|
|
306
|
+
content: React.createElement("div", { className: "py-2" }, [
|
|
307
|
+
React.createElement(
|
|
308
|
+
"label",
|
|
309
|
+
{
|
|
310
|
+
key: "label",
|
|
311
|
+
className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
|
312
|
+
},
|
|
313
|
+
label
|
|
314
|
+
),
|
|
315
|
+
React.createElement(
|
|
316
|
+
Link,
|
|
317
|
+
{
|
|
318
|
+
key: "link",
|
|
319
|
+
to: filterUrl,
|
|
320
|
+
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"
|
|
321
|
+
},
|
|
322
|
+
[
|
|
323
|
+
React.createElement(
|
|
324
|
+
"svg",
|
|
325
|
+
{
|
|
326
|
+
key: "icon",
|
|
327
|
+
className: "h-4 w-4 mr-1",
|
|
328
|
+
fill: "none",
|
|
329
|
+
stroke: "currentColor",
|
|
330
|
+
viewBox: "0 0 24 24",
|
|
331
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
332
|
+
},
|
|
333
|
+
React.createElement("path", {
|
|
334
|
+
strokeLinecap: "round",
|
|
335
|
+
strokeLinejoin: "round",
|
|
336
|
+
strokeWidth: 2,
|
|
337
|
+
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"
|
|
338
|
+
})
|
|
339
|
+
),
|
|
340
|
+
`See Related ${pluralDisplayName}${countText}`
|
|
341
|
+
]
|
|
342
|
+
)
|
|
343
|
+
])
|
|
344
|
+
});
|
|
345
|
+
formFields.push(formField);
|
|
251
346
|
}
|
|
252
347
|
});
|
|
253
348
|
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.13",
|
|
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.12",
|
|
43
43
|
"react": "^19.0.0",
|
|
44
44
|
"react-router": "^7.0.0"
|
|
45
45
|
},
|