@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.
- package/README.md +43 -39
- package/index.js +6 -1
- package/lib/components/ExportButton.d.ts +14 -0
- package/lib/components/ExportButton.js +281 -0
- package/lib/components/FilterField.js +1 -2
- package/lib/components/RelationFieldWrapper.d.ts +1 -1
- package/lib/components/RelationFieldWrapper.js +2 -2
- package/lib/components/filters/DateRangeFilter.d.ts +1 -1
- package/lib/components/filters/DateRangeFilter.js +2 -2
- package/lib/components/filters/EnumFilter.d.ts +1 -1
- package/lib/components/filters/NumberRangeFilter.d.ts +1 -1
- package/lib/components/filters/NumberRangeFilter.js +2 -2
- package/lib/components/filters/RelationComponents.d.ts +5 -5
- package/lib/components/filters/RelationComponents.js +40 -13
- package/lib/components/filters/RelationFilterField.d.ts +1 -1
- package/lib/components/filters/RelationFilterField.js +18 -10
- package/lib/components/index.d.ts +1 -0
- package/lib/components/shared/AdminBreadcrumbs.js +2 -17
- package/lib/components/shared/AdminErrorStates.js +6 -1
- package/lib/components/shared/AdminStatusDisplay.js +7 -11
- package/lib/context/AdminDataContext.d.ts +2 -2
- package/lib/hooks/useAdminList.js +1 -1
- package/lib/hooks/useClickOutside.js +1 -1
- package/lib/hooks/useRelationData.js +2 -1
- package/lib/layouts/AdminDataLayout.js +87 -14
- package/lib/pages/AdminDataCreatePage.d.ts +1 -1
- package/lib/pages/AdminDataCreatePage.js +68 -45
- package/lib/pages/AdminDataEditPage.js +71 -38
- package/lib/pages/AdminDataListPage.js +116 -85
- package/lib/utils/graphql-utils.d.ts +7 -1
- package/lib/utils/graphql-utils.js +343 -331
- package/lib/utils/secure-storage.js +26 -20
- package/lib/utils/string-utils.js +31 -8
- 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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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. **
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
161
|
+
### Step 5: Access the Data Browser
|
|
162
|
+
|
|
163
|
+
Navigate to `/admin/data` in your application.
|
|
164
|
+
|
|
165
|
+
## Security Model
|
|
171
166
|
|
|
172
|
-
|
|
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}
|
|
208
|
-
databaseModels={DATABASE_MODELS}
|
|
209
|
-
|
|
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
|
|
286
|
+
### Import errors for formTheme
|
|
282
287
|
|
|
283
|
-
**Solution**: Ensure your project exports
|
|
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
|
|
305
|
+
pnpm nx build data-browser
|
|
302
306
|
```
|
|
303
307
|
|
|
304
|
-
Output: `dist/libs/
|
|
308
|
+
Output: `dist/libs/data-browser/`
|
|
305
309
|
|
|
306
310
|
### Publish
|
|
307
311
|
|
|
308
312
|
```bash
|
|
309
|
-
nx publish
|
|
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 {
|
|
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
|
|
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.
|
|
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 (
|
|
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 =
|
|
9
|
-
const toDate =
|
|
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 =
|
|
10
|
-
const maxValue =
|
|
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;
|