@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.
@@ -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 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 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 formFields = editableFields.map((field) => {
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
- 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);
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
- return FormFieldClass.text(field.name, options);
161
+ break;
136
162
  case "int":
137
163
  case "bigint":
138
- return FormFieldClass.text(field.name, options);
164
+ formField = FormFieldClass.text(field.name, options);
165
+ break;
139
166
  case "float":
140
167
  case "decimal":
141
- return FormFieldClass.text(field.name, options);
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
- return FormFieldClass.checkbox(field.name, {
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
- return FormFieldClass.dateTimePicker(field.name, options);
180
+ formField = FormFieldClass.dateTimePicker(field.name, options);
181
+ break;
152
182
  case "date":
153
- return FormFieldClass.datePicker(field.name, options);
154
- default:
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
- return FormFieldClass.select(field.name, {
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 adminDocumentName = `__Admin${field.type}sDocument`;
176
- const regularDocumentName = `${field.type}sDocument`;
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
- return FormFieldClass.searchSelectApollo(relationFieldName, {
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: field.type.toLowerCase() + "s",
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
- return FormFieldClass.text(relationFieldName, {
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
- return FormFieldClass.select(field.name, {
293
+ formField = FormFieldClass.select(field.name, {
246
294
  ...options,
247
295
  options: selectOptions
248
296
  });
297
+ break;
249
298
  }
250
- return FormFieldClass.text(field.name, options);
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.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.12",
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/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.12",
42
+ "@nestledjs/shared-components": "^0.1.15",
43
43
  "react": "^19.0.0",
44
44
  "react-router": "^7.0.0"
45
45
  },