@nestledjs/data-browser 1.0.15 → 1.0.16

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/README.md CHANGED
@@ -4,14 +4,12 @@ Universal admin data browser for Nestled framework projects with full CRUD opera
4
4
 
5
5
  ## Features
6
6
 
7
- - 🔍 **Auto-generated CRUD Interface** - Automatically generates admin UI for all Prisma models
8
- - 📊 **Advanced Data Table** - Sorting, filtering, pagination, column selection
9
- - 🔎 **Smart Search** - Multi-field text search with debouncing
10
- - 📝 **Dynamic Forms** - Auto-generated create/edit forms from model schema
11
- - 🎨 **Dark Mode Support** - Full dark mode theming
12
- - 💾 **Persistent Preferences** - Saves column visibility, sort order, and search preferences per model
13
- - 🔐 **Type-Safe** - Full TypeScript support with GraphQL code generation
14
- - 📱 **Responsive** - Mobile-friendly with fullscreen mode
7
+ - Auto-generated admin CRUD interface from Nestled model metadata
8
+ - Sorting, filtering, pagination, column selection, and search
9
+ - Dynamic create/edit forms based on generated model metadata
10
+ - Persistent per-model table preferences
11
+ - TypeScript-first API around a generated GraphQL SDK
12
+ - Responsive admin workflow suitable for operational tools
15
13
 
16
14
  ## Installation
17
15
 
@@ -32,6 +30,7 @@ This package requires a Nestled framework project with:
32
30
  - **@nestledjs/forms** for form generation
33
31
  - **Prisma** for database models
34
32
  - **Generated GraphQL SDK** with admin CRUD operations
33
+ - **A form theme** compatible with `@nestledjs/forms`
35
34
 
36
35
  ### Peer Dependencies
37
36
 
@@ -48,14 +47,10 @@ This package requires a Nestled framework project with:
48
47
 
49
48
  Your Nestled project must also export:
50
49
 
51
- 1. **Web UI Components** from `@your-project/web-ui`:
52
- - `WebUiDataTable`
53
- - `WebUiErrorBoundary`
54
-
55
- 2. **Form Theme** from `@your-project/shared/styles`:
50
+ 1. **Form Theme** from `@your-project/shared/styles`:
56
51
  - `formTheme`
57
52
 
58
- 3. **GraphQL SDK** from `@your-project/shared/sdk`:
53
+ 2. **GraphQL SDK** from `@your-project/shared/sdk`:
59
54
  - `DATABASE_MODELS` (auto-generated model metadata)
60
55
  - GraphQL documents with `__Admin*Document` naming
61
56
 
@@ -67,23 +62,14 @@ Your Nestled project must also export:
67
62
  pnpm add @nestledjs/data-browser
68
63
  ```
69
64
 
70
- ### Step 2: Update Import Paths
71
-
72
- Since this package imports from your project's namespaced packages, find and replace in the source:
73
-
74
- ```
75
- @nestled-template → @your-project-name
76
- ```
77
-
78
- This affects 3 files in `libs/admin-data/src/lib/pages/`.
79
-
80
- ### Step 3: Create Route Wrapper
65
+ ### Step 2: Create Route Wrapper
81
66
 
82
67
  Create `apps/web/app/routes/admin/data/_layout.tsx`:
83
68
 
84
69
  ```typescript
85
70
  import * as Sdk from '@your-project/shared/sdk'
86
71
  import { DATABASE_MODELS } from '@your-project/shared/sdk'
72
+ import { formTheme } from '@your-project/shared/styles'
87
73
  import { AdminDataProvider, AdminDataLayout } from '@nestledjs/data-browser'
88
74
 
