@nestledjs/data-browser 1.0.14 → 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.
Files changed (34) hide show
  1. package/README.md +43 -39
  2. package/index.js +6 -1
  3. package/lib/components/ExportButton.d.ts +14 -0
  4. package/lib/components/ExportButton.js +281 -0
  5. package/lib/components/FilterField.js +1 -2
  6. package/lib/components/RelationFieldWrapper.d.ts +1 -1
  7. package/lib/components/RelationFieldWrapper.js +2 -2
  8. package/lib/components/filters/DateRangeFilter.d.ts +1 -1
  9. package/lib/components/filters/DateRangeFilter.js +2 -2
  10. package/lib/components/filters/EnumFilter.d.ts +1 -1
  11. package/lib/components/filters/NumberRangeFilter.d.ts +1 -1
  12. package/lib/components/filters/NumberRangeFilter.js +2 -2
  13. package/lib/components/filters/RelationComponents.d.ts +5 -5
  14. package/lib/components/filters/RelationComponents.js +40 -13
  15. package/lib/components/filters/RelationFilterField.d.ts +1 -1
  16. package/lib/components/filters/RelationFilterField.js +18 -10
  17. package/lib/components/index.d.ts +1 -0
  18. package/lib/components/shared/AdminBreadcrumbs.js +2 -17
  19. package/lib/components/shared/AdminErrorStates.js +6 -1
  20. package/lib/components/shared/AdminStatusDisplay.js +7 -11
  21. package/lib/context/AdminDataContext.d.ts +2 -2
  22. package/lib/hooks/useAdminList.js +1 -1
  23. package/lib/hooks/useClickOutside.js +1 -1
  24. package/lib/hooks/useRelationData.js +2 -1
  25. package/lib/layouts/AdminDataLayout.js +87 -14
  26. package/lib/pages/AdminDataCreatePage.d.ts +1 -1
  27. package/lib/pages/AdminDataCreatePage.js +68 -45
  28. package/lib/pages/AdminDataEditPage.js +71 -38
  29. package/lib/pages/AdminDataListPage.js +116 -85
  30. package/lib/utils/graphql-utils.d.ts +7 -1
  31. package/lib/utils/graphql-utils.js +343 -331
  32. package/lib/utils/secure-storage.js +26 -20
  33. package/lib/utils/string-utils.js +31 -8
  34. package/package.json +2 -2
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
@@ -18,7 +18,8 @@ import { AdminBreadcrumbs } from "./lib/components/shared/AdminBreadcrumbs.js";
18
18
  import { AdminEmptyState, AdminErrorState, AdminLoadingState } from "./lib/components/shared/AdminErrorStates.js";
19
19
  import { AdminStatusDisplay, AdminUserStatus } from "./lib/components/shared/AdminStatusDisplay.js";
20
20
  import { RelationFieldWrapper } from "./lib/components/RelationFieldWrapper.js";
21
- import { buildFormFields, cleanFormInput, getAdminDocuments, getMutationName } from "./lib/utils/graphql-utils.js";
21
+ import { ExportButton } from "./lib/components/ExportButton.js";
22
+ import { buildFormFields, cleanFormInput, getAdminDocuments, getMutationName, sanitizeInput, toKebabCase, toReadableText } from "./lib/utils/graphql-utils.js";
22
23
  import { AdminLocalStorage, SecureAdminLocalStorage } from "./lib/utils/secure-storage.js";
23
24
  import { formatFieldName, getItemDisplayName, getSmartSearchFields, kebabCase, normalizeModelNameForDocument, spacedWords } from "./lib/utils/string-utils.js";
24
25
  import { getPluralName } from "./lib/utils/get-plural-names.js";
