@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 +43 -39
- package/index.js +4 -1
- package/lib/components/ExportButton.d.ts +7 -7
- package/lib/components/ExportButton.js +133 -36
- package/lib/components/RelationFieldWrapper.d.ts +1 -1
- package/lib/components/RelationFieldWrapper.js +1 -1
- package/lib/components/filters/DateRangeFilter.d.ts +1 -1
- package/lib/components/filters/EnumFilter.d.ts +1 -1
- package/lib/components/filters/NumberRangeFilter.d.ts +1 -1
- 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 +14 -6
- 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/useClickOutside.js +1 -1
- package/lib/hooks/useRelationData.js +1 -0
- package/lib/layouts/AdminDataLayout.js +84 -9
- package/lib/pages/AdminDataCreatePage.d.ts +1 -1
- package/lib/pages/AdminDataCreatePage.js +66 -46
- package/lib/pages/AdminDataEditPage.js +53 -19
- package/lib/pages/AdminDataListPage.js +87 -79
- package/lib/utils/graphql-utils.d.ts +7 -1
- package/lib/utils/graphql-utils.js +337 -320
- package/lib/utils/secure-storage.js +23 -14
- package/lib/utils/string-utils.js +31 -8
- package/package.json +3 -3
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
|
@@ -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
|
-
|
|
30
|
+
const id = obj.id;
|
|
31
|
+
str = typeof id === "string" || typeof id === "number" ? `${id}` : JSON.stringify(val);
|
|
12
32
|
} else {
|
|
13
|
-
str =
|
|
33
|
+
str = stringifyCsvScalar(val);
|
|
14
34
|
}
|
|
15
35
|
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
16
|
-
return `"${str.
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
"
|
|
149
|
+
"button",
|
|
106
150
|
{
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
"
|
|
216
|
+
" ",
|
|
217
|
+
"columns selected"
|
|
146
218
|
] }),
|
|
147
219
|
activeFilterCount > 0 && /* @__PURE__ */ jsxs("li", { children: [
|
|
148
220
|
activeFilterCount,
|
|
149
221
|
" filter",
|
|
150
|
-
activeFilterCount
|
|
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(
|
|
167
|
-
|
|
168
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
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({
|