89
75
  export default function DataLayoutRoute() {
@@ -91,6 +77,7 @@ export default function DataLayoutRoute() {
91
77
  <AdminDataProvider
92
78
  sdk={Sdk}
93
79
  databaseModels={DATABASE_MODELS}
80
+ formTheme={formTheme}
94
81
  basePath="/admin/data"
95
82
  >
96
83
  <AdminDataLayout />
@@ -99,17 +86,19 @@ export default function DataLayoutRoute() {
99
86
  }
100
87
  ```
101
88
 
102
- ### Step 4: Create Page Routes
89
+ ### Step 3: Create Page Routes
103
90
 
104
91
  Create these minimal route files:
105
92
 
106
93
  **`apps/web/app/routes/admin/data/index.tsx`**:
94
+
107
95
  ```typescript
108
96
  import { AdminDataIndexPage } from '@nestledjs/data-browser'
109
97
  export default AdminDataIndexPage
110
98
  ```
111
99
 
112
100
  **`apps/web/app/routes/admin/data/$dataTypePlural.tsx`**:
101
+
113
102
  ```typescript
114
103
  import { AdminDataListPage, AdminDataErrorBoundary } from '@nestledjs/data-browser'
115
104
 
@@ -123,6 +112,7 @@ export function ErrorBoundary({ error }: Readonly<{ error: Error }>) {
123
112
  ```
124
113
 
125
114
  **`apps/web/app/routes/admin/data/$dataType.create.tsx`**:
115
+
126
116
  ```typescript
127
117
  import { AdminDataCreatePage, AdminDataCreateErrorBoundary } from '@nestledjs/data-browser'
128
118
 
@@ -136,6 +126,7 @@ export function ErrorBoundary({ error }: Readonly<{ error: Error }>) {
136
126
  ```
137
127
 
138
128
  **`apps/web/app/routes/admin/data/$dataType.$id.tsx`**:
129
+
139
130
  ```typescript
140
131
  import { AdminDataEditPage, AdminDataEditErrorBoundary } from '@nestledjs/data-browser'
141
132
 
@@ -148,7 +139,7 @@ export function ErrorBoundary({ error }: Readonly<{ error: Error }>) {
148
139
  }
149
140
  ```
150
141
 
151
- ### Step 5: Register Routes
142
+ ### Step 4: Register Routes
152
143
 
153
144
  In `apps/web/app/routes.tsx`:
154
145
 
@@ -167,9 +158,20 @@ export default [
167
158
  ] satisfies RouteConfig
168
159
  ```
169
160
 
170
- ### Step 6: Access the Data Browser
161
+ ### Step 5: Access the Data Browser
162
+
163
+ Navigate to `/admin/data` in your application.
164
+
165
+ ## Security Model
171
166
 
172
- Navigate to `/admin/data` in your application!
167
+ The data browser assumes the backing GraphQL admin CRUD operations are protected
168
+ by the API. In a standard Nestled project, generated CRUD is admin-only by
169
+ default. User-facing workflows should use separate custom resolvers instead of
170
+ relaxing generated admin CRUD.
171
+
172
+ Do not expose security-sensitive internal models, such as password hash history
173
+ or token material, through generic admin browsing unless you have a deliberate
174
+ operational need and appropriate masking.
173
175
 
174
176
  ## Usage
175
177
 
@@ -188,6 +190,7 @@ Shows all available database models with a searchable list.
188
190
  ### Create (`/admin/data/user/create`)
189
191
 
190
192
  Auto-generated form based on your Prisma model schema with:
193
+
191
194
  - Type-aware inputs (text, number, date, enum, relation dropdowns)
192
195
  - Validation based on model requirements
193
196
  - Real-time error handling
@@ -204,9 +207,10 @@ The context provider that makes SDK and models available to all components.
204
207
 
205
208
  ```typescript
206
209
  <AdminDataProvider
207
- sdk={Sdk} // Your GraphQL SDK namespace
208
- databaseModels={DATABASE_MODELS} // Array of model metadata
209
- basePath="/admin/data" // Optional: Custom route prefix
210
+ sdk={Sdk}
211
+ databaseModels={DATABASE_MODELS}
212
+ formTheme={formTheme}
213
+ basePath="/admin/data"
210
214
  >
211
215
  {children}
212
216
  </AdminDataProvider>
@@ -270,6 +274,7 @@ The data browser uses Tailwind CSS classes. Customize via your project's Tailwin
270
274
  ### "Missing GraphQL documents for model X"
271
275
 
272
276
  **Solution**: Run GraphQL code generation:
277
+
273
278
  ```bash
274
279
  pnpm sdk
275
280
  ```
@@ -278,15 +283,14 @@ pnpm sdk
278
283
 
279
284
  **Solution**: Ensure all data browser components are children of `<AdminDataProvider>` in `_layout.tsx`.
280
285
 
281
- ### Import errors for WebUiDataTable or formTheme
286
+ ### Import errors for formTheme
282
287
 
283
- **Solution**: Ensure your project exports these from:
284
- - `@your-project/web-ui`
285
- - `@your-project/shared/styles`
288
+ **Solution**: Ensure your project exports a form theme from `@your-project/shared/styles`.
286
289
 
287
290
  ### DATABASE_MODELS is undefined
288
291
 
289
292
  **Solution**: Run the model generator:
293
+
290
294
  ```bash
291
295
  pnpm generate:models
292
296
  ```
@@ -298,15 +302,15 @@ This creates the `DATABASE_MODELS` export from your Prisma schema.
298
302
  ### Build
299
303
 
300
304
  ```bash
301
- nx build admin-data
305
+ pnpm nx build data-browser
302
306
  ```
303
307
 
304
- Output: `dist/libs/admin-data/`
308
+ Output: `dist/libs/data-browser/`
305
309
 
306
310
  ### Publish
307
311
 
308
312
  ```bash
309
- nx publish admin-data
313
+ pnpm nx publish data-browser
310
314
  ```
311
315
 
312
316
  ## License
package/index.js CHANGED
@@ -19,7 +19,7 @@ import { AdminEmptyState, AdminErrorState, AdminLoadingState } from "./lib/compo
19
19
  import { AdminStatusDisplay, AdminUserStatus } from "./lib/components/shared/AdminStatusDisplay.js";
20
20
  import { RelationFieldWrapper } from "./lib/components/RelationFieldWrapper.js";
21
21
  import { ExportButton } from "./lib/components/ExportButton.js";
22
- import { buildFormFields, cleanFormInput, getAdminDocuments, getMutationName } from "./lib/utils/graphql-utils.js";
22
+ import { buildFormFields, cleanFormInput, getAdminDocuments, getMutationName, sanitizeInput, toKebabCase, toReadableText } from "./lib/utils/graphql-utils.js";
23
23
  import { AdminLocalStorage, SecureAdminLocalStorage } from "./lib/utils/secure-storage.js";
24
24
  import { formatFieldName, getItemDisplayName, getSmartSearchFields, kebabCase, normalizeModelNameForDocument, spacedWords } from "./lib/utils/string-utils.js";
25
25
  import { getPluralName } from "./lib/utils/get-plural-names.js";
@@ -63,7 +63,10 @@ export {
63
63
  initialState,
64
64
  kebabCase,
65
65
  normalizeModelNameForDocument,
66
+ sanitizeInput,
66
67
  spacedWords,
68
+ toKebabCase,
69
+ toReadableText,
67
70
  useAdminDataContext,
68
71
  useAdminList,
69
72
  useClickOutside,
@@ -1,14 +1,14 @@
1
1
  interface ExportButtonProps {
2
- query: any;
3
- dataPath: string;
2
+ readonly query: any;
3
+ readonly dataPath: string;
4
4
  /** Current query variables (includes filters, search, sort) */
5
- variables: {
5
+ readonly variables: {
6
6
  input: Record<string, unknown>;
7
7
  };
8
- visibleColumns: string[];
9
- fieldNames: string[];
10
- modelName: string;
11
- hasActiveFilters: boolean;
8
+ readonly visibleColumns: string[];
9
+ readonly fieldNames: string[];
10
+ readonly modelName: string;
11
+ readonly hasActiveFilters: boolean;
12
12
  }
13
13
  export declare function ExportButton({ query, dataPath, variables, visibleColumns, fieldNames, modelName, hasActiveFilters, }: ExportButtonProps): import("react/jsx-runtime").JSX.Element;
14
14
  export {};
@@ -3,27 +3,55 @@ import { useState, useRef, useCallback } from "react";
3
3
  import { useLazyQuery } from "@apollo/client/react";
4
4
  import { formatFieldName } from "../utils/string-utils.js";
5
5
  const MAX_EXPORT_ROWS = 5e4;
6
+ function stringifyCsvScalar(value) {
7
+ if (value === null || value === void 0) return "";
8
+ switch (typeof value) {
9
+ case "string":
10
+ return value;
11
+ case "number":
12
+ case "boolean":
13
+ case "bigint":
14
+ return `${value}`;
15
+ case "symbol":
16
+ return value.description ?? "";
17
+ case "function":
18
+ return value.name || "[function]";
19
+ case "object":
20
+ return JSON.stringify(value);
21
+ default:
22
+ return "";
23
+ }
24
+ }
6
25
  function escapeCsvValue(val) {
7
26
  if (val === null || val === void 0) return "";
8
27
  let str;
9
28
  if (typeof val === "object") {
10
29
  const obj = val;
11
- str = obj.id ? String(obj.id) : JSON.stringify(val);
30
+ const id = obj.id;
31
+ str = typeof id === "string" || typeof id === "number" ? `${id}` : JSON.stringify(val);
12
32
  } else {
13
- str = String(val);
33
+ str = stringifyCsvScalar(val);
14
34
  }
15
35
  if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
16
- return `"${str.replace(/"/g, '""')}"`;
36
+ return `"${str.replaceAll('"', '""')}"`;
17
37
  }
18
38
  return str;
19
39
  }