@@ -41,6 +42,7 @@ export {
41
42
  AdminUserStatus,
42
43
  DateRangeFilter,
43
44
  EnumFilter,
45
+ ExportButton,
44
46
  NumberRangeFilter,
45
47
  RelationDropdownButton,
46
48
  RelationDropdownContent,
@@ -61,7 +63,10 @@ export {
61
63
  initialState,
62
64
  kebabCase,
63
65
  normalizeModelNameForDocument,
66
+ sanitizeInput,
64
67
  spacedWords,
68
+ toKebabCase,
69
+ toReadableText,
65
70
  useAdminDataContext,
66
71
  useAdminList,
67
72
  useClickOutside,
@@ -0,0 +1,14 @@
1
+ interface ExportButtonProps {
2
+ readonly query: any;
3
+ readonly dataPath: string;
4
+ /** Current query variables (includes filters, search, sort) */
5
+ readonly variables: {
6
+ input: Record<string, unknown>;
7
+ };
8
+ readonly visibleColumns: string[];
9
+ readonly fieldNames: string[];
10
+ readonly modelName: string;
11
+ readonly hasActiveFilters: boolean;
12
+ }
13
+ export declare function ExportButton({ query, dataPath, variables, visibleColumns, fieldNames, modelName, hasActiveFilters, }: ExportButtonProps): import("react/jsx-runtime").JSX.Element;
14
+ export {};
@@ -0,0 +1,281 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useRef, useCallback } from "react";
3
+ import { useLazyQuery } from "@apollo/client/react";
4
+ import { formatFieldName } from "../utils/string-utils.js";
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
+ }
25
+ function escapeCsvValue(val) {
26
+ if (val === null || val === void 0) return "";
27
+ let str;
28
+ if (typeof val === "object") {
29
+ const obj = val;
30
+ const id = obj.id;
31
+ str = typeof id === "string" || typeof id === "number" ? `${id}` : JSON.stringify(val);
32
+ } else {
33
+ str = stringifyCsvScalar(val);
34
+ }
35
+ if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
36
+ return `"${str.replaceAll('"', '""')}"`;
37
+ }
38
+ return str;
39
+ }
40
+ function generateCsv(items, columns) {
41
+ const header = columns.map((c) => escapeCsvValue(formatFieldName(c))).join(",");
42
+ const rows = items.map((item) => columns.map((col) => escapeCsvValue(item[col])).join(","));
43
+ return [header, ...rows].join("\r\n");
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
+ }
55
+ function downloadCsv(csv, filename) {
56
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
57
+ const url = URL.createObjectURL(blob);
58
+ const link = document.createElement("a");
59
+ link.href = url;
60
+ link.download = filename;
61
+ document.body.appendChild(link);
62
+ link.click();
63
+ link.remove();
64
+ URL.revokeObjectURL(url);
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
+ }
76
+ function ExportButton({
77
+ query,
78
+ dataPath,
79
+ variables,
80
+ visibleColumns,
81
+ fieldNames,
82
+ modelName,
83
+ hasActiveFilters
84
+ }) {
85
+ const [open, setOpen] = useState(false);
86
+ const [exporting, setExporting] = useState(null);
87
+ const [error, setError] = useState(null);
88
+ const modalRef = useRef(null);
89
+ const [runQuery] = useLazyQuery(query, {
90
+ fetchPolicy: "network-only"
91
+ });
92
+ const doExport = useCallback(
93
+ async (mode) => {
94
+ setExporting(mode);
95
+ setError(null);
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);
103
+ try {
104
+ const { data, error: queryError } = await runQuery({ variables: { input } });
105
+ if (queryError) throw queryError;
106
+ const anyData = data;
107
+ const items = findItemsInData(anyData, dataPath);
108
+ const csv = generateCsv(items, columns);
109
+ downloadCsv(csv, buildExportFilename(modelName, mode, hasActiveFilters));
110
+ setOpen(false);
111
+ } catch (err) {
112
+ setError(err instanceof Error ? err.message : "Export failed");
113
+ } finally {
114
+ setExporting(null);
115
+ }
116
+ },
117
+ [runQuery, variables, fieldNames, visibleColumns, dataPath, modelName, hasActiveFilters]
118
+ );
119
+ const activeFilterCount = Object.keys(
120
+ variables.input.filters ?? {}
121
+ ).length;
122
+ const hasSearch = Boolean(variables.input.search);
123
+ return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
124
+ /* @__PURE__ */ jsxs(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ onClick: () => {
129
+ setOpen(!open);
130
+ setError(null);
131
+ },
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",
133
+ children: [
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
+ ) }),
143
+ "Export"
144
+ ]
145
+ }
146
+ ),
147
+ open && /* @__PURE__ */ jsxs(Fragment, { children: [
148
+ /* @__PURE__ */ jsx(
149
+ "button",
150
+ {
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"
155
+ }
156
+ ),
157
+ /* @__PURE__ */ jsx(
158
+ "div",
159
+ {
160
+ ref: modalRef,
161
+ className: "absolute right-0 mt-2 w-80 bg-white dark:bg-gray-800 rounded-md shadow-lg z-50 border border-gray-200 dark:border-gray-700",
162
+ children: /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
163
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium text-gray-900 dark:text-gray-100 mb-3", children: "Export as CSV" }),
164
+ error && /* @__PURE__ */ jsx("div", { className: "mb-3 text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 rounded p-2", children: error }),
165
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
166
+ /* @__PURE__ */ jsxs("div", { className: "border border-gray-200 dark:border-gray-700 rounded-md p-3", children: [
167
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-gray-100 mb-1", children: "Export All" }),
168
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mb-2", children: "All records and all columns. No filters applied." }),
169
+ /* @__PURE__ */ jsx(
170
+ "button",
171
+ {
172
+ disabled: exporting !== null,
173
+ onClick: () => doExport("all"),
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",
175
+ children: exporting === "all" ? /* @__PURE__ */ jsxs(Fragment, { children: [
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
+ ),
205
+ "Exporting..."
206
+ ] }) : `Download (${fieldNames.length} columns)`
207
+ }
208
+ )
209
+ ] }),
210
+ /* @__PURE__ */ jsxs("div", { className: "border border-green-200 dark:border-green-800 rounded-md p-3 bg-green-50 dark:bg-green-900/20", children: [
211
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-gray-100 mb-1", children: "Export with Current Settings" }),
212
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mb-1", children: "Uses your current filters and visible columns." }),
213
+ /* @__PURE__ */ jsxs("ul", { className: "text-xs text-gray-500 dark:text-gray-400 mb-2 space-y-0.5", children: [
214
+ /* @__PURE__ */ jsxs("li", { children: [
215
+ visibleColumns.length > 0 ? visibleColumns.length : fieldNames.length,
216
+ " ",
217
+ "columns selected"
218
+ ] }),
219
+ activeFilterCount > 0 && /* @__PURE__ */ jsxs("li", { children: [
220
+ activeFilterCount,
221
+ " filter",
222
+ activeFilterCount === 1 ? "" : "s",
223
+ " active"
224
+ ] }),
225
+ hasSearch && /* @__PURE__ */ jsxs("li", { children: [
226
+ 'Search: "',
227
+ variables.input.search,
228
+ '"'
229
+ ] })
230
+ ] }),
231
+ /* @__PURE__ */ jsx(
232
+ "button",
233
+ {
234
+ disabled: exporting !== null,
235
+ onClick: () => doExport("filtered"),
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",
237
+ children: exporting === "filtered" ? /* @__PURE__ */ jsxs(Fragment, { children: [
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
+ ),
267
+ "Exporting..."
268
+ ] }) : "Download"
269
+ }
270
+ )
271
+ ] })
272
+ ] })
273
+ ] })
274
+ }
275
+ )
276
+ ] })
277
+ ] });
278
+ }
279
+ export {
280
+ ExportButton
281
+ };
@@ -24,9 +24,8 @@ function getRegularFieldValue(fieldName, filters) {
24
24
  return filters[fieldName];
25
25
  }
