@nestledjs/data-browser 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +2 -0
- package/lib/components/ExportButton.d.ts +14 -0
- package/lib/components/ExportButton.js +184 -0
- package/lib/components/FilterField.js +1 -2
- package/lib/components/RelationFieldWrapper.js +1 -1
- package/lib/components/filters/DateRangeFilter.js +2 -2
- package/lib/components/filters/NumberRangeFilter.js +2 -2
- package/lib/components/filters/RelationFilterField.js +4 -4
- package/lib/components/index.d.ts +1 -0
- package/lib/hooks/useAdminList.js +1 -1
- package/lib/hooks/useRelationData.js +1 -1
- package/lib/layouts/AdminDataLayout.js +3 -5
- package/lib/pages/AdminDataCreatePage.js +9 -6
- package/lib/pages/AdminDataEditPage.js +21 -42
- package/lib/pages/AdminDataListPage.js +33 -10
- package/lib/utils/graphql-utils.js +13 -35
- package/lib/utils/secure-storage.js +3 -6
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ 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 { ExportButton } from "./lib/components/ExportButton.js";
|
|
21
22
|
import { buildFormFields, cleanFormInput, getAdminDocuments, getMutationName } 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";
|
|
@@ -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,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface ExportButtonProps {
|
|
2
|
+
query: any;
|
|
3
|
+
dataPath: string;
|
|
4
|
+
/** Current query variables (includes filters, search, sort) */
|
|
5
|
+
variables: {
|
|
6
|
+
input: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
visibleColumns: string[];
|
|
9
|
+
fieldNames: string[];
|
|
10
|
+
modelName: string;
|
|
11
|
+
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,184 @@
|
|
|
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 escapeCsvValue(val) {
|
|
7
|
+
if (val === null || val === void 0) return "";
|
|
8
|
+
let str;
|
|
9
|
+
if (typeof val === "object") {
|
|
10
|
+
const obj = val;
|
|
11
|
+
str = obj.id ? String(obj.id) : JSON.stringify(val);
|
|
12
|
+
} else {
|
|
13
|
+
str = String(val);
|
|
14
|
+
}
|
|
15
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
16
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
17
|
+
}
|
|
18
|
+
return str;
|
|
19
|
+
}
|
|
20
|
+
function generateCsv(items, columns) {
|
|
21
|
+
const header = columns.map((c) => escapeCsvValue(formatFieldName(c))).join(",");
|
|
22
|
+
const rows = items.map(
|
|
23
|
+
(item) => columns.map((col) => escapeCsvValue(item[col])).join(",")
|
|
24
|
+
);
|
|
25
|
+
return [header, ...rows].join("\r\n");
|
|
26
|
+
}
|
|
27
|
+
function downloadCsv(csv, filename) {
|
|
28
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
29
|
+
const url = URL.createObjectURL(blob);
|
|
30
|
+
const link = document.createElement("a");
|
|
31
|
+
link.href = url;
|
|
32
|
+
link.download = filename;
|
|
33
|
+
document.body.appendChild(link);
|
|
34
|
+
link.click();
|
|
35
|
+
document.body.removeChild(link);
|
|
36
|
+
URL.revokeObjectURL(url);
|
|
37
|
+
}
|
|
38
|
+
function ExportButton({
|
|
39
|
+
query,
|
|
40
|
+
dataPath,
|
|
41
|
+
variables,
|
|
42
|
+
visibleColumns,
|
|
43
|
+
fieldNames,
|
|
44
|
+
modelName,
|
|
45
|
+
hasActiveFilters
|
|
46
|
+
}) {
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
const [exporting, setExporting] = useState(null);
|
|
49
|
+
const [error, setError] = useState(null);
|
|
50
|
+
const modalRef = useRef(null);
|
|
51
|
+
const [runQuery] = useLazyQuery(query, {
|
|
52
|
+
fetchPolicy: "network-only"
|
|
53
|
+
});
|
|
54
|
+
const doExport = useCallback(
|
|
55
|
+
async (mode) => {
|
|
56
|
+
setExporting(mode);
|
|
57
|
+
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 };
|
|
60
|
+
try {
|
|
61
|
+
const { data, error: queryError } = await runQuery({ variables: { input } });
|
|
62
|
+
if (queryError) throw queryError;
|
|
63
|
+
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
|
+
}
|
|
73
|
+
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`);
|
|
77
|
+
setOpen(false);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
setError(err instanceof Error ? err.message : "Export failed");
|
|
80
|
+
} finally {
|
|
81
|
+
setExporting(null);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[runQuery, variables, fieldNames, visibleColumns, dataPath, modelName, hasActiveFilters]
|
|
85
|
+
);
|
|
86
|
+
const activeFilterCount = Object.keys(variables.input.filters ?? {}).length;
|
|
87
|
+
const hasSearch = Boolean(variables.input.search);
|
|
88
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
89
|
+
/* @__PURE__ */ jsxs(
|
|
90
|
+
"button",
|
|
91
|
+
{
|
|
92
|
+
onClick: () => {
|
|
93
|
+
setOpen(!open);
|
|
94
|
+
setError(null);
|
|
95
|
+
},
|
|
96
|
+
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
|
+
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" }) }),
|
|
99
|
+
"Export"
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
),
|
|
103
|
+
open && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
104
|
+
/* @__PURE__ */ jsx(
|
|
105
|
+
"div",
|
|
106
|
+
{
|
|
107
|
+
className: "fixed inset-0 z-40",
|
|
108
|
+
onClick: () => setOpen(false)
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
/* @__PURE__ */ jsx(
|
|
112
|
+
"div",
|
|
113
|
+
{
|
|
114
|
+
ref: modalRef,
|
|
115
|
+
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",
|
|
116
|
+
children: /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
|
|
117
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-medium text-gray-900 dark:text-gray-100 mb-3", children: "Export as CSV" }),
|
|
118
|
+
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 }),
|
|
119
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
120
|
+
/* @__PURE__ */ jsxs("div", { className: "border border-gray-200 dark:border-gray-700 rounded-md p-3", children: [
|
|
121
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-gray-100 mb-1", children: "Export All" }),
|
|
122
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mb-2", children: "All records and all columns. No filters applied." }),
|
|
123
|
+
/* @__PURE__ */ jsx(
|
|
124
|
+
"button",
|
|
125
|
+
{
|
|
126
|
+
disabled: exporting !== null,
|
|
127
|
+
onClick: () => doExport("all"),
|
|
128
|
+
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
|
+
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
|
+
] }),
|
|
134
|
+
"Exporting..."
|
|
135
|
+
] }) : `Download (${fieldNames.length} columns)`
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
] }),
|
|
139
|
+
/* @__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: [
|
|
140
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-gray-100 mb-1", children: "Export with Current Settings" }),
|
|
141
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mb-1", children: "Uses your current filters and visible columns." }),
|
|
142
|
+
/* @__PURE__ */ jsxs("ul", { className: "text-xs text-gray-500 dark:text-gray-400 mb-2 space-y-0.5", children: [
|
|
143
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
144
|
+
visibleColumns.length > 0 ? visibleColumns.length : fieldNames.length,
|
|
145
|
+
" columns selected"
|
|
146
|
+
] }),
|
|
147
|
+
activeFilterCount > 0 && /* @__PURE__ */ jsxs("li", { children: [
|
|
148
|
+
activeFilterCount,
|
|
149
|
+
" filter",
|
|
150
|
+
activeFilterCount !== 1 ? "s" : "",
|
|
151
|
+
" active"
|
|
152
|
+
] }),
|
|
153
|
+
hasSearch && /* @__PURE__ */ jsxs("li", { children: [
|
|
154
|
+
'Search: "',
|
|
155
|
+
variables.input.search,
|
|
156
|
+
'"'
|
|
157
|
+
] })
|
|
158
|
+
] }),
|
|
159
|
+
/* @__PURE__ */ jsx(
|
|
160
|
+
"button",
|
|
161
|
+
{
|
|
162
|
+
disabled: exporting !== null,
|
|
163
|
+
onClick: () => doExport("filtered"),
|
|
164
|
+
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
|
+
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
|
+
] }),
|
|
170
|
+
"Exporting..."
|
|
171
|
+
] }) : "Download"
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
] })
|
|
175
|
+
] })
|
|
176
|
+
] })
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
] })
|
|
180
|
+
] });
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
ExportButton
|
|
184
|
+
};
|
|
@@ -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: [
|
|
@@ -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) {
|
|
@@ -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) {
|
|
@@ -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") {
|
|
@@ -21,12 +21,12 @@ function RelationFilterField({
|
|
|
21
21
|
relatedModelName,
|
|
22
22
|
searchTerm,
|
|
23
23
|
isOpen,
|
|
24
|
-
currentValue
|
|
24
|
+
currentValue?.id
|
|
25
25
|
// Pass current value ID so it can be loaded even when dropdown is closed
|
|
26
26
|
);
|
|
27
27
|
const currentItem = useMemo(
|
|
28
|
-
() =>
|
|
29
|
-
[currentValue
|
|
28
|
+
() => currentValue?.id ? relatedItems.find((item) => item.id === currentValue.id) : null,
|
|
29
|
+
[currentValue?.id, relatedItems]
|
|
30
30
|
);
|
|
31
31
|
const handleSelect = useCallback((item) => {
|
|
32
32
|
onChange({ id: item.id });
|
|
@@ -54,7 +54,7 @@ function RelationFilterField({
|
|
|
54
54
|
"input",
|
|
55
55
|
{
|
|
56
56
|
type: "text",
|
|
57
|
-
value:
|
|
57
|
+
value: currentValue?.id || "",
|
|
58
58
|
onChange: (e) => onChange(e.target.value ? { id: e.target.value } : void 0),
|
|
59
59
|
placeholder: "Enter ID...",
|
|
60
60
|
className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web text-sm"
|
|
@@ -9,7 +9,7 @@ function adminListReducer(state, action) {
|
|
|
9
9
|
case "SET_SKIP":
|
|
10
10
|
return { ...state, skip: action.payload };
|
|
11
11
|
case "SET_PAGE_SIZE":
|
|
12
|
-
return { ...state, pageSize: action.payload };
|
|
12
|
+
return { ...state, pageSize: action.payload, skip: 0 };
|
|
13
13
|
case "SET_SORT":
|
|
14
14
|
return { ...state, sort: action.payload };
|
|
15
15
|
case "SET_VISIBLE_COLUMNS":
|
|
@@ -16,7 +16,7 @@ function useRelationData(relatedModelName, searchTerm, isOpen, currentValueId) {
|
|
|
16
16
|
[sdk, relatedModel]
|
|
17
17
|
);
|
|
18
18
|
const relatedDataPath = useMemo(
|
|
19
|
-
() =>
|
|
19
|
+
() => relatedModel?.pluralModelPropertyName || relatedModelName.charAt(0).toLowerCase() + relatedModelName.slice(1) + "s",
|
|
20
20
|
[relatedModel, relatedModelName]
|
|
21
21
|
);
|
|
22
22
|
const searchableFields = useMemo(() => {
|
|
@@ -60,8 +60,7 @@ function AdminDataLayout() {
|
|
|
60
60
|
showNotification("success", "Preferences exported successfully");
|
|
61
61
|
};
|
|
62
62
|
const handleImport = async (event) => {
|
|
63
|
-
|
|
64
|
-
const file = (_a = event.target.files) == null ? void 0 : _a[0];
|
|
63
|
+
const file = event.target.files?.[0];
|
|
65
64
|
if (!file) return;
|
|
66
65
|
try {
|
|
67
66
|
const content2 = await file.text();
|
|
@@ -85,8 +84,7 @@ function AdminDataLayout() {
|
|
|
85
84
|
}
|
|
86
85
|
};
|
|
87
86
|
const triggerImport = () => {
|
|
88
|
-
|
|
89
|
-
(_a = fileInputRef.current) == null ? void 0 : _a.click();
|
|
87
|
+
fileInputRef.current?.click();
|
|
90
88
|
};
|
|
91
89
|
const content = /* @__PURE__ */ jsxs("div", { className: "max-w-full mx-auto flex flex-col p-0.5", children: [
|
|
92
90
|
notification && /* @__PURE__ */ jsx("div", { className: `mb-4 px-4 py-3 rounded-md border ${notification.type === "success" ? "bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200" : "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200"}`, children: /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
|
|
@@ -102,7 +100,7 @@ function AdminDataLayout() {
|
|
|
102
100
|
"select",
|
|
103
101
|
{
|
|
104
102
|
id: "model-selector",
|
|
105
|
-
value:
|
|
103
|
+
value: currentModel?.name || "",
|
|
106
104
|
onChange: handleModelChange,
|
|
107
105
|
className: "w-full h-[50px] pl-4 pr-10 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web text-base appearance-none cursor-pointer",
|
|
108
106
|
children: [
|
|
@@ -83,10 +83,9 @@ function AdminDataCreatePageContent({ model, basePath, formTheme, displayFieldCo
|
|
|
83
83
|
}
|
|
84
84
|
}, [sdk, model]);
|
|
85
85
|
const CREATE_MUTATION = useMemo(() => {
|
|
86
|
-
|
|
87
|
-
if (!(documents == null ? void 0 : documents.create)) return null;
|
|
86
|
+
if (!documents?.create) return null;
|
|
88
87
|
try {
|
|
89
|
-
if (
|
|
88
|
+
if (documents.create?.kind === "Document" && documents.create?.definitions) {
|
|
90
89
|
return documents.create;
|
|
91
90
|
}
|
|
92
91
|
return gql(documents.create);
|
|
@@ -142,9 +141,6 @@ function AdminDataCreatePageContent({ model, basePath, formTheme, displayFieldCo
|
|
|
142
141
|
status: "success",
|
|
143
142
|
message: `${toReadableText(model.name)} created successfully!`
|
|
144
143
|
});
|
|
145
|
-
setTimeout(() => {
|
|
146
|
-
navigate(`${basePath}/${toKebabCase(model.pluralName)}`);
|
|
147
|
-
}, 1500);
|
|
148
144
|
} catch (error) {
|
|
149
145
|
setSubmissionState({
|
|
150
146
|
status: "error",
|
|
@@ -152,6 +148,13 @@ function AdminDataCreatePageContent({ model, basePath, formTheme, displayFieldCo
|
|
|
152
148
|
});
|
|
153
149
|
}
|
|
154
150
|
};
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (submissionState.status !== "success") return;
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
navigate(`${basePath}/${toKebabCase(model.pluralName)}`);
|
|
155
|
+
}, 1500);
|
|
156
|
+
return () => clearTimeout(timer);
|
|
157
|
+
}, [submissionState.status, navigate, basePath, model.pluralName]);
|
|
155
158
|
useEffect(() => {
|
|
156
159
|
if (submissionState.status === "error") {
|
|
157
160
|
const timer = setTimeout(() => {
|
|
@@ -36,8 +36,7 @@ const toKebabCase = (str) => {
|
|
|
36
36
|
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
37
37
|
};
|
|
38
38
|
function processRelationFieldValue(field, item) {
|
|
39
|
-
|
|
40
|
-
const relationFieldName = ((_a = field.relationFromFields) == null ? void 0 : _a[0]) || `${field.name}Id`;
|
|
39
|
+
const relationFieldName = field.relationFromFields?.[0] || `${field.name}Id`;
|
|
41
40
|
let value = item[relationFieldName];
|
|
42
41
|
if (value === void 0) {
|
|
43
42
|
const relationObject = item[field.name];
|
|
@@ -95,7 +94,7 @@ function performFinalSafetyChecks(initialValues, model) {
|
|
|
95
94
|
initialValues[key] = value.id;
|
|
96
95
|
} else if (value instanceof Date) {
|
|
97
96
|
const field = model.fields.find((f) => f.name === key);
|
|
98
|
-
initialValues[key] =
|
|
97
|
+
initialValues[key] = field?.type.toLowerCase() === "date" ? value.toISOString().split("T")[0] : value.toISOString();
|
|
99
98
|
} else {
|
|
100
99
|
initialValues[key] = "";
|
|
101
100
|
}
|
|
@@ -106,7 +105,6 @@ function extractInitialValues(model, item) {
|
|
|
106
105
|
if (!item) {
|
|
107
106
|
return initialValues;
|
|
108
107
|
}
|
|
109
|
-
console.log(`[DataBrowser] Extracting initial values for ${model.name}:`, item);
|
|
110
108
|
const idField = model.fields.find((f) => f.isId);
|
|
111
109
|
if (idField) {
|
|
112
110
|
initialValues[idField.name] = item[idField.name];
|
|
@@ -118,10 +116,9 @@ function extractInitialValues(model, item) {
|
|
|
118
116
|
return true;
|
|
119
117
|
});
|
|
120
118
|
editableFields.forEach((field) => {
|
|
121
|
-
var _a;
|
|
122
119
|
let value = item[field.name];
|
|
123
120
|
if (field.relationName && !field.isList) {
|
|
124
|
-
const relationFieldName =
|
|
121
|
+
const relationFieldName = field.relationFromFields?.[0] || `${field.name}Id`;
|
|
125
122
|
initialValues[relationFieldName] = processRelationFieldValue(field, item);
|
|
126
123
|
} else {
|
|
127
124
|
const fieldTypeLower = field.type.toLowerCase();
|
|
@@ -129,32 +126,16 @@ function extractInitialValues(model, item) {
|
|
|
129
126
|
if (!Array.isArray(value)) {
|
|
130
127
|
value = [];
|
|
131
128
|
}
|
|
132
|
-
const arrayValue = value;
|
|
133
129
|
value = value.join(",");
|
|
134
|
-
console.log(`[DataBrowser] Enum array field detected - "${field.name}":`, {
|
|
135
|
-
type: field.type,
|
|
136
|
-
arrayValue,
|
|
137
|
-
convertedValue: value
|
|
138
|
-
});
|
|
139
130
|
} else if (fieldTypeLower === "datetime" || fieldTypeLower === "date") {
|
|
140
131
|
value = processDateFieldValue(field, value);
|
|
141
132
|
} else {
|
|
142
133
|
value = sanitizeFieldValue(value, field);
|
|
143
134
|
}
|
|
144
135
|
initialValues[field.name] = value;
|
|
145
|
-
if (value !== null && typeof value === "string" && fieldTypeLower !== "string" && fieldTypeLower !== "datetime" && fieldTypeLower !== "date") {
|
|
146
|
-
console.log(`[DataBrowser] Enum field detected - "${field.name}":`, {
|
|
147
|
-
type: field.type,
|
|
148
|
-
kind: field.kind,
|
|
149
|
-
isList: field.isList,
|
|
150
|
-
extractedValue: value,
|
|
151
|
-
rawValue: item[field.name]
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
136
|
}
|
|
155
137
|
});
|
|
156
138
|
performFinalSafetyChecks(initialValues, model);
|
|
157
|
-
console.log(`[DataBrowser] Final initial values for ${model.name}:`, initialValues);
|
|
158
139
|
return initialValues;
|
|
159
140
|
}
|
|
160
141
|
const validateId = (id) => {
|
|
@@ -367,10 +348,9 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
367
348
|
}
|
|
368
349
|
}, [sdk, model]);
|
|
369
350
|
const QUERY = useMemo(() => {
|
|
370
|
-
|
|
371
|
-
if (!(documents == null ? void 0 : documents.query)) return null;
|
|
351
|
+
if (!documents?.query) return null;
|
|
372
352
|
try {
|
|
373
|
-
if (
|
|
353
|
+
if (documents.query?.kind === "Document" && documents.query?.definitions) {
|
|
374
354
|
return documents.query;
|
|
375
355
|
}
|
|
376
356
|
return gql(documents.query);
|
|
@@ -379,10 +359,9 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
379
359
|
}
|
|
380
360
|
}, [documents]);
|
|
381
361
|
const UPDATE_MUTATION = useMemo(() => {
|
|
382
|
-
|
|
383
|
-
if (!(documents == null ? void 0 : documents.update)) return null;
|
|
362
|
+
if (!documents?.update) return null;
|
|
384
363
|
try {
|
|
385
|
-
if (
|
|
364
|
+
if (documents.update?.kind === "Document" && documents.update?.definitions) {
|
|
386
365
|
return documents.update;
|
|
387
366
|
}
|
|
388
367
|
return gql(documents.update);
|
|
@@ -391,10 +370,9 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
391
370
|
}
|
|
392
371
|
}, [documents]);
|
|
393
372
|
const DELETE_MUTATION = useMemo(() => {
|
|
394
|
-
|
|
395
|
-
if (!(documents == null ? void 0 : documents.delete)) return null;
|
|
373
|
+
if (!documents?.delete) return null;
|
|
396
374
|
try {
|
|
397
|
-
if (
|
|
375
|
+
if (documents.delete?.kind === "Document" && documents.delete?.definitions) {
|
|
398
376
|
return documents.delete;
|
|
399
377
|
}
|
|
400
378
|
return gql(documents.delete);
|
|
@@ -430,6 +408,13 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
430
408
|
}
|
|
431
409
|
`
|
|
432
410
|
);
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
if (deleteState.status !== "success") return;
|
|
413
|
+
const timer = setTimeout(() => {
|
|
414
|
+
navigate(`${basePath}/${toKebabCase(model.pluralName)}`);
|
|
415
|
+
}, 1500);
|
|
416
|
+
return () => clearTimeout(timer);
|
|
417
|
+
}, [deleteState.status, navigate, basePath, model.pluralName]);
|
|
433
418
|
useEffect(() => {
|
|
434
419
|
if (submissionState.status === "error") {
|
|
435
420
|
const timer = setTimeout(() => {
|
|
@@ -482,7 +467,7 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
482
467
|
}
|
|
483
468
|
);
|
|
484
469
|
}
|
|
485
|
-
const item = data
|
|
470
|
+
const item = data?.[responseFieldName];
|
|
486
471
|
if (!item) {
|
|
487
472
|
return /* @__PURE__ */ jsx(
|
|
488
473
|
AdminDataStateMessage,
|
|
@@ -505,18 +490,17 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
505
490
|
});
|
|
506
491
|
const initialValues = extractInitialValues(model, item);
|
|
507
492
|
const handleSubmit = async (formData) => {
|
|
508
|
-
console.log(`[DataBrowser] Form submission for ${model.name}:`, formData);
|
|
509
493
|
setSubmissionState({ status: "loading" });
|
|
510
494
|
const result = await executeUpdateMutation(updateMutation, formData, model, id, idVariableName);
|
|
511
495
|
setSubmissionState({
|
|
512
496
|
status: result.success ? "success" : "error",
|
|
513
497
|
message: result.message
|
|
514
498
|
});
|
|
515
|
-
|
|
499
|
+
if (typeof window !== "undefined") {
|
|
500
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
501
|
+
}
|
|
516
502
|
if (result.success) {
|
|
517
|
-
|
|
518
|
-
const refetchResult = await refetch();
|
|
519
|
-
console.log(`[DataBrowser] Refetch complete for ${model.name}:`, refetchResult.data);
|
|
503
|
+
await refetch();
|
|
520
504
|
}
|
|
521
505
|
};
|
|
522
506
|
const handleDelete = async () => {
|
|
@@ -526,11 +510,6 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
526
510
|
status: result.success ? "success" : "error",
|
|
527
511
|
message: result.message
|
|
528
512
|
});
|
|
529
|
-
if (result.success) {
|
|
530
|
-
setTimeout(() => {
|
|
531
|
-
navigate(`${basePath}/${toKebabCase(model.pluralName)}`);
|
|
532
|
-
}, 1500);
|
|
533
|
-
}
|
|
534
513
|
};
|
|
535
514
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
536
515
|
/* @__PURE__ */ jsxs("div", { className: "mb-8", children: [
|
|
@@ -7,6 +7,7 @@ import { AdminLocalStorage } from "../utils/secure-storage.js";
|
|
|
7
7
|
import { kebabCase, formatFieldName } from "../utils/string-utils.js";
|
|
8
8
|
import { useParams, useSearchParams, Link } from "react-router";
|
|
9
9
|
import { FilterField } from "../components/FilterField.js";
|
|
10
|
+
import { ExportButton } from "../components/ExportButton.js";
|
|
10
11
|
import { useAdminList } from "../hooks/useAdminList.js";
|
|
11
12
|
import { getAdminDocuments } from "../utils/graphql-utils.js";
|
|
12
13
|
import { useAdminDataContext } from "../context/AdminDataContext.js";
|
|
@@ -114,7 +115,7 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
114
115
|
}, 500);
|
|
115
116
|
return () => clearTimeout(timer);
|
|
116
117
|
}, [state.search]);
|
|
117
|
-
const pluralParam = propModelName || (
|
|
118
|
+
const pluralParam = propModelName || (params?.dataTypePlural ?? "");
|
|
118
119
|
const model = useMemo(() => {
|
|
119
120
|
if (propModelName) {
|
|
120
121
|
return databaseModels.find((m) => m.name === propModelName);
|
|
@@ -131,10 +132,7 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
131
132
|
for (const [key, value] of searchParams.entries()) {
|
|
132
133
|
if (key !== "page" && key !== "search" && key !== "sort") {
|
|
133
134
|
const relationField = model.fields.find(
|
|
134
|
-
(f) =>
|
|
135
|
-
var _a;
|
|
136
|
-
return f.relationName && !f.isList && ((_a = f.relationFromFields) == null ? void 0 : _a[0]) === key;
|
|
137
|
-
}
|
|
135
|
+
(f) => f.relationName && !f.isList && f.relationFromFields?.[0] === key
|
|
138
136
|
);
|
|
139
137
|
if (relationField) {
|
|
140
138
|
filters2[relationField.name] = { id: value };
|
|
@@ -162,7 +160,7 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
162
160
|
}, [sdk, model]);
|
|
163
161
|
const { listQuery: query } = documents;
|
|
164
162
|
const dataPath = useMemo(() => {
|
|
165
|
-
return
|
|
163
|
+
return model?.pluralModelPropertyName || pluralParam.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
166
164
|
}, [model, pluralParam]);
|
|
167
165
|
const paginationPath = useMemo(() => {
|
|
168
166
|
return "counters";
|
|
@@ -232,7 +230,7 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
232
230
|
dispatch({ type: "SET_FILTERS", payload: urlFilters });
|
|
233
231
|
dispatch({ type: "SET_SHOW_FILTERS", payload: true });
|
|
234
232
|
}
|
|
235
|
-
}, [model
|
|
233
|
+
}, [model?.name, fieldNames, searchableFieldNames, getDefaultSearchFields, setVisibleColumns, setSearchFields, dispatch, urlFilters]);
|
|
236
234
|
const setSortSafely = useCallback(
|
|
237
235
|
(newSort) => {
|
|
238
236
|
const resolvedSort = typeof newSort === "function" ? newSort(sort) : newSort;
|
|
@@ -301,11 +299,10 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
301
299
|
}
|
|
302
300
|
});
|
|
303
301
|
const { validatedItems, validatedPagination, dataError } = useMemo(() => {
|
|
304
|
-
var _a, _b;
|
|
305
302
|
if (error) {
|
|
306
303
|
const apolloError = error;
|
|
307
304
|
if (apolloError.networkError) ;
|
|
308
|
-
if (
|
|
305
|
+
if (apolloError.graphQLErrors?.length > 0) ;
|
|
309
306
|
}
|
|
310
307
|
if (!data) {
|
|
311
308
|
return {
|
|
@@ -321,7 +318,7 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
321
318
|
if (!processedItems || processedItems.length === 0) {
|
|
322
319
|
for (const [key, value] of Object.entries(anyData)) {
|
|
323
320
|
if (Array.isArray(value)) {
|
|
324
|
-
if (value.length > 0 &&
|
|
321
|
+
if (value.length > 0 && value[0]?.id) {
|
|
325
322
|
processedItems = value;
|
|
326
323
|
break;
|
|
327
324
|
}
|
|
@@ -744,6 +741,32 @@ function AdminDataListPage({ modelName: propModelName } = {}) {
|
|
|
744
741
|
}
|
|
745
742
|
),
|
|
746
743
|
columnSelector,
|
|
744
|
+
query && /* @__PURE__ */ jsx(
|
|
745
|
+
ExportButton,
|
|
746
|
+
{
|
|
747
|
+
query,
|
|
748
|
+
dataPath,
|
|
749
|
+
variables,
|
|
750
|
+
visibleColumns,
|
|
751
|
+
fieldNames,
|
|
752
|
+
modelName: model.name,
|
|
753
|
+
hasActiveFilters: Object.keys(filters).length > 0
|
|
754
|
+
}
|
|
755
|
+
),
|
|
756
|
+
/* @__PURE__ */ jsxs(
|
|
757
|
+
"select",
|
|
758
|
+
{
|
|
759
|
+
value: pageSize,
|
|
760
|
+
onChange: (e) => dispatch({ type: "SET_PAGE_SIZE", payload: Number(e.target.value) }),
|
|
761
|
+
className: "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",
|
|
762
|
+
"aria-label": "Rows per page",
|
|
763
|
+
children: [
|
|
764
|
+
/* @__PURE__ */ jsx("option", { value: 20, children: "20 / page" }),
|
|
765
|
+
/* @__PURE__ */ jsx("option", { value: 50, children: "50 / page" }),
|
|
766
|
+
/* @__PURE__ */ jsx("option", { value: 100, children: "100 / page" })
|
|
767
|
+
]
|
|
768
|
+
}
|
|
769
|
+
),
|
|
747
770
|
/* @__PURE__ */ jsx(
|
|
748
771
|
Link,
|
|
749
772
|
{
|
|
@@ -77,7 +77,6 @@ function toLowerCamelCase(str) {
|
|
|
77
77
|
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
78
78
|
}
|
|
79
79
|
function findForeignKeyFieldName(relatedModel, currentModelName, relationName) {
|
|
80
|
-
var _a;
|
|
81
80
|
if (!relatedModel) return null;
|
|
82
81
|
const foreignKeyField = relatedModel.fields.find((f) => {
|
|
83
82
|
if (f.type !== currentModelName) return false;
|
|
@@ -86,7 +85,7 @@ function findForeignKeyFieldName(relatedModel, currentModelName, relationName) {
|
|
|
86
85
|
if (relationName && f.relationName !== relationName) return false;
|
|
87
86
|
return true;
|
|
88
87
|
});
|
|
89
|
-
return
|
|
88
|
+
return foreignKeyField?.relationFromFields?.[0] || null;
|
|
90
89
|
}
|
|
91
90
|
function buildFormFields(sdk, model, operation, options = {}) {
|
|
92
91
|
const {
|
|
@@ -118,7 +117,6 @@ function buildFormFields(sdk, model, operation, options = {}) {
|
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
119
|
regularFields.forEach((field) => {
|
|
121
|
-
var _a;
|
|
122
120
|
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
123
121
|
let initialValue = currentItem && operation === "update" ? currentItem[field.name] : void 0;
|
|
124
122
|
if (field.type.toLowerCase() === "datetime" || field.type.toLowerCase() === "date") {
|
|
@@ -191,22 +189,11 @@ function buildFormFields(sdk, model, operation, options = {}) {
|
|
|
191
189
|
default: {
|
|
192
190
|
const enumValues = getEnumValues(sdk, field.type);
|
|
193
191
|
if (enumValues) {
|
|
194
|
-
console.log(`[DataBrowser] Building enum field "${field.name}":`, {
|
|
195
|
-
enumType: field.type,
|
|
196
|
-
availableValues: enumValues,
|
|
197
|
-
currentValue: initialValue,
|
|
198
|
-
operation,
|
|
199
|
-
isList: field.isList
|
|
200
|
-
});
|
|
201
192
|
if (field.isList) {
|
|
202
193
|
let defaultValue = "";
|
|
203
194
|
if (Array.isArray(initialValue) && initialValue.length > 0) {
|
|
204
195
|
defaultValue = initialValue.join(",");
|
|
205
196
|
}
|
|
206
|
-
console.log(`[DataBrowser] Array enum field "${field.name}" converted:`, {
|
|
207
|
-
arrayValue: initialValue,
|
|
208
|
-
stringValue: defaultValue
|
|
209
|
-
});
|
|
210
197
|
const checkboxOptions = enumValues.map((value) => ({
|
|
211
198
|
key: value,
|
|
212
199
|
value,
|
|
@@ -231,7 +218,7 @@ function buildFormFields(sdk, model, operation, options = {}) {
|
|
|
231
218
|
break;
|
|
232
219
|
}
|
|
233
220
|
if (field.relationName && !field.isList) {
|
|
234
|
-
const relationFieldName =
|
|
221
|
+
const relationFieldName = field.relationFromFields?.[0] || `${field.name}Id`;
|
|
235
222
|
let relationValue = currentItem && operation === "update" ? currentItem[relationFieldName] : void 0;
|
|
236
223
|
if (relationValue === void 0 && currentItem && operation === "update") {
|
|
237
224
|
const relationObject = currentItem[field.name];
|
|
@@ -250,9 +237,9 @@ function buildFormFields(sdk, model, operation, options = {}) {
|
|
|
250
237
|
const regularDocumentName = `${properPluralName}`;
|
|
251
238
|
const relationDocument = sdk[adminDocumentName] || sdk[regularDocumentName];
|
|
252
239
|
if (relationDocument) {
|
|
253
|
-
const config = displayFieldConfig
|
|
254
|
-
const displayFields =
|
|
255
|
-
const searchFields =
|
|
240
|
+
const config = displayFieldConfig?.[field.type];
|
|
241
|
+
const displayFields = config?.display || ["name", "title"];
|
|
242
|
+
const searchFields = config?.search || displayFields;
|
|
256
243
|
const getDisplayLabel = (item) => {
|
|
257
244
|
const allValues = displayFields.map((field2) => item[field2]).filter((val) => val != null && val !== "");
|
|
258
245
|
if (allValues.length > 0) {
|
|
@@ -342,17 +329,16 @@ function buildFormFields(sdk, model, operation, options = {}) {
|
|
|
342
329
|
formFields.push(formField);
|
|
343
330
|
});
|
|
344
331
|
listRelationFields.forEach((field) => {
|
|
345
|
-
var _a;
|
|
346
332
|
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
347
333
|
if (operation === "update" && currentItem) {
|
|
348
334
|
const pluralModelName = getPluralName(field.type);
|
|
349
335
|
const relatedModelKebab = toKebabCase(pluralModelName);
|
|
350
336
|
const displayName = field.type.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
351
337
|
const pluralDisplayName = getPluralName(displayName);
|
|
352
|
-
const relatedModel = databaseModels
|
|
338
|
+
const relatedModel = databaseModels?.find((m) => m.name === field.type);
|
|
353
339
|
const foreignKeyFieldName = findForeignKeyFieldName(relatedModel, model.name, field.relationName) || `${toLowerCamelCase(model.name)}Id`;
|
|
354
340
|
const filterUrl = `${basePath}/${relatedModelKebab}?${foreignKeyFieldName}=${currentItem.id}`;
|
|
355
|
-
const countData =
|
|
341
|
+
const countData = currentItem._count?.[field.name];
|
|
356
342
|
const countText = countData !== void 0 ? ` (${countData})` : "";
|
|
357
343
|
const formField = FormFieldClass.content(field.name, {
|
|
358
344
|
content: React.createElement("div", { className: "py-2" }, [
|
|
@@ -428,11 +414,10 @@ function shouldSkipValue(key, value) {
|
|
|
428
414
|
return SYSTEM_FIELDS.has(key) || value === void 0;
|
|
429
415
|
}
|
|
430
416
|
function convertStringValue(value, field) {
|
|
431
|
-
var _a;
|
|
432
417
|
if (value === "") {
|
|
433
418
|
return null;
|
|
434
419
|
}
|
|
435
|
-
if (
|
|
420
|
+
if (field?.type.toLowerCase() === "datetime" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) {
|
|
436
421
|
try {
|
|
437
422
|
const date = new Date(value);
|
|
438
423
|
return date.toISOString();
|
|
@@ -442,7 +427,7 @@ function convertStringValue(value, field) {
|
|
|
442
427
|
}
|
|
443
428
|
if (value === "true") return true;
|
|
444
429
|
if (value === "false") return false;
|
|
445
|
-
const fieldType =
|
|
430
|
+
const fieldType = field?.type?.toLowerCase();
|
|
446
431
|
const isNumericField = fieldType && ["int", "bigint", "float", "decimal"].includes(fieldType);
|
|
447
432
|
if (isNumericField) {
|
|
448
433
|
const numericPattern = /^-?\d+(\.\d+)?$/;
|
|
@@ -460,17 +445,15 @@ function processNestedObject(value, model) {
|
|
|
460
445
|
return Object.keys(cleanedNested).length > 0 ? cleanedNested : null;
|
|
461
446
|
}
|
|
462
447
|
function cleanFormInput(input, model) {
|
|
463
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
464
|
-
console.log(`[DataBrowser] Cleaning form input for ${model == null ? void 0 : model.name}:`, input);
|
|
465
448
|
const cleaned = {};
|
|
466
449
|
const booleanFields = new Set(
|
|
467
|
-
|
|
450
|
+
model?.fields?.filter((field) => field.type.toLowerCase() === "boolean")?.map((field) => field.name) || []
|
|
468
451
|
);
|
|
469
452
|
const requiredArrayFields = new Set(
|
|
470
|
-
|
|
453
|
+
model?.fields?.filter((field) => field.isList && !field.isOptional && !field.relationName)?.map((field) => field.name) || []
|
|
471
454
|
);
|
|
472
455
|
const enumArrayFields = new Set(
|
|
473
|
-
|
|
456
|
+
model?.fields?.filter((field) => field.isList && field.kind === "enum")?.map((field) => field.name) || []
|
|
474
457
|
);
|
|
475
458
|
for (const [key, value] of Object.entries(input)) {
|
|
476
459
|
if (booleanFields.has(key) && value === void 0) {
|
|
@@ -480,10 +463,6 @@ function cleanFormInput(input, model) {
|
|
|
480
463
|
if (enumArrayFields.has(key)) {
|
|
481
464
|
if (typeof value === "string") {
|
|
482
465
|
const arrayValue = value.split(",").filter((v) => v.trim() !== "");
|
|
483
|
-
console.log(`[DataBrowser] Converting enum array "${key}":`, {
|
|
484
|
-
stringValue: value,
|
|
485
|
-
arrayValue
|
|
486
|
-
});
|
|
487
466
|
cleaned[key] = arrayValue;
|
|
488
467
|
continue;
|
|
489
468
|
} else if (Array.isArray(value)) {
|
|
@@ -502,7 +481,7 @@ function cleanFormInput(input, model) {
|
|
|
502
481
|
continue;
|
|
503
482
|
}
|
|
504
483
|
if (typeof value === "string") {
|
|
505
|
-
const field =
|
|
484
|
+
const field = model?.fields?.find((f) => f.name === key);
|
|
506
485
|
const convertedValue = convertStringValue(value, field);
|
|
507
486
|
cleaned[key] = convertedValue;
|
|
508
487
|
continue;
|
|
@@ -521,7 +500,6 @@ function cleanFormInput(input, model) {
|
|
|
521
500
|
}
|
|
522
501
|
cleaned[key] = value;
|
|
523
502
|
}
|
|
524
|
-
console.log(`[DataBrowser] Cleaned form input for ${model == null ? void 0 : model.name}:`, cleaned);
|
|
525
503
|
return cleaned;
|
|
526
504
|
}
|
|
527
505
|
export {
|
|
@@ -82,11 +82,10 @@ const SecureAdminLocalStorage = {
|
|
|
82
82
|
},
|
|
83
83
|
// Get visible columns for a specific model with sanitization
|
|
84
84
|
getColumnVisibility: (modelName) => {
|
|
85
|
-
var _a;
|
|
86
85
|
const sanitizedModelName = sanitizeString(modelName);
|
|
87
86
|
if (!sanitizedModelName) return null;
|
|
88
87
|
const config = SecureAdminLocalStorage.getConfig();
|
|
89
|
-
const columns =
|
|
88
|
+
const columns = config.models[sanitizedModelName]?.visibleColumns;
|
|
90
89
|
return columns ? sanitizeArray(columns) : null;
|
|
91
90
|
},
|
|
92
91
|
// Set visible columns for a specific model with validation
|
|
@@ -103,11 +102,10 @@ const SecureAdminLocalStorage = {
|
|
|
103
102
|
},
|
|
104
103
|
// Get sort preference for a specific model with validation
|
|
105
104
|
getSortPreference: (modelName) => {
|
|
106
|
-
var _a;
|
|
107
105
|
const sanitizedModelName = sanitizeString(modelName);
|
|
108
106
|
if (!sanitizedModelName) return null;
|
|
109
107
|
const config = SecureAdminLocalStorage.getConfig();
|
|
110
|
-
const sortPref =
|
|
108
|
+
const sortPref = config.models[sanitizedModelName]?.sortPreference;
|
|
111
109
|
return sortPref ? sanitizeSortPreference(sortPref) : null;
|
|
112
110
|
},
|
|
113
111
|
// Set sort preference for a specific model with validation
|
|
@@ -124,11 +122,10 @@ const SecureAdminLocalStorage = {
|
|
|
124
122
|
},
|
|
125
123
|
// Get search fields for a specific model with sanitization
|
|
126
124
|
getSearchFields: (modelName) => {
|
|
127
|
-
var _a;
|
|
128
125
|
const sanitizedModelName = sanitizeString(modelName);
|
|
129
126
|
if (!sanitizedModelName) return null;
|
|
130
127
|
const config = SecureAdminLocalStorage.getConfig();
|
|
131
|
-
const searchFields =
|
|
128
|
+
const searchFields = config.models[sanitizedModelName]?.searchFields;
|
|
132
129
|
return searchFields ? sanitizeArray(searchFields) : null;
|
|
133
130
|
},
|
|
134
131
|
// Set search fields for a specific model with validation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestledjs/data-browser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Universal admin data browser for Nestled framework projects with full CRUD operations",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@apollo/client": "^4.0.0",
|
|
39
39
|
"@heroicons/react": "^2.0.0",
|
|
40
40
|
"@nestledjs/forms": "^0.6.3",
|
|
41
|
-
"@nestledjs/shared-components": "^1.0.
|
|
41
|
+
"@nestledjs/shared-components": "^1.0.14",
|
|
42
42
|
"react": "^19.0.0",
|
|
43
43
|
"react-router": "^7.0.0"
|
|
44
44
|
},
|
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
"registry": "https://registry.npmjs.org/"
|
|
54
54
|
},
|
|
55
55
|
"type": "module"
|
|
56
|
-
}
|
|
56
|
+
}
|