@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 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 (_a = filters[relationName]) == null ? void 0 : _a[enumFieldName];
28
+ return filters[relationName]?.[enumFieldName];
30
29
  }
31
30
  function BooleanFilter({ fieldName, currentValue, onChange }) {
32
31
  return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
@@ -18,7 +18,7 @@ function RelationFieldWrapper({
18
18
  }
19
19
  if (fieldName) {
20
20
  const selectElement = document.querySelector(`[name="${fieldName}"]`);
21
- if ((selectElement == null ? void 0 : selectElement.value) && selectElement.value !== currentValue) {
21
+ if (selectElement?.value && selectElement.value !== currentValue) {
22
22
  setCurrentValue(selectElement.value);
23
23
  }
24
24
  if (selectElement) {
@@ -5,8 +5,8 @@ function DateRangeFilter({
5
5
  currentValue,
6
6
  onChange
7
7
  }) {
8
- const fromDate = (currentValue == null ? void 0 : currentValue.gte) ? new Date(currentValue.gte).toISOString().split("T")[0] : "";
9
- const toDate = (currentValue == null ? void 0 : currentValue.lte) ? new Date(currentValue.lte).toISOString().split("T")[0] : "";
8
+ const fromDate = currentValue?.gte ? new Date(currentValue.gte).toISOString().split("T")[0] : "";
9
+ const toDate = currentValue?.lte ? new Date(currentValue.lte).toISOString().split("T")[0] : "";
10
10
  const handleFromChange = (date) => {
11
11
  const newValue = { ...currentValue };
12
12
  if (date) {
@@ -6,8 +6,8 @@ function NumberRangeFilter({
6
6
  currentValue,
7
7
  onChange
8
8
  }) {
9
- const minValue = (currentValue == null ? void 0 : currentValue.gte) === void 0 ? "" : currentValue.gte.toString();
10
- const maxValue = (currentValue == null ? void 0 : currentValue.lte) === void 0 ? "" : currentValue.lte.toString();
9
+ const minValue = currentValue?.gte === void 0 ? "" : currentValue.gte.toString();
10
+ const maxValue = currentValue?.lte === void 0 ? "" : currentValue.lte.toString();
11
11
  const parseNumber = (value) => {
12
12
  if (!value) return void 0;
13
13
  if (fieldType === "int" || fieldType === "bigint") {
@@ -21,12 +21,12 @@ function RelationFilterField({
21
21
  relatedModelName,
22
22
  searchTerm,
23
23
  isOpen,
24
- currentValue == null ? void 0 : currentValue.id
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
- () => (currentValue == null ? void 0 : currentValue.id) ? relatedItems.find((item) => item.id === currentValue.id) : null,
29
- [currentValue == null ? void 0 : currentValue.id, relatedItems]
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: (currentValue == null ? void 0 : currentValue.id) || "",
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"
@@ -1,3 +1,4 @@
1
1
  export * from './filters';
2
2
  export * from './shared';
3
3
  export * from './RelationFieldWrapper';
4
+ export * from './ExportButton';
@@ -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
- () => (relatedModel == null ? void 0 : relatedModel.pluralModelPropertyName) || relatedModelName.charAt(0).toLowerCase() + relatedModelName.slice(1) + "s",
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
- var _a;
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
- var _a;
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: (currentModel == null ? void 0 : currentModel.name) || "",
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
- var _a, _b;
87
- if (!(documents == null ? void 0 : documents.create)) return null;
86
+ if (!documents?.create) return null;
88
87
  try {
89
- if (((_a = documents.create) == null ? void 0 : _a.kind) === "Document" && ((_b = documents.create) == null ? void 0 : _b.definitions)) {
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
- var _a;
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] = (field == null ? void 0 : field.type.toLowerCase()) === "date" ? value.toISOString().split("T")[0] : value.toISOString();
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 = ((_a = field.relationFromFields) == null ? void 0 : _a[0]) || `${field.name}Id`;
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
- var _a, _b;
371
- if (!(documents == null ? void 0 : documents.query)) return null;
351
+ if (!documents?.query) return null;
372
352
  try {
373
- if (((_a = documents.query) == null ? void 0 : _a.kind) === "Document" && ((_b = documents.query) == null ? void 0 : _b.definitions)) {
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
- var _a, _b;
383
- if (!(documents == null ? void 0 : documents.update)) return null;
362
+ if (!documents?.update) return null;
384
363
  try {
385
- if (((_a = documents.update) == null ? void 0 : _a.kind) === "Document" && ((_b = documents.update) == null ? void 0 : _b.definitions)) {
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
- var _a, _b;
395
- if (!(documents == null ? void 0 : documents.delete)) return null;
373
+ if (!documents?.delete) return null;
396
374
  try {
397
- if (((_a = documents.delete) == null ? void 0 : _a.kind) === "Document" && ((_b = documents.delete) == null ? void 0 : _b.definitions)) {
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 == null ? void 0 : data[responseFieldName];
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
- window.scrollTo({ top: 0, behavior: "smooth" });
499
+ if (typeof window !== "undefined") {
500
+ window.scrollTo({ top: 0, behavior: "smooth" });
501
+ }
516
502
  if (result.success) {
517
- console.log(`[DataBrowser] Update successful, refetching data for ${model.name}...`);
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 || ((params == null ? void 0 : params.dataTypePlural) ?? "");
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 (model == null ? void 0 : model.pluralModelPropertyName) || pluralParam.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
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 == null ? void 0 : model.name, fieldNames, searchableFieldNames, getDefaultSearchFields, setVisibleColumns, setSearchFields, dispatch, urlFilters]);
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 (((_a = apolloError.graphQLErrors) == null ? void 0 : _a.length) > 0) ;
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 && ((_b = value[0]) == null ? void 0 : _b.id)) {
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 ((_a = foreignKeyField == null ? void 0 : foreignKeyField.relationFromFields) == null ? void 0 : _a[0]) || null;
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 = ((_a = field.relationFromFields) == null ? void 0 : _a[0]) || `${field.name}Id`;
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 == null ? void 0 : displayFieldConfig[field.type];
254
- const displayFields = (config == null ? void 0 : config.display) || ["name", "title"];
255
- const searchFields = (config == null ? void 0 : config.search) || displayFields;
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 == null ? void 0 : databaseModels.find((m) => m.name === field.type);
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 = (_a = currentItem._count) == null ? void 0 : _a[field.name];
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 ((field == null ? void 0 : field.type.toLowerCase()) === "datetime" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) {
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 = (_a = field == null ? void 0 : field.type) == null ? void 0 : _a.toLowerCase();
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
- ((_b = (_a = model == null ? void 0 : model.fields) == null ? void 0 : _a.filter((field) => field.type.toLowerCase() === "boolean")) == null ? void 0 : _b.map((field) => field.name)) || []
450
+ model?.fields?.filter((field) => field.type.toLowerCase() === "boolean")?.map((field) => field.name) || []
468
451
  );
469
452
  const requiredArrayFields = new Set(
470
- ((_d = (_c = model == null ? void 0 : model.fields) == null ? void 0 : _c.filter((field) => field.isList && !field.isOptional && !field.relationName)) == null ? void 0 : _d.map((field) => field.name)) || []
453
+ model?.fields?.filter((field) => field.isList && !field.isOptional && !field.relationName)?.map((field) => field.name) || []
471
454
  );
472
455
  const enumArrayFields = new Set(
473
- ((_f = (_e = model == null ? void 0 : model.fields) == null ? void 0 : _e.filter((field) => field.isList && field.kind === "enum")) == null ? void 0 : _f.map((field) => field.name)) || []
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 = (_g = model == null ? void 0 : model.fields) == null ? void 0 : _g.find((f) => f.name === key);
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 = (_a = config.models[sanitizedModelName]) == null ? void 0 : _a.visibleColumns;
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 = (_a = config.models[sanitizedModelName]) == null ? void 0 : _a.sortPreference;
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 = (_a = config.models[sanitizedModelName]) == null ? void 0 : _a.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.13",
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.13",
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
+ }