20
40
  function generateCsv(items, columns) {
21
41
  const header = columns.map((c) => escapeCsvValue(formatFieldName(c))).join(",");
22
- const rows = items.map(
23
- (item) => columns.map((col) => escapeCsvValue(item[col])).join(",")
24
- );
42
+ const rows = items.map((item) => columns.map((col) => escapeCsvValue(item[col])).join(","));
25
43
  return [header, ...rows].join("\r\n");
26
44
  }
45
+ function findItemsInData(data, dataPath) {
46
+ const direct = data?.[dataPath];
47
+ if (direct?.length) return direct;
48
+ for (const value of Object.values(data ?? {})) {
49
+ if (Array.isArray(value) && value.length > 0 && value[0]?.id) {
50
+ return value;
51
+ }
52
+ }
53
+ return [];
54
+ }
27
55
  function downloadCsv(csv, filename) {
28
56
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
29
57
  const url = URL.createObjectURL(blob);
@@ -32,9 +60,19 @@ function downloadCsv(csv, filename) {
32
60
  link.download = filename;
33
61
  document.body.appendChild(link);
34
62
  link.click();
35
- document.body.removeChild(link);
63
+ link.remove();
36
64
  URL.revokeObjectURL(url);
37
65
  }
66
+ function buildExportInput(mode, variables) {
67
+ if (mode === "all")
68
+ return { take: MAX_EXPORT_ROWS, skip: 0, orderBy: "id", orderDirection: "desc" };
69
+ return { ...variables.input, take: MAX_EXPORT_ROWS, skip: 0 };
70
+ }
71
+ function buildExportFilename(modelName, mode, hasActiveFilters) {
72
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
73
+ const suffix = mode === "filtered" && hasActiveFilters ? "-filtered" : "";
74
+ return `${modelName}${suffix}-${timestamp}.csv`;
75
+ }
38
76
  function ExportButton({
39
77
  query,
40
78
  dataPath,
@@ -55,25 +93,20 @@ function ExportButton({
55
93
  async (mode) => {
56
94
  setExporting(mode);
57
95
  setError(null);
58
- const columns = mode === "all" ? fieldNames : visibleColumns.length > 0 ? visibleColumns : fieldNames;
59
- const input = mode === "all" ? { take: MAX_EXPORT_ROWS, skip: 0, orderBy: "id", orderDirection: "desc" } : { ...variables.input, take: MAX_EXPORT_ROWS, skip: 0 };
96
+ let columns;
97
+ if (mode === "all") {
98
+ columns = fieldNames;
99
+ } else {
100
+ columns = visibleColumns.length > 0 ? visibleColumns : fieldNames;
101
+ }
102
+ const input = buildExportInput(mode, variables);
60
103
  try {
61
104
  const { data, error: queryError } = await runQuery({ variables: { input } });
62
105
  if (queryError) throw queryError;
63
106
  const anyData = data;
64
- let items = anyData?.[dataPath] ?? [];
65
- if (!items.length) {
66
- for (const value of Object.values(anyData ?? {})) {
67
- if (Array.isArray(value) && value.length > 0 && value[0]?.id) {
68
- items = value;
69
- break;
70
- }
71
- }
72
- }
107
+ const items = findItemsInData(anyData, dataPath);
73
108
  const csv = generateCsv(items, columns);
74
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
75
- const suffix = mode === "filtered" && hasActiveFilters ? "-filtered" : "";
76
- downloadCsv(csv, `${modelName}${suffix}-${timestamp}.csv`);
109
+ downloadCsv(csv, buildExportFilename(modelName, mode, hasActiveFilters));
77
110
  setOpen(false);
78
111
  } catch (err) {
79
112
  setError(err instanceof Error ? err.message : "Export failed");
@@ -83,29 +116,42 @@ function ExportButton({
83
116
  },
84
117
  [runQuery, variables, fieldNames, visibleColumns, dataPath, modelName, hasActiveFilters]
85
118
  );
86
- const activeFilterCount = Object.keys(variables.input.filters ?? {}).length;
119
+ const activeFilterCount = Object.keys(
120
+ variables.input.filters ?? {}
121
+ ).length;
87
122
  const hasSearch = Boolean(variables.input.search);
88
123
  return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
89
124
  /* @__PURE__ */ jsxs(
90
125
  "button",
91
126
  {
127
+ type: "button",
92
128
  onClick: () => {
93
129
  setOpen(!open);
94
130
  setError(null);
95
131
  },
96
132
  className: "inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-web",
97
133
  children: [
98
- /* @__PURE__ */ jsx("svg", { className: "w-4 h-4 mr-2", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" }) }),
134
+ /* @__PURE__ */ jsx("svg", { className: "w-4 h-4 mr-2", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx(
135
+ "path",
136
+ {
137
+ strokeLinecap: "round",
138
+ strokeLinejoin: "round",
139
+ strokeWidth: 2,
140
+ d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
141
+ }
142
+ ) }),
99
143
  "Export"
100
144
  ]
101
145
  }
102
146
  ),
103
147
  open && /* @__PURE__ */ jsxs(Fragment, { children: [
104
148
  /* @__PURE__ */ jsx(
105
- "div",
149
+ "button",
106
150
  {
107
- className: "fixed inset-0 z-40",
108
- onClick: () => setOpen(false)
151
+ type: "button",
152
+ className: "fixed inset-0 z-40 cursor-default bg-transparent border-0 p-0",
153
+ onClick: () => setOpen(false),
154
+ "aria-label": "Close export menu"
109
155
  }
110
156
  ),
111
157
  /* @__PURE__ */ jsx(
@@ -127,10 +173,35 @@ function ExportButton({
127
173
  onClick: () => doExport("all"),
128
174
  className: "w-full inline-flex justify-center items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 text-xs font-medium rounded text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed",
129
175
  children: exporting === "all" ? /* @__PURE__ */ jsxs(Fragment, { children: [
130
- /* @__PURE__ */ jsxs("svg", { className: "animate-spin -ml-1 mr-2 h-3 w-3 text-gray-500", fill: "none", viewBox: "0 0 24 24", children: [
131
- /* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
132
- /* @__PURE__ */ jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })
133
- ] }),
176
+ /* @__PURE__ */ jsxs(
177
+ "svg",
178
+ {
179
+ className: "animate-spin -ml-1 mr-2 h-3 w-3 text-gray-500",
180
+ fill: "none",
181
+ viewBox: "0 0 24 24",
182
+ children: [
183
+ /* @__PURE__ */ jsx(
184
+ "circle",
185
+ {
186
+ className: "opacity-25",
187
+ cx: "12",
188
+ cy: "12",
189
+ r: "10",
190
+ stroke: "currentColor",
191
+ strokeWidth: "4"
192
+ }
193
+ ),
194
+ /* @__PURE__ */ jsx(
195
+ "path",
196
+ {
197
+ className: "opacity-75",
198
+ fill: "currentColor",
199
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
200
+ }
201
+ )
202
+ ]
203
+ }
204
+ ),
134
205
  "Exporting..."
135
206
  ] }) : `Download (${fieldNames.length} columns)`
136
207
  }
@@ -142,12 +213,13 @@ function ExportButton({
142
213
  /* @__PURE__ */ jsxs("ul", { className: "text-xs text-gray-500 dark:text-gray-400 mb-2 space-y-0.5", children: [
143
214
  /* @__PURE__ */ jsxs("li", { children: [
144
215
  visibleColumns.length > 0 ? visibleColumns.length : fieldNames.length,
145
- " columns selected"
216
+ " ",
217
+ "columns selected"
146
218
  ] }),
147
219
  activeFilterCount > 0 && /* @__PURE__ */ jsxs("li", { children: [
148
220
  activeFilterCount,
149
221
  " filter",
150
- activeFilterCount !== 1 ? "s" : "",
222
+ activeFilterCount === 1 ? "" : "s",
151
223
  " active"
152
224
  ] }),
153
225
  hasSearch && /* @__PURE__ */ jsxs("li", { children: [
@@ -163,10 +235,35 @@ function ExportButton({
163
235
  onClick: () => doExport("filtered"),
164
236
  className: "w-full inline-flex justify-center items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed",
165
237
  children: exporting === "filtered" ? /* @__PURE__ */ jsxs(Fragment, { children: [
166
- /* @__PURE__ */ jsxs("svg", { className: "animate-spin -ml-1 mr-2 h-3 w-3 text-white", fill: "none", viewBox: "0 0 24 24", children: [
167
- /* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
168
- /* @__PURE__ */ jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })
169
- ] }),
238
+ /* @__PURE__ */ jsxs(
239
+ "svg",
240
+ {
241
+ className: "animate-spin -ml-1 mr-2 h-3 w-3 text-white",
242
+ fill: "none",
243
+ viewBox: "0 0 24 24",
244
+ children: [
245
+ /* @__PURE__ */ jsx(
246
+ "circle",
247
+ {
248
+ className: "opacity-25",
249
+ cx: "12",
250
+ cy: "12",
251
+ r: "10",
252
+ stroke: "currentColor",
253
+ strokeWidth: "4"
254
+ }
255
+ ),
256
+ /* @__PURE__ */ jsx(
257
+ "path",
258
+ {
259
+ className: "opacity-75",
260
+ fill: "currentColor",
261
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
262
+ }
263
+ )
264
+ ]
265
+ }
266
+ ),
170
267
  "Exporting..."
171
268
  ] }) : "Download"
172
269
  }
@@ -6,5 +6,5 @@ interface RelationFieldWrapperProps {
6
6
  readonly fieldName?: string;
7
7
  readonly basePath?: string;
8
8
  }
9
- export declare function RelationFieldWrapper({ children, relationType, initialValue, fieldName, basePath }: RelationFieldWrapperProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function RelationFieldWrapper({ children, relationType, initialValue, fieldName, basePath, }: RelationFieldWrapperProps): import("react/jsx-runtime").JSX.Element;
10
10
  export {};
@@ -2,7 +2,7 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { Link } from "react-router";
3
3
  import { useState, useEffect } from "react";
4
4
  const toKebabCase = (str) => {
5
- return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
5
+ return str.replaceAll(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
6
6
  };
7
7
  function RelationFieldWrapper({
8
8
  children,
@@ -3,5 +3,5 @@ interface DateRangeFilterProps {
3
3
  currentValue: any;
4
4
  onChange: (value: any) => void;
5
5
  }
6
- export declare function DateRangeFilter({ fieldName, currentValue, onChange }: Readonly<DateRangeFilterProps>): import("react/jsx-runtime").JSX.Element;
6
+ export declare function DateRangeFilter({ fieldName, currentValue, onChange, }: Readonly<DateRangeFilterProps>): import("react/jsx-runtime").JSX.Element;
7
7
  export {};
@@ -4,5 +4,5 @@ interface EnumFilterProps {
4
4
  onChange: (value: string | undefined) => void;
5
5
  enumValues: string[];
6
6
  }
7
- export declare function EnumFilter({ fieldName, currentValue, onChange, enumValues }: Readonly<EnumFilterProps>): import("react/jsx-runtime").JSX.Element;
7
+ export declare function EnumFilter({ fieldName, currentValue, onChange, enumValues, }: Readonly<EnumFilterProps>): import("react/jsx-runtime").JSX.Element;
8
8
  export {};
@@ -4,5 +4,5 @@ interface NumberRangeFilterProps {
4
4
  currentValue: any;
5
5
  onChange: (value: any) => void;
6
6
  }
7
- export declare function NumberRangeFilter({ fieldName, fieldType, currentValue, onChange }: Readonly<NumberRangeFilterProps>): import("react/jsx-runtime").JSX.Element;
7
+ export declare function NumberRangeFilter({ fieldName, fieldType, currentValue, onChange, }: Readonly<NumberRangeFilterProps>): import("react/jsx-runtime").JSX.Element;
8
8
  export {};
@@ -1,26 +1,26 @@
1
- export declare function RelationDropdownButton({ currentItem, relatedModelName, isOpen, onClick }: Readonly<{
1
+ export declare function RelationDropdownButton({ currentItem, relatedModelName, isOpen, onClick, }: Readonly<{
2
2
  currentItem: any;
3
3
  relatedModelName: string;
4
4
  isOpen: boolean;
5
5
  onClick: () => void;
6
6
  }>): import("react/jsx-runtime").JSX.Element;
7
- export declare function RelationSearchInput({ searchTerm, onSearchChange, relatedModelName }: Readonly<{
7
+ export declare function RelationSearchInput({ searchTerm, onSearchChange, relatedModelName, }: Readonly<{
8
8
  searchTerm: string;
9
9
  onSearchChange: (value: string) => void;
10
10
  relatedModelName: string;
11
11
  }>): import("react/jsx-runtime").JSX.Element;
12
- export declare function RelationItem({ item, onSelect }: Readonly<{
12
+ export declare function RelationItem({ item, onSelect, }: Readonly<{
13
13
  item: any;
14
14
  onSelect: (item: any) => void;
15
15
  }>): import("react/jsx-runtime").JSX.Element;
16
- export declare function RelationItemList({ items, loading, error, onSelect, onClear }: Readonly<{
16
+ export declare function RelationItemList({ items, loading, error, onSelect, onClear, }: Readonly<{
17
17
  items: any[];
18
18
  loading: boolean;
19
19
  error?: any;
20
20
  onSelect: (item: any) => void;
21
21
  onClear: () => void;
22
22
  }>): import("react/jsx-runtime").JSX.Element;
23
- export declare function RelationDropdownContent({ searchTerm, onSearchChange, relatedModelName, items, loading, error, onSelect, onClear }: Readonly<{
23
+ export declare function RelationDropdownContent({ searchTerm, onSearchChange, relatedModelName, items, loading, error, onSelect, onClear, }: Readonly<{
24
24
  searchTerm: string;
25
25
  onSearchChange: (value: string) => void;
26
26
  relatedModelName: string;
@@ -78,27 +78,54 @@ function RelationItemList({
78
78
  ),
79
79
  error && /* @__PURE__ */ jsxs("div", { className: "px-3 py-2 text-sm text-red-600", children: [
80
80
  /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
81
- /* @__PURE__ */ jsx("svg", { className: "w-4 h-4 mr-2", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }),
81
+ /* @__PURE__ */ jsx("svg", { className: "w-4 h-4 mr-2", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx(
82
+ "path",
83
+ {
84
+ strokeLinecap: "round",
85
+ strokeLinejoin: "round",
86
+ strokeWidth: 2,
87
+ d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
88
+ }
89
+ ) }),
82
90
  "Failed to load options"
83
91
  ] }),
84
92
  /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500 mt-1", children: error.networkError ? "Network error" : "Please try again" })
85
93
  ] }),
86
94
  !error && loading && /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-sm text-gray-500", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
87
- /* @__PURE__ */ jsxs("svg", { className: "animate-spin -ml-1 mr-2 h-4 w-4 text-gray-500", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [
88
- /* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
89
- /* @__PURE__ */ jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })
90
- ] }),
95
+ /* @__PURE__ */ jsxs(
96
+ "svg",
97
+ {
98
+ className: "animate-spin -ml-1 mr-2 h-4 w-4 text-gray-500",
99
+ xmlns: "http://www.w3.org/2000/svg",
100
+ fill: "none",
101
+ viewBox: "0 0 24 24",
102
+ children: [
103
+ /* @__PURE__ */ jsx(
104
+ "circle",
105
+ {
106
+ className: "opacity-25",
107
+ cx: "12",
108
+ cy: "12",
109
+ r: "10",
110
+ stroke: "currentColor",
111
+ strokeWidth: "4"
112
+ }
113
+ ),
114
+ /* @__PURE__ */ jsx(
115
+ "path",
116
+ {
117
+ className: "opacity-75",
118
+ fill: "currentColor",
119
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
120
+ }
121
+ )
122
+ ]
123
+ }
124
+ ),
91
125
  "Loading..."
92
126
  ] }) }),
93
127
  !error && !loading && items.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-sm text-gray-500", children: "No items found" }),
94
- !error && !loading && items.length > 0 && items.map((item) => /* @__PURE__ */ jsx(
95
- RelationItem,
96
- {
97
- item,
98
- onSelect
99
- },
100
- item.id
101
- ))
128
+ !error && !loading && items.length > 0 && items.map((item) => /* @__PURE__ */ jsx(RelationItem, { item, onSelect }, item.id))
102
129
  ] });
103
130
  }
104
131
  function RelationDropdownContent({