@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.
@@ -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 buildFormFields(sdk, model, operation, currentItem, isSubmitting) {
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 formFields = editableFields.map((field) => {
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
- return FormFieldClass.email(field.name, options);
131
- }
132
- if (field.name.toLowerCase().includes("description") || field.name.toLowerCase().includes("content") || field.name.toLowerCase().includes("notes")) {
133
- return FormFieldClass.textArea(field.name, options);
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
- return FormFieldClass.text(field.name, options);
149
+ break;
136
150
  case "int":
137
151
  case "bigint":
138
- return FormFieldClass.text(field.name, options);
152
+ formField = FormFieldClass.text(field.name, options);
153
+ break;
139
154
  case "float":
140
155
  case "decimal":
141
- return FormFieldClass.text(field.name, options);
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
- return FormFieldClass.checkbox(field.name, {
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
- return FormFieldClass.dateTimePicker(field.name, options);
168
+ formField = FormFieldClass.dateTimePicker(field.name, options);
169
+ break;
152
170
  case "date":
153
- return FormFieldClass.datePicker(field.name, options);
154
- default:
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
- return FormFieldClass.select(field.name, {
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 adminDocumentName = `__Admin${field.type}sDocument`;
176
- const regularDocumentName = `${field.type}sDocument`;
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
- return FormFieldClass.searchSelectApollo(relationFieldName, {
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: field.type.toLowerCase() + "s",
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
- return FormFieldClass.text(relationFieldName, {
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
- return FormFieldClass.select(field.name, {
281
+ formField = FormFieldClass.select(field.name, {
246
282
  ...options,
247
283
  options: selectOptions
248
284
  });
285
+ break;
249
286
  }
250
- return FormFieldClass.text(field.name, options);
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.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
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.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2");
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.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
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.11",
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/admin-data"
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.11",
42
+ "@nestledjs/shared-components": "^0.1.12",
43
43
  "react": "^19.0.0",
44
44
  "react-router": "^7.0.0"
45
45
  },