@nestledjs/data-browser 1.0.14 → 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 +20 -21
- package/lib/pages/AdminDataListPage.js +33 -10
- package/lib/utils/graphql-utils.js +13 -18
- package/lib/utils/secure-storage.js +3 -6
- package/package.json +2 -2
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
|
}
|
|
@@ -117,10 +116,9 @@ function extractInitialValues(model, item) {
|
|
|
117
116
|
return true;
|
|
118
117
|
});
|
|
119
118
|
editableFields.forEach((field) => {
|
|
120
|
-
var _a;
|
|
121
119
|
let value = item[field.name];
|
|
122
120
|
if (field.relationName && !field.isList) {
|
|
123
|
-
const relationFieldName =
|
|
121
|
+
const relationFieldName = field.relationFromFields?.[0] || `${field.name}Id`;
|
|
124
122
|
initialValues[relationFieldName] = processRelationFieldValue(field, item);
|
|
125
123
|
} else {
|
|
126
124
|
const fieldTypeLower = field.type.toLowerCase();
|
|
@@ -350,10 +348,9 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
350
348
|
}
|
|
351
349
|
}, [sdk, model]);
|
|
352
350
|
const QUERY = useMemo(() => {
|
|
353
|
-
|
|
354
|
-
if (!(documents == null ? void 0 : documents.query)) return null;
|
|
351
|
+
if (!documents?.query) return null;
|
|
355
352
|
try {
|
|
356
|
-
if (
|
|
353
|
+
if (documents.query?.kind === "Document" && documents.query?.definitions) {
|
|
357
354
|
return documents.query;
|
|
358
355
|
}
|
|
359
356
|
return gql(documents.query);
|
|
@@ -362,10 +359,9 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
362
359
|
}
|
|
363
360
|
}, [documents]);
|
|
364
361
|
const UPDATE_MUTATION = useMemo(() => {
|
|
365
|
-
|
|
366
|
-
if (!(documents == null ? void 0 : documents.update)) return null;
|
|
362
|
+
if (!documents?.update) return null;
|
|
367
363
|
try {
|
|
368
|
-
if (
|
|
364
|
+
if (documents.update?.kind === "Document" && documents.update?.definitions) {
|
|
369
365
|
return documents.update;
|
|
370
366
|
}
|
|
371
367
|
return gql(documents.update);
|
|
@@ -374,10 +370,9 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
374
370
|
}
|
|
375
371
|
}, [documents]);
|
|
376
372
|
const DELETE_MUTATION = useMemo(() => {
|
|
377
|
-
|
|
378
|
-
if (!(documents == null ? void 0 : documents.delete)) return null;
|
|
373
|
+
if (!documents?.delete) return null;
|
|
379
374
|
try {
|
|
380
|
-
if (
|
|
375
|
+
if (documents.delete?.kind === "Document" && documents.delete?.definitions) {
|
|
381
376
|
return documents.delete;
|
|
382
377
|
}
|
|
383
378
|
return gql(documents.delete);
|
|
@@ -413,6 +408,13 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
413
408
|
}
|
|
414
409
|
`
|
|
415
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]);
|
|
416
418
|
useEffect(() => {
|
|
417
419
|
if (submissionState.status === "error") {
|
|
418
420
|
const timer = setTimeout(() => {
|
|
@@ -465,7 +467,7 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
465
467
|
}
|
|
466
468
|
);
|
|
467
469
|
}
|
|
468
|
-
const item = data
|
|
470
|
+
const item = data?.[responseFieldName];
|
|
469
471
|
if (!item) {
|
|
470
472
|
return /* @__PURE__ */ jsx(
|
|
471
473
|
AdminDataStateMessage,
|
|
@@ -494,7 +496,9 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
494
496
|
status: result.success ? "success" : "error",
|
|
495
497
|
message: result.message
|
|
496
498
|
});
|
|
497
|
-
|
|
499
|
+
if (typeof window !== "undefined") {
|
|
500
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
501
|
+
}
|
|
498
502
|
if (result.success) {
|
|
499
503
|
await refetch();
|
|
500
504
|
}
|
|
@@ -506,11 +510,6 @@ function AdminDataEditPageContent({ model, id, basePath, formTheme, displayField
|
|
|
506
510
|
status: result.success ? "success" : "error",
|
|
507
511
|
message: result.message
|
|
508
512
|
});
|
|
509
|
-
if (result.success) {
|
|
510
|
-
setTimeout(() => {
|
|
511
|
-
navigate(`${basePath}/${toKebabCase(model.pluralName)}`);
|
|
512
|
-
}, 1500);
|
|
513
|
-
}
|
|
514
513
|
};
|
|
515
514
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
516
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") {
|
|
@@ -220,7 +218,7 @@ function buildFormFields(sdk, model, operation, options = {}) {
|
|
|
220
218
|
break;
|
|
221
219
|
}
|
|
222
220
|
if (field.relationName && !field.isList) {
|
|
223
|
-
const relationFieldName =
|
|
221
|
+
const relationFieldName = field.relationFromFields?.[0] || `${field.name}Id`;
|
|
224
222
|
let relationValue = currentItem && operation === "update" ? currentItem[relationFieldName] : void 0;
|
|
225
223
|
if (relationValue === void 0 && currentItem && operation === "update") {
|
|
226
224
|
const relationObject = currentItem[field.name];
|
|
@@ -239,9 +237,9 @@ function buildFormFields(sdk, model, operation, options = {}) {
|
|
|
239
237
|
const regularDocumentName = `${properPluralName}`;
|
|
240
238
|
const relationDocument = sdk[adminDocumentName] || sdk[regularDocumentName];
|
|
241
239
|
if (relationDocument) {
|
|
242
|
-
const config = displayFieldConfig
|
|
243
|
-
const displayFields =
|
|
244
|
-
const searchFields =
|
|
240
|
+
const config = displayFieldConfig?.[field.type];
|
|
241
|
+
const displayFields = config?.display || ["name", "title"];
|
|
242
|
+
const searchFields = config?.search || displayFields;
|
|
245
243
|
const getDisplayLabel = (item) => {
|
|
246
244
|
const allValues = displayFields.map((field2) => item[field2]).filter((val) => val != null && val !== "");
|
|
247
245
|
if (allValues.length > 0) {
|
|
@@ -331,17 +329,16 @@ function buildFormFields(sdk, model, operation, options = {}) {
|
|
|
331
329
|
formFields.push(formField);
|
|
332
330
|
});
|
|
333
331
|
listRelationFields.forEach((field) => {
|
|
334
|
-
var _a;
|
|
335
332
|
const label = field.name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
336
333
|
if (operation === "update" && currentItem) {
|
|
337
334
|
const pluralModelName = getPluralName(field.type);
|
|
338
335
|
const relatedModelKebab = toKebabCase(pluralModelName);
|
|
339
336
|
const displayName = field.type.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
340
337
|
const pluralDisplayName = getPluralName(displayName);
|
|
341
|
-
const relatedModel = databaseModels
|
|
338
|
+
const relatedModel = databaseModels?.find((m) => m.name === field.type);
|
|
342
339
|
const foreignKeyFieldName = findForeignKeyFieldName(relatedModel, model.name, field.relationName) || `${toLowerCamelCase(model.name)}Id`;
|
|
343
340
|
const filterUrl = `${basePath}/${relatedModelKebab}?${foreignKeyFieldName}=${currentItem.id}`;
|
|
344
|
-
const countData =
|
|
341
|
+
const countData = currentItem._count?.[field.name];
|
|
345
342
|
const countText = countData !== void 0 ? ` (${countData})` : "";
|
|
346
343
|
const formField = FormFieldClass.content(field.name, {
|
|
347
344
|
content: React.createElement("div", { className: "py-2" }, [
|
|
@@ -417,11 +414,10 @@ function shouldSkipValue(key, value) {
|
|
|
417
414
|
return SYSTEM_FIELDS.has(key) || value === void 0;
|
|
418
415
|
}
|
|
419
416
|
function convertStringValue(value, field) {
|
|
420
|
-
var _a;
|
|
421
417
|
if (value === "") {
|
|
422
418
|
return null;
|
|
423
419
|
}
|
|
424
|
-
if (
|
|
420
|
+
if (field?.type.toLowerCase() === "datetime" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) {
|
|
425
421
|
try {
|
|
426
422
|
const date = new Date(value);
|
|
427
423
|
return date.toISOString();
|
|
@@ -431,7 +427,7 @@ function convertStringValue(value, field) {
|
|
|
431
427
|
}
|
|
432
428
|
if (value === "true") return true;
|
|
433
429
|
if (value === "false") return false;
|
|
434
|
-
const fieldType =
|
|
430
|
+
const fieldType = field?.type?.toLowerCase();
|
|
435
431
|
const isNumericField = fieldType && ["int", "bigint", "float", "decimal"].includes(fieldType);
|
|
436
432
|
if (isNumericField) {
|
|
437
433
|
const numericPattern = /^-?\d+(\.\d+)?$/;
|
|
@@ -449,16 +445,15 @@ function processNestedObject(value, model) {
|
|
|
449
445
|
return Object.keys(cleanedNested).length > 0 ? cleanedNested : null;
|
|
450
446
|
}
|
|
451
447
|
function cleanFormInput(input, model) {
|
|
452
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
453
448
|
const cleaned = {};
|
|
454
449
|
const booleanFields = new Set(
|
|
455
|
-
|
|
450
|
+
model?.fields?.filter((field) => field.type.toLowerCase() === "boolean")?.map((field) => field.name) || []
|
|
456
451
|
);
|
|
457
452
|
const requiredArrayFields = new Set(
|
|
458
|
-
|
|
453
|
+
model?.fields?.filter((field) => field.isList && !field.isOptional && !field.relationName)?.map((field) => field.name) || []
|
|
459
454
|
);
|
|
460
455
|
const enumArrayFields = new Set(
|
|
461
|
-
|
|
456
|
+
model?.fields?.filter((field) => field.isList && field.kind === "enum")?.map((field) => field.name) || []
|
|
462
457
|
);
|
|
463
458
|
for (const [key, value] of Object.entries(input)) {
|
|
464
459
|
if (booleanFields.has(key) && value === void 0) {
|
|
@@ -486,7 +481,7 @@ function cleanFormInput(input, model) {
|
|
|
486
481
|
continue;
|
|
487
482
|
}
|
|
488
483
|
if (typeof value === "string") {
|
|
489
|
-
const field =
|
|
484
|
+
const field = model?.fields?.find((f) => f.name === key);
|
|
490
485
|
const convertedValue = convertStringValue(value, field);
|
|
491
486
|
cleaned[key] = convertedValue;
|
|
492
487
|
continue;
|
|
@@ -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",
|
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
"registry": "https://registry.npmjs.org/"
|
|
54
54
|
},
|
|
55
55
|
"type": "module"
|
|
56
|
-
}
|
|
56
|
+
}
|