@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 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
  }
@@ -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 = ((_a = field.relationFromFields) == null ? void 0 : _a[0]) || `${field.name}Id`;
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
- var _a, _b;
354
- if (!(documents == null ? void 0 : documents.query)) return null;
351
+ if (!documents?.query) return null;
355
352
  try {
356
- 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) {
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
- var _a, _b;
366
- if (!(documents == null ? void 0 : documents.update)) return null;
362
+ if (!documents?.update) return null;
367
363
  try {
368
- 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) {
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
- var _a, _b;
378
- if (!(documents == null ? void 0 : documents.delete)) return null;
373
+ if (!documents?.delete) return null;
379
374
  try {
380
- 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) {
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 == null ? void 0 : data[responseFieldName];
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
- window.scrollTo({ top: 0, behavior: "smooth" });
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 || ((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") {
@@ -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 = ((_a = field.relationFromFields) == null ? void 0 : _a[0]) || `${field.name}Id`;
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 == null ? void 0 : displayFieldConfig[field.type];
243
- const displayFields = (config == null ? void 0 : config.display) || ["name", "title"];
244
- 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;
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 == null ? void 0 : databaseModels.find((m) => m.name === field.type);
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 = (_a = currentItem._count) == null ? void 0 : _a[field.name];
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 ((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)) {
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 = (_a = field == null ? void 0 : field.type) == null ? void 0 : _a.toLowerCase();
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
- ((_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) || []
456
451
  );
457
452
  const requiredArrayFields = new Set(
458
- ((_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) || []
459
454
  );
460
455
  const enumArrayFields = new Set(
461
- ((_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) || []
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 = (_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);
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 = (_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.14",
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
+ }