@nestledjs/data-browser 0.1.25 → 1.0.0
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/AdminDataStateMessage.d.ts +15 -0
- package/lib/components/AdminDataStateMessage.js +44 -0
- package/lib/components/FilterField.d.ts +14 -0
- package/lib/components/FilterField.js +152 -0
- package/lib/components/filters/EnumFilter.d.ts +8 -0
- package/lib/components/filters/EnumFilter.js +39 -0
- package/lib/components/filters/index.d.ts +1 -0
- package/lib/context/AdminDataContext.d.ts +16 -1
- package/lib/context/AdminDataContext.js +4 -3
- package/lib/pages/AdminDataCreatePage.js +9 -4
- package/lib/pages/AdminDataEditPage.js +313 -246
- package/lib/pages/AdminDataListPage.js +65 -101
- package/lib/utils/graphql-utils.d.ts +12 -1
- package/lib/utils/graphql-utils.js +59 -36
- package/lib/utils/string-utils.js +14 -0
- package/package.json +2 -2
|
@@ -6,9 +6,7 @@ import { getPluralName } from "../utils/get-plural-names.js";
|
|
|
6
6
|
import { AdminLocalStorage } from "../utils/secure-storage.js";
|
|
7
7
|
import { kebabCase, formatFieldName } from "../utils/string-utils.js";
|
|
8
8
|
import { useParams, useSearchParams, Link } from "react-router";
|
|
9
|
-
import {
|
|
10
|
-
import { NumberRangeFilter } from "../components/filters/NumberRangeFilter.js";
|
|
11
|
-
import { RelationFilterField } from "../components/filters/RelationFilterField.js";
|
|
9
|
+
import { FilterField } from "../components/FilterField.js";
|
|
12
10
|
import { useAdminList } from "../hooks/useAdminList.js";
|
|
13
11
|
import { getAdminDocuments } from "../utils/graphql-utils.js";
|
|
14
12
|
import { useAdminDataContext } from "../context/AdminDataContext.js";
|
|
@@ -32,6 +30,36 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
32
30
|
return null;
|
|
33
31
|
}
|
|
34
32
|
}, [sdk]);
|
|
33
|
+
const updateRelatedEnumFilter = useCallback((filters2, relationName, enumFieldName, value) => {
|
|
34
|
+
const newFilters = { ...filters2 };
|
|
35
|
+
if (value === void 0 || value === null || value === "") {
|
|
36
|
+
const relationFilter = newFilters[relationName];
|
|
37
|
+
if (relationFilter && typeof relationFilter === "object") {
|
|
38
|
+
delete relationFilter[enumFieldName];
|
|
39
|
+
if (Object.keys(relationFilter).length === 0) {
|
|
40
|
+
delete newFilters[relationName];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
if (!newFilters[relationName]) {
|
|
45
|
+
newFilters[relationName] = {};
|
|
46
|
+
}
|
|
47
|
+
const relationFilter = newFilters[relationName];
|
|
48
|
+
if (relationFilter && typeof relationFilter === "object") {
|
|
49
|
+
relationFilter[enumFieldName] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return newFilters;
|
|
53
|
+
}, []);
|
|
54
|
+
const updateSimpleFilter = useCallback((filters2, fieldName, value) => {
|
|
55
|
+
const newFilters = { ...filters2 };
|
|
56
|
+
if (value === void 0 || value === null || value === "") {
|
|
57
|
+
delete newFilters[fieldName];
|
|
58
|
+
} else {
|
|
59
|
+
newFilters[fieldName] = value;
|
|
60
|
+
}
|
|
61
|
+
return newFilters;
|
|
62
|
+
}, []);
|
|
35
63
|
const { state, dispatch } = useAdminList();
|
|
36
64
|
const {
|
|
37
65
|
search,
|
|
@@ -145,7 +173,7 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
145
173
|
}, [model]);
|
|
146
174
|
const filterableFieldNames = useMemo(() => {
|
|
147
175
|
if (!model) return [];
|
|
148
|
-
|
|
176
|
+
const directFields = model.fields.filter((field) => {
|
|
149
177
|
if (field.relationName && !field.isList) return true;
|
|
150
178
|
if (field.relationName) return false;
|
|
151
179
|
if (field.isList) return false;
|
|
@@ -153,7 +181,21 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
153
181
|
const fieldType = field.type.toLowerCase();
|
|
154
182
|
return ["boolean", "int", "float", "date", "datetime"].includes(fieldType) || field.kind === "enum";
|
|
155
183
|
}).map((field) => field.name);
|
|
156
|
-
|
|
184
|
+
const relatedEnumFields = [];
|
|
185
|
+
model.fields.forEach((field) => {
|
|
186
|
+
if (field.relationName && !field.isList) {
|
|
187
|
+
const relatedModel = databaseModels.find((m) => m.name === field.type);
|
|
188
|
+
if (relatedModel) {
|
|
189
|
+
relatedModel.fields.forEach((relatedField) => {
|
|
190
|
+
if (relatedField.kind === "enum") {
|
|
191
|
+
relatedEnumFields.push(`${field.name}.${relatedField.name}`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return [...directFields, ...relatedEnumFields];
|
|
198
|
+
}, [model, databaseModels]);
|
|
157
199
|
const searchableFieldNames = useMemo(() => {
|
|
158
200
|
if (!model) return [];
|
|
159
201
|
return model.fields.filter((field) => {
|
|
@@ -588,108 +630,30 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
588
630
|
] })
|
|
589
631
|
] }),
|
|
590
632
|
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4", children: filterableFieldNames.map((fieldName) => {
|
|
591
|
-
const
|
|
592
|
-
if (!field) return null;
|
|
593
|
-
const currentValue = filters[fieldName];
|
|
633
|
+
const isRelatedEnumField = fieldName.includes(".");
|
|
594
634
|
const handleChange = (value) => {
|
|
595
|
-
|
|
596
|
-
if (
|
|
597
|
-
|
|
635
|
+
let newFilters;
|
|
636
|
+
if (isRelatedEnumField) {
|
|
637
|
+
const [relationName, enumFieldName] = fieldName.split(".");
|
|
638
|
+
newFilters = updateRelatedEnumFilter(filters, relationName, enumFieldName, value);
|
|
598
639
|
} else {
|
|
599
|
-
newFilters
|
|
640
|
+
newFilters = updateSimpleFilter(filters, fieldName, value);
|
|
600
641
|
}
|
|
601
642
|
setFilters(newFilters);
|
|
602
643
|
dispatch({ type: "RESET_PAGINATION" });
|
|
603
644
|
};
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (field.type.toLowerCase() === "datetime" || field.type.toLowerCase() === "date") {
|
|
617
|
-
return /* @__PURE__ */ jsx(
|
|
618
|
-
DateRangeFilter,
|
|
619
|
-
{
|
|
620
|
-
fieldName,
|
|
621
|
-
currentValue,
|
|
622
|
-
onChange: handleChange
|
|
623
|
-
},
|
|
624
|
-
fieldName
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
if (["int", "bigint", "float", "decimal"].includes(field.type.toLowerCase())) {
|
|
628
|
-
return /* @__PURE__ */ jsx(
|
|
629
|
-
NumberRangeFilter,
|
|
630
|
-
{
|
|
631
|
-
fieldName,
|
|
632
|
-
fieldType: field.type.toLowerCase(),
|
|
633
|
-
currentValue,
|
|
634
|
-
onChange: handleChange
|
|
635
|
-
},
|
|
636
|
-
fieldName
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
if (field.kind === "enum") {
|
|
640
|
-
const enumValues = getEnumValues(field.type);
|
|
641
|
-
if (enumValues) {
|
|
642
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
643
|
-
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700", children: formatFieldName(fieldName) }),
|
|
644
|
-
/* @__PURE__ */ jsxs(
|
|
645
|
-
"select",
|
|
646
|
-
{
|
|
647
|
-
value: typeof currentValue === "string" ? currentValue : "",
|
|
648
|
-
onChange: (e) => handleChange(e.target.value || void 0),
|
|
649
|
-
className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web text-sm",
|
|
650
|
-
children: [
|
|
651
|
-
/* @__PURE__ */ jsx("option", { value: "", children: "All" }),
|
|
652
|
-
enumValues.map((val) => /* @__PURE__ */ jsx("option", { value: val, children: val }, val))
|
|
653
|
-
]
|
|
654
|
-
}
|
|
655
|
-
)
|
|
656
|
-
] }, fieldName);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
if (field.type.toLowerCase() === "boolean") {
|
|
660
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
661
|
-
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700", children: formatFieldName(fieldName) }),
|
|
662
|
-
/* @__PURE__ */ jsxs(
|
|
663
|
-
"select",
|
|
664
|
-
{
|
|
665
|
-
value: currentValue === void 0 || currentValue === null ? "" : currentValue.toString(),
|
|
666
|
-
onChange: (e) => {
|
|
667
|
-
const value = e.target.value;
|
|
668
|
-
handleChange(value === "" ? void 0 : value === "true");
|
|
669
|
-
},
|
|
670
|
-
className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web text-sm",
|
|
671
|
-
children: [
|
|
672
|
-
/* @__PURE__ */ jsx("option", { value: "", children: "All" }),
|
|
673
|
-
/* @__PURE__ */ jsx("option", { value: "true", children: "Yes" }),
|
|
674
|
-
/* @__PURE__ */ jsx("option", { value: "false", children: "No" })
|
|
675
|
-
]
|
|
676
|
-
}
|
|
677
|
-
)
|
|
678
|
-
] }, fieldName);
|
|
679
|
-
}
|
|
680
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
681
|
-
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700", children: formatFieldName(fieldName) }),
|
|
682
|
-
/* @__PURE__ */ jsx(
|
|
683
|
-
"input",
|
|
684
|
-
{
|
|
685
|
-
type: "text",
|
|
686
|
-
value: typeof currentValue === "string" ? currentValue : "",
|
|
687
|
-
onChange: (e) => handleChange(e.target.value || void 0),
|
|
688
|
-
placeholder: `Filter by ${formatFieldName(fieldName).toLowerCase()}...`,
|
|
689
|
-
className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web text-sm"
|
|
690
|
-
}
|
|
691
|
-
)
|
|
692
|
-
] }, fieldName);
|
|
645
|
+
return /* @__PURE__ */ jsx(
|
|
646
|
+
FilterField,
|
|
647
|
+
{
|
|
648
|
+
fieldName,
|
|
649
|
+
model,
|
|
650
|
+
databaseModels,
|
|
651
|
+
filters,
|
|
652
|
+
onChange: handleChange,
|
|
653
|
+
getEnumValues
|
|
654
|
+
},
|
|
655
|
+
fieldName
|
|
656
|
+
);
|
|
693
657
|
}) })
|
|
694
658
|
] });
|
|
695
659
|
const searchFilter = /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FormField } from '@nestledjs/forms';
|
|
2
2
|
import { DatabaseModel } from '../types';
|
|
3
|
+
import { DisplayFieldConfig } from '../context/AdminDataContext';
|
|
3
4
|
/**
|
|
4
5
|
* Get GraphQL documents for admin CRUD operations
|
|
5
6
|
*/
|
|
@@ -14,10 +15,20 @@ export declare function getAdminDocuments(sdk: any, model: DatabaseModel): {
|
|
|
14
15
|
* Get the GraphQL mutation name for a given operation
|
|
15
16
|
*/
|
|
16
17
|
export declare function getMutationName(model: DatabaseModel, operation: 'create' | 'update' | 'delete'): string;
|
|
18
|
+
/**
|
|
19
|
+
* Options for building form fields
|
|
20
|
+
*/
|
|
21
|
+
export interface BuildFormFieldsOptions {
|
|
22
|
+
currentItem?: any;
|
|
23
|
+
isSubmitting?: boolean;
|
|
24
|
+
basePath?: string;
|
|
25
|
+
databaseModels?: DatabaseModel[];
|
|
26
|
+
displayFieldConfig?: DisplayFieldConfig;
|
|
27
|
+
}
|
|
17
28
|
/**
|
|
18
29
|
* Build form fields for a model and operation
|
|
19
30
|
*/
|
|
20
|
-
export declare function buildFormFields(sdk: any, model: DatabaseModel, operation: 'create' | 'update',
|
|
31
|
+
export declare function buildFormFields(sdk: any, model: DatabaseModel, operation: 'create' | 'update', options?: BuildFormFieldsOptions): FormField[];
|
|
21
32
|
/**
|
|
22
33
|
* Clean form input data for GraphQL mutations
|
|
23
34
|
* Removes Apollo metadata and system fields
|
|
@@ -9,6 +9,9 @@ function getEnumValues(sdk, enumType) {
|
|
|
9
9
|
if (!enumObject || typeof enumObject !== "object") {
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
|
+
if (enumObject.kind === "Document" || enumObject.definitions) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
12
15
|
const values = Object.values(enumObject).filter((value) => typeof value === "string");
|
|
13
16
|
if (values.length === 0) {
|
|
14
17
|
const keys = Object.keys(enumObject).filter((key) => isNaN(Number(key)));
|
|
@@ -31,11 +34,11 @@ function getAdminDocuments(sdk, model) {
|
|
|
31
34
|
}
|
|
32
35
|
const normalizedModelName = normalizeModelNameForDocument(model.name);
|
|
33
36
|
const normalizedPluralName = normalizeModelNameForDocument(getPluralName(model.name));
|
|
34
|
-
const singleQueryDocumentName = `__Admin${normalizedModelName}
|
|
35
|
-
const listQueryDocumentName = `__Admin${normalizedPluralName}
|
|
36
|
-
const updateDocumentName = `__AdminUpdate${normalizedModelName}
|
|
37
|
-
const deleteDocumentName = `__AdminDelete${normalizedModelName}
|
|
38
|
-
const createDocumentName = `__AdminCreate${normalizedModelName}
|
|
37
|
+
const singleQueryDocumentName = `__Admin${normalizedModelName}`;
|
|
38
|
+
const listQueryDocumentName = `__Admin${normalizedPluralName}`;
|
|
39
|
+
const updateDocumentName = `__AdminUpdate${normalizedModelName}`;
|
|
40
|
+
const deleteDocumentName = `__AdminDelete${normalizedModelName}`;
|
|
41
|
+
const createDocumentName = `__AdminCreate${normalizedModelName}`;
|
|
39
42
|
const documents = {
|
|
40
43
|
query: sdk[singleQueryDocumentName],
|
|
41
44
|
// For single item
|
|
@@ -90,9 +93,16 @@ function findForeignKeyFieldName(relatedModel, currentModelName, relationName) {
|
|
|
90
93
|
});
|
|
91
94
|
return ((_a = foreignKeyField == null ? void 0 : foreignKeyField.relationFromFields) == null ? void 0 : _a[0]) || null;
|
|
92
95
|
}
|
|
93
|
-
function buildFormFields(sdk, model, operation,
|
|
96
|
+
function buildFormFields(sdk, model, operation, options = {}) {
|
|
97
|
+
const {
|
|
98
|
+
currentItem,
|
|
99
|
+
isSubmitting = false,
|
|
100
|
+
basePath = "/admin/data",
|
|
101
|
+
databaseModels,
|
|
102
|
+
displayFieldConfig
|
|
103
|
+
} = options;
|
|
94
104
|
const editableFields = model.fields.filter((field) => {
|
|
95
|
-
if (
|
|
105
|
+
if (field.isId) return false;
|
|
96
106
|
if (field.isReadOnly || field.isGenerated) return false;
|
|
97
107
|
if (field.isUpdatedAt || field.name === "createdAt") return false;
|
|
98
108
|
return true;
|
|
@@ -100,6 +110,18 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
100
110
|
const regularFields = editableFields.filter((f) => !(f.relationName && f.isList));
|
|
101
111
|
const listRelationFields = editableFields.filter((f) => f.relationName && f.isList);
|
|
102
112
|
const formFields = [];
|
|
113
|
+
if (operation === "update" && currentItem) {
|
|
114
|
+
const idField = model.fields.find((f) => f.isId);
|
|
115
|
+
if (idField) {
|
|
116
|
+
formFields.push(
|
|
117
|
+
FormFieldClass.text(idField.name, {
|
|
118
|
+
label: idField.name.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase()),
|
|
119
|
+
disabled: true,
|
|
120
|
+
helpText: "ID fields are immutable and cannot be changed"
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
103
125
|
regularFields.forEach((field) => {
|
|
104
126
|
var _a;
|
|
105
127
|
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
@@ -132,7 +154,7 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
132
154
|
}
|
|
133
155
|
const isArrayField = Boolean(field.isList);
|
|
134
156
|
const isRequired = isArrayField ? false : !field.isOptional;
|
|
135
|
-
const
|
|
157
|
+
const options2 = {
|
|
136
158
|
label,
|
|
137
159
|
required: isRequired,
|
|
138
160
|
...initialValue !== void 0 && { value: initialValue }
|
|
@@ -141,35 +163,35 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
141
163
|
switch (field.type.toLowerCase()) {
|
|
142
164
|
case "string":
|
|
143
165
|
if (field.name.toLowerCase().includes("email")) {
|
|
144
|
-
formField = FormFieldClass.email(field.name,
|
|
166
|
+
formField = FormFieldClass.email(field.name, options2);
|
|
145
167
|
} else if (field.name.toLowerCase().includes("description") || field.name.toLowerCase().includes("content") || field.name.toLowerCase().includes("notes")) {
|
|
146
|
-
formField = FormFieldClass.textArea(field.name,
|
|
168
|
+
formField = FormFieldClass.textArea(field.name, options2);
|
|
147
169
|
} else {
|
|
148
|
-
formField = FormFieldClass.text(field.name,
|
|
170
|
+
formField = FormFieldClass.text(field.name, options2);
|
|
149
171
|
}
|
|
150
172
|
break;
|
|
151
173
|
case "int":
|
|
152
174
|
case "bigint":
|
|
153
|
-
formField = FormFieldClass.text(field.name,
|
|
175
|
+
formField = FormFieldClass.text(field.name, options2);
|
|
154
176
|
break;
|
|
155
177
|
case "float":
|
|
156
178
|
case "decimal":
|
|
157
|
-
formField = FormFieldClass.text(field.name,
|
|
179
|
+
formField = FormFieldClass.text(field.name, options2);
|
|
158
180
|
break;
|
|
159
181
|
case "boolean": {
|
|
160
182
|
const booleanValue = currentItem && operation === "update" ? Boolean(currentItem[field.name]) : false;
|
|
161
183
|
formField = FormFieldClass.checkbox(field.name, {
|
|
162
|
-
...
|
|
184
|
+
...options2,
|
|
163
185
|
required: false,
|
|
164
186
|
...operation === "update" && { value: booleanValue }
|
|
165
187
|
});
|
|
166
188
|
break;
|
|
167
189
|
}
|
|
168
190
|
case "datetime":
|
|
169
|
-
formField = FormFieldClass.dateTimePicker(field.name,
|
|
191
|
+
formField = FormFieldClass.dateTimePicker(field.name, options2);
|
|
170
192
|
break;
|
|
171
193
|
case "date":
|
|
172
|
-
formField = FormFieldClass.datePicker(field.name,
|
|
194
|
+
formField = FormFieldClass.datePicker(field.name, options2);
|
|
173
195
|
break;
|
|
174
196
|
default: {
|
|
175
197
|
const enumValues = getEnumValues(sdk, field.type);
|
|
@@ -179,7 +201,7 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
179
201
|
label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
|
|
180
202
|
}));
|
|
181
203
|
formField = FormFieldClass.select(field.name, {
|
|
182
|
-
...
|
|
204
|
+
...options2,
|
|
183
205
|
options: selectOptions
|
|
184
206
|
});
|
|
185
207
|
break;
|
|
@@ -200,25 +222,25 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
200
222
|
relationValue = "";
|
|
201
223
|
}
|
|
202
224
|
const properPluralName = getPluralName(field.type);
|
|
203
|
-
const adminDocumentName = `__Admin${properPluralName}
|
|
204
|
-
const regularDocumentName = `${properPluralName}
|
|
225
|
+
const adminDocumentName = `__Admin${properPluralName}`;
|
|
226
|
+
const regularDocumentName = `${properPluralName}`;
|
|
205
227
|
const relationDocument = sdk[adminDocumentName] || sdk[regularDocumentName];
|
|
206
228
|
if (relationDocument) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
229
|
+
const config = displayFieldConfig == null ? void 0 : displayFieldConfig[field.type];
|
|
230
|
+
const displayFields = (config == null ? void 0 : config.display) || ["name", "title"];
|
|
231
|
+
const searchFields = (config == null ? void 0 : config.search) || displayFields;
|
|
232
|
+
const getDisplayLabel = (item) => {
|
|
233
|
+
const allValues = displayFields.map((field2) => item[field2]).filter((val) => val != null && val !== "");
|
|
234
|
+
if (allValues.length > 0) {
|
|
235
|
+
return allValues.join(" ");
|
|
236
|
+
}
|
|
237
|
+
return item.id;
|
|
238
|
+
};
|
|
217
239
|
let initialOptions = [];
|
|
218
240
|
if (currentItem && operation === "update") {
|
|
219
241
|
const currentRelationData = currentItem[field.name];
|
|
220
242
|
if (currentRelationData && typeof currentRelationData === "object" && currentRelationData.id) {
|
|
221
|
-
const displayLabel = currentRelationData
|
|
243
|
+
const displayLabel = getDisplayLabel(currentRelationData);
|
|
222
244
|
initialOptions = [
|
|
223
245
|
{
|
|
224
246
|
value: currentRelationData.id,
|
|
@@ -230,16 +252,16 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
230
252
|
formField = FormFieldClass.searchSelectApollo(relationFieldName, {
|
|
231
253
|
label,
|
|
232
254
|
// Remove "ID" suffix - just use the field name
|
|
233
|
-
required:
|
|
255
|
+
required: options2.required,
|
|
234
256
|
dataType: properPluralName.charAt(0).toLowerCase() + properPluralName.slice(1),
|
|
235
257
|
// e.g., Course → courses (camelCase)
|
|
236
258
|
document: relationDocument,
|
|
237
|
-
searchFields
|
|
259
|
+
searchFields,
|
|
238
260
|
// For searching
|
|
239
261
|
selectOptionsFunction: (items) => {
|
|
240
262
|
const queryOptions = items.map((item) => ({
|
|
241
263
|
value: item.id,
|
|
242
|
-
label: item
|
|
264
|
+
label: getDisplayLabel(item)
|
|
243
265
|
}));
|
|
244
266
|
const allOptions = [...initialOptions];
|
|
245
267
|
queryOptions.forEach((option) => {
|
|
@@ -247,6 +269,7 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
247
269
|
allOptions.push(option);
|
|
248
270
|
}
|
|
249
271
|
});
|
|
272
|
+
allOptions.sort((a, b) => a.label.localeCompare(b.label));
|
|
250
273
|
return allOptions;
|
|
251
274
|
},
|
|
252
275
|
...initialOptions.length > 0 && { initialOptions },
|
|
@@ -270,7 +293,7 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
270
293
|
} else {
|
|
271
294
|
formField = FormFieldClass.text(relationFieldName, {
|
|
272
295
|
label: `${label} ID`,
|
|
273
|
-
required:
|
|
296
|
+
required: options2.required,
|
|
274
297
|
helpText: "Enter the ID of the related record",
|
|
275
298
|
...relationValue !== void 0 && { value: relationValue }
|
|
276
299
|
});
|
|
@@ -283,12 +306,12 @@ function buildFormFields(sdk, model, operation, currentItem, isSubmitting, baseP
|
|
|
283
306
|
label: value.replace(/_/g, " ").toLowerCase().replace(/^./, (str) => str.toUpperCase())
|
|
284
307
|
}));
|
|
285
308
|
formField = FormFieldClass.select(field.name, {
|
|
286
|
-
...
|
|
309
|
+
...options2,
|
|
287
310
|
options: selectOptions
|
|
288
311
|
});
|
|
289
312
|
break;
|
|
290
313
|
}
|
|
291
|
-
formField = FormFieldClass.text(field.name,
|
|
314
|
+
formField = FormFieldClass.text(field.name, options2);
|
|
292
315
|
break;
|
|
293
316
|
}
|
|
294
317
|
}
|
|
@@ -5,6 +5,20 @@ function spacedWords(name) {
|
|
|
5
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
|
+
if (fieldName.includes(".")) {
|
|
9
|
+
return fieldName.split(".").map((part) => {
|
|
10
|
+
if (part === part.toUpperCase() && part.length > 1) {
|
|
11
|
+
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
return part.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
14
|
+
}).join(" ");
|
|
15
|
+
}
|
|
16
|
+
if (fieldName === fieldName.toUpperCase() && fieldName.length > 1) {
|
|
17
|
+
if (fieldName.includes("_")) {
|
|
18
|
+
return fieldName.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
|
|
19
|
+
}
|
|
20
|
+
return fieldName.charAt(0).toUpperCase() + fieldName.slice(1).toLowerCase();
|
|
21
|
+
}
|
|
8
22
|
return fieldName.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
9
23
|
}
|
|
10
24
|
function normalizeModelNameForDocument(modelName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestledjs/data-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@apollo/client": "^4.0.0",
|
|
39
39
|
"@heroicons/react": "^2.0.0",
|
|
40
40
|
"@nestledjs/forms": "^0.5.0",
|
|
41
|
-
"@nestledjs/shared-components": "^0.
|
|
41
|
+
"@nestledjs/shared-components": "^1.0.0",
|
|
42
42
|
"react": "^19.0.0",
|
|
43
43
|
"react-router": "^7.0.0"
|
|
44
44
|
},
|