26
26
  function getRelatedEnumFieldValue(fieldName, filters) {
27
- var _a;
28
27
  const [relationName, enumFieldName] = fieldName.split(".");
29
- return (_a = filters[relationName]) == null ? void 0 : _a[enumFieldName];
28
+ return filters[relationName]?.[enumFieldName];
30
29
  }
31
30
  function BooleanFilter({ fieldName, currentValue, onChange }) {
32
31
  return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
@@ -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,
@@ -18,7 +18,7 @@ function RelationFieldWrapper({
18
18
  }
19
19
  if (fieldName) {
20
20
  const selectElement = document.querySelector(`[name="${fieldName}"]`);
21
- if ((selectElement == null ? void 0 : selectElement.value) && selectElement.value !== currentValue) {
21
+ if (selectElement?.value && selectElement.value !== currentValue) {
22
22
  setCurrentValue(selectElement.value);
23
23
  }
24
24
  if (selectElement) {
@@ -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 {};
@@ -5,8 +5,8 @@ function DateRangeFilter({
5
5
  currentValue,
6
6
  onChange
7
7
  }) {
8
- const fromDate = (currentValue == null ? void 0 : currentValue.gte) ? new Date(currentValue.gte).toISOString().split("T")[0] : "";
9
- const toDate = (currentValue == null ? void 0 : currentValue.lte) ? new Date(currentValue.lte).toISOString().split("T")[0] : "";
8
+ const fromDate = currentValue?.gte ? new Date(currentValue.gte).toISOString().split("T")[0] : "";
9
+ const toDate = currentValue?.lte ? new Date(currentValue.lte).toISOString().split("T")[0] : "";
10
10
  const handleFromChange = (date) => {
11
11
  const newValue = { ...currentValue };
12
12
  if (date) {
@@ -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 {};
@@ -6,8 +6,8 @@ function NumberRangeFilter({
6
6
  currentValue,
7
7
  onChange
8
8
  }) {
9
- const minValue = (currentValue == null ? void 0 : currentValue.gte) === void 0 ? "" : currentValue.gte.toString();
10
- const maxValue = (currentValue == null ? void 0 : currentValue.lte) === void 0 ? "" : currentValue.lte.toString();
9
+ const minValue = currentValue?.gte === void 0 ? "" : currentValue.gte.toString();
10
+ const maxValue = currentValue?.lte === void 0 ? "" : currentValue.lte.toString();
11
11
  const parseNumber = (value) => {
12
12
  if (!value) return void 0;
13
13
  if (fieldType === "int" || fieldType === "bigint") {
@@ -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;