@airoom/nextmin-react 2.0.1 → 2.0.3

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.
Files changed (54) hide show
  1. package/README.md +57 -0
  2. package/dist/components/AddressAutocomplete.d.ts +4 -1
  3. package/dist/components/AddressAutocomplete.js +2 -2
  4. package/dist/components/AdminApp.js +86 -1
  5. package/dist/components/FileUploader.d.ts +3 -0
  6. package/dist/components/FileUploader.js +84 -28
  7. package/dist/components/PasswordInput.d.ts +2 -0
  8. package/dist/components/PasswordInput.js +2 -2
  9. package/dist/components/PhoneInput.d.ts +2 -0
  10. package/dist/components/PhoneInput.js +2 -2
  11. package/dist/components/QRCodeModal.d.ts +10 -0
  12. package/dist/components/QRCodeModal.js +153 -0
  13. package/dist/components/RefSelect.d.ts +4 -1
  14. package/dist/components/RefSelect.js +163 -65
  15. package/dist/components/SchemaForm.d.ts +2 -1
  16. package/dist/components/SchemaForm.js +132 -55
  17. package/dist/components/Sidebar.js +96 -7
  18. package/dist/components/TableFilters.js +1 -1
  19. package/dist/components/editor/TiptapEditor.js +46 -13
  20. package/dist/components/editor/Toolbar.js +195 -62
  21. package/dist/components/editor/components/CustomFontSizeModal.d.ts +13 -0
  22. package/dist/components/editor/components/CustomFontSizeModal.js +81 -0
  23. package/dist/components/editor/components/DistrictGridModal.js +1 -1
  24. package/dist/components/editor/components/SchemaInsertionModal.js +1 -1
  25. package/dist/components/editor/editor.css +2 -0
  26. package/dist/components/editor/extensions/FontSize.d.ts +19 -0
  27. package/dist/components/editor/extensions/FontSize.js +41 -0
  28. package/dist/components/viewer/DynamicViewer.js +50 -15
  29. package/dist/components/viewer/useViewDrawer.js +42 -2
  30. package/dist/hooks/useAppNavigation.d.ts +3 -0
  31. package/dist/hooks/useAppNavigation.js +37 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +1 -0
  34. package/dist/lib/QueryBuilder.d.ts +3 -1
  35. package/dist/lib/QueryBuilder.js +10 -0
  36. package/dist/lib/api.d.ts +6 -0
  37. package/dist/lib/api.js +45 -24
  38. package/dist/lib/schemaUtils.js +1 -1
  39. package/dist/lib/types.d.ts +6 -1
  40. package/dist/nextmin.css +1 -1
  41. package/dist/providers/NextMinProvider.d.ts +5 -1
  42. package/dist/providers/NextMinProvider.js +12 -2
  43. package/dist/providers/themeResolver.d.ts +25 -0
  44. package/dist/providers/themeResolver.js +174 -0
  45. package/dist/router/NextMinRouter.d.ts +0 -1
  46. package/dist/router/NextMinRouter.js +0 -1
  47. package/dist/views/CreateEditPage.js +20 -11
  48. package/dist/views/DashboardPage.js +1 -1
  49. package/dist/views/ListPage.js +37 -6
  50. package/dist/views/SettingsEdit.js +1 -1
  51. package/dist/views/list/DataTableHero.d.ts +5 -5
  52. package/dist/views/list/DataTableHero.js +154 -41
  53. package/dist/views/list/ListHeader.js +9 -1
  54. package/package.json +5 -2
@@ -1,12 +1,16 @@
1
1
  import React from 'react';
2
2
  import { NextMinClient } from '../lib/api';
3
+ import { ThemeConfig } from './themeResolver';
3
4
  export interface NextMinContextType {
4
5
  client: NextMinClient;
6
+ theme: ThemeConfig | null;
7
+ navigate?: (url: string) => void;
5
8
  }
6
9
  export declare function useNextMin(): NextMinContextType;
7
- export declare function NextMinProvider({ children, navigate, apiUrl, apiKey }: {
10
+ export declare function NextMinProvider({ children, navigate, apiUrl, apiKey, theme }: {
8
11
  children: React.ReactNode;
9
12
  navigate?: (url: string) => void;
10
13
  apiUrl?: string;
11
14
  apiKey?: string;
15
+ theme?: ThemeConfig;
12
16
  }): import("react/jsx-runtime").JSX.Element;
@@ -7,6 +7,7 @@ import { nextminstore } from '../state/store';
7
7
  import { fetchSchemas, setSchemas } from '../state/schemasSlice';
8
8
  import { RealtimeClient } from '../lib/RealtimeClient';
9
9
  import { AuthClient } from '../lib/AuthClient';
10
+ import { loadThemeConfigServer } from './themeResolver';
10
11
  function Bootstrapper() {
11
12
  const dispatch = useDispatch();
12
13
  const status = useSelector((s) => s.schemas.status);
@@ -29,6 +30,7 @@ function Bootstrapper() {
29
30
  return null;
30
31
  }
31
32
  import { NextMinClient } from '../lib/api';
33
+ import { getThemeConfig } from './themeResolver';
32
34
  const NextMinContext = React.createContext(undefined);
33
35
  export function useNextMin() {
34
36
  const context = React.useContext(NextMinContext);
@@ -36,7 +38,7 @@ export function useNextMin() {
36
38
  throw new Error('useNextMin must be used within NextMinProvider');
37
39
  return context;
38
40
  }
39
- export function NextMinProvider({ children, navigate, apiUrl, apiKey }) {
41
+ export function NextMinProvider({ children, navigate, apiUrl, apiKey, theme }) {
40
42
  const client = React.useMemo(() => {
41
43
  if (apiUrl)
42
44
  AuthClient.setBaseURL(apiUrl);
@@ -55,5 +57,13 @@ export function NextMinProvider({ children, navigate, apiUrl, apiKey }) {
55
57
  apiKey: apiKey
56
58
  });
57
59
  }, [apiUrl, apiKey]);
58
- return (_jsx(Provider, { store: nextminstore, children: _jsx(NextMinContext.Provider, { value: { client }, children: _jsxs(HeroUIProvider, { navigate: navigate, children: [_jsx(Bootstrapper, {}), children] }) }) }));
60
+ const themeConfig = loadThemeConfigServer();
61
+ const resolvedTheme = React.useMemo(() => {
62
+ if (theme)
63
+ return theme;
64
+ return getThemeConfig();
65
+ }, [theme]);
66
+ return (_jsxs(Provider, { store: nextminstore, children: [themeConfig && !theme && (_jsx("script", { id: "nextmin-theme-state", dangerouslySetInnerHTML: {
67
+ __html: `window.__NEXTMIN_THEME__ = ${JSON.stringify(themeConfig)};`,
68
+ } })), _jsx(NextMinContext.Provider, { value: { client, theme: resolvedTheme, navigate }, children: _jsxs(HeroUIProvider, { navigate: navigate, children: [_jsx(Bootstrapper, {}), children] }) })] }));
59
69
  }
@@ -0,0 +1,25 @@
1
+ export interface ThemeConfig {
2
+ mainAdminBgColor?: string;
3
+ sidebarColor?: string;
4
+ activeMenuBgColor?: string;
5
+ activeMenuTextColor?: string;
6
+ menuTextColor?: string;
7
+ menuHoverBgColor?: string;
8
+ menuHoverTextColor?: string;
9
+ sidebarTextColor?: string;
10
+ sidebarBorderColor?: string;
11
+ topNavBgColor?: string;
12
+ topNavHeight?: string | number;
13
+ inputHeight?: string | number;
14
+ inputRadius?: string | number;
15
+ sidebarWidth?: string | number;
16
+ sidebarPosition?: 'left' | 'right' | (string & {});
17
+ primaryColor?: string;
18
+ }
19
+ export declare function isTailwindClass(val: unknown): boolean;
20
+ export declare function resolveHeightValue(val: unknown): string | undefined;
21
+ export declare function resolveRadiusValue(val: unknown): string | undefined;
22
+ export declare function resolveSidebarWidth(val: unknown): string | undefined;
23
+ export declare function hexToHslComponents(hex: string): string | null;
24
+ export declare function loadThemeConfigServer(): ThemeConfig | null;
25
+ export declare function getThemeConfig(): ThemeConfig | null;
@@ -0,0 +1,174 @@
1
+ 'use client';
2
+ const tailwindHeights = {
3
+ 'h-6': '1.5rem',
4
+ 'h-7': '1.75rem',
5
+ 'h-8': '2rem',
6
+ 'h-9': '2.25rem',
7
+ 'h-10': '2.5rem',
8
+ 'h-11': '2.75rem',
9
+ 'h-12': '3rem',
10
+ 'h-14': '3.5rem',
11
+ 'h-16': '4rem',
12
+ 'h-20': '5rem',
13
+ 'h-24': '6rem',
14
+ };
15
+ const tailwindRadii = {
16
+ 'rounded-none': '0px',
17
+ 'rounded-sm': '0.125rem',
18
+ 'rounded': '0.25rem',
19
+ 'rounded-md': '0.375rem',
20
+ 'rounded-lg': '0.5rem',
21
+ 'rounded-xl': '0.75rem',
22
+ 'rounded-2xl': '1rem',
23
+ 'rounded-3xl': '1.5rem',
24
+ 'rounded-full': '9999px',
25
+ };
26
+ export function isTailwindClass(val) {
27
+ if (typeof val !== 'string')
28
+ return false;
29
+ const s = val.trim();
30
+ return (s.startsWith('bg-') ||
31
+ s.startsWith('text-') ||
32
+ s.startsWith('border-') ||
33
+ s.startsWith('h-') ||
34
+ s.startsWith('w-') ||
35
+ s.startsWith('rounded-') ||
36
+ s.includes(':') ||
37
+ s.startsWith('from-') ||
38
+ s.startsWith('to-'));
39
+ }
40
+ export function resolveHeightValue(val) {
41
+ if (typeof val === 'number')
42
+ return `${val}px`;
43
+ if (typeof val === 'string') {
44
+ const s = val.trim();
45
+ const match = s.match(/^h-\[(.+)\]$/);
46
+ if (match)
47
+ return match[1];
48
+ if (s.startsWith('h-'))
49
+ return tailwindHeights[s] || undefined;
50
+ if (/^\d+$/.test(s))
51
+ return `${s}px`;
52
+ return s;
53
+ }
54
+ return undefined;
55
+ }
56
+ export function resolveRadiusValue(val) {
57
+ if (typeof val === 'number')
58
+ return `${val}px`;
59
+ if (typeof val === 'string') {
60
+ const s = val.trim();
61
+ const match = s.match(/^rounded-\[(.+)\]$/);
62
+ if (match)
63
+ return match[1];
64
+ if (s.startsWith('rounded-'))
65
+ return tailwindRadii[s] || undefined;
66
+ if (/^\d+$/.test(s))
67
+ return `${s}px`;
68
+ return s;
69
+ }
70
+ return undefined;
71
+ }
72
+ export function resolveSidebarWidth(val) {
73
+ if (typeof val === 'number')
74
+ return `${val}px`;
75
+ if (typeof val === 'string') {
76
+ const s = val.trim();
77
+ const match = s.match(/^w-\[(.+)\]$/);
78
+ if (match)
79
+ return match[1];
80
+ if (/^\d+$/.test(s))
81
+ return `${s}px`;
82
+ return s;
83
+ }
84
+ return undefined;
85
+ }
86
+ export function hexToHslComponents(hex) {
87
+ let cleanHex = hex.replace(/[^0-9a-fA-F]/g, '');
88
+ if (cleanHex.length === 3) {
89
+ cleanHex = cleanHex[0] + cleanHex[0] + cleanHex[1] + cleanHex[1] + cleanHex[2] + cleanHex[2];
90
+ }
91
+ if (cleanHex.length !== 6 && cleanHex.length !== 8) {
92
+ return null;
93
+ }
94
+ const r = parseInt(cleanHex.slice(0, 2), 16) / 255;
95
+ const g = parseInt(cleanHex.slice(2, 4), 16) / 255;
96
+ const b = parseInt(cleanHex.slice(4, 6), 16) / 255;
97
+ const max = Math.max(r, g, b);
98
+ const min = Math.min(r, g, b);
99
+ let h = 0;
100
+ let s = 0;
101
+ const l = (max + min) / 2;
102
+ if (max !== min) {
103
+ const d = max - min;
104
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
105
+ if (max === r) {
106
+ h = (g - b) / d + (g < b ? 6 : 0);
107
+ }
108
+ else if (max === g) {
109
+ h = (b - r) / d + 2;
110
+ }
111
+ else if (max === b) {
112
+ h = (r - g) / d + 4;
113
+ }
114
+ h /= 6;
115
+ }
116
+ return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
117
+ }
118
+ function parseThemeFile(content) {
119
+ try {
120
+ const firstBrace = content.indexOf('{');
121
+ const lastBrace = content.lastIndexOf('}');
122
+ if (firstBrace === -1 || lastBrace === -1)
123
+ return null;
124
+ const jsonLike = content.slice(firstBrace, lastBrace + 1);
125
+ // Evaluate it safely using Function constructor
126
+ const fn = new Function(`return (${jsonLike})`);
127
+ return fn();
128
+ }
129
+ catch (e) {
130
+ console.error('Error parsing nextmin-theme config:', e);
131
+ return null;
132
+ }
133
+ }
134
+ let serverLoadedConfig = null;
135
+ let hasAttemptedServerLoad = false;
136
+ export function loadThemeConfigServer() {
137
+ if (typeof window !== 'undefined')
138
+ return null;
139
+ const isDev = process.env.NODE_ENV === 'development';
140
+ if (!isDev && hasAttemptedServerLoad)
141
+ return serverLoadedConfig;
142
+ hasAttemptedServerLoad = true;
143
+ try {
144
+ const r = eval('require');
145
+ const p = r('path');
146
+ const f = r('fs');
147
+ const cwd = process.cwd();
148
+ const tsPath = p.join(cwd, 'nextmin-theme.ts');
149
+ const jsPath = p.join(cwd, 'nextmin-theme.js');
150
+ let content = '';
151
+ if (f.existsSync(tsPath)) {
152
+ content = f.readFileSync(tsPath, 'utf-8');
153
+ }
154
+ else if (f.existsSync(jsPath)) {
155
+ content = f.readFileSync(jsPath, 'utf-8');
156
+ }
157
+ if (content) {
158
+ serverLoadedConfig = parseThemeFile(content);
159
+ }
160
+ else {
161
+ serverLoadedConfig = null; // Clear if file was removed
162
+ }
163
+ }
164
+ catch (err) {
165
+ // Ignore server-side loading errors
166
+ }
167
+ return serverLoadedConfig;
168
+ }
169
+ export function getThemeConfig() {
170
+ if (typeof window !== 'undefined') {
171
+ return window.__NEXTMIN_THEME__ || null;
172
+ }
173
+ return loadThemeConfigServer();
174
+ }
@@ -1,2 +1 @@
1
- import '../components/editor/editor.css';
2
1
  export declare function NextMinRouter(): import("react/jsx-runtime").JSX.Element;
@@ -10,7 +10,6 @@ import { DashboardPage } from '../views/DashboardPage';
10
10
  import { SectionLoader } from '../components/SectionLoader';
11
11
  import SettingsEdit from '../views/SettingsEdit';
12
12
  import ProfilePage from '../views/ProfilePage';
13
- import '../components/editor/editor.css';
14
13
  export function NextMinRouter() {
15
14
  const pathname = usePathname();
16
15
  const router = useRouter();
@@ -1,25 +1,31 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
4
  import { useSelector } from 'react-redux';
6
5
  import { api } from '../lib/api';
7
6
  import { SchemaForm } from '../components/SchemaForm';
8
- import Link from 'next/link';
9
7
  import { Button, Divider } from '@heroui/react';
8
+ import { useAppNavigation } from '../hooks/useAppNavigation';
10
9
  function extractFieldErrors(err) {
11
- // Try common shapes: axios error -> err.response.data.fields
12
- const data = err?.response?.data ?? err?.data ?? err;
13
- const fields = Array.isArray(data?.fields) ? data.fields : [];
14
- return fields
10
+ // Try common shapes: ApiError -> err.info.fields, Axios -> err.response.data.fields, etc.
11
+ const data = err?.info ?? err?.response?.data ?? err?.data ?? err;
12
+ const rawFields = Array.isArray(data?.fields)
13
+ ? data.fields
14
+ : Array.isArray(err?.fields)
15
+ ? err.fields
16
+ : Array.isArray(err?.info)
17
+ ? err.info
18
+ : [];
19
+ return rawFields
15
20
  .filter((f) => f && typeof f === 'object')
16
21
  .map((f) => ({
22
+ error: Boolean(f.error ?? true),
17
23
  field: String(f.field ?? ''),
18
24
  message: String(f.message ?? 'Invalid value'),
19
25
  }));
20
26
  }
21
27
  export function CreateEditPage({ model, id }) {
22
- const router = useRouter();
28
+ const navigateTo = useAppNavigation();
23
29
  const { items } = useSelector((s) => s.schemas);
24
30
  const schema = useMemo(() => items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model]);
25
31
  const [initialValues, setInitialValues] = useState({});
@@ -50,6 +56,9 @@ export function CreateEditPage({ model, id }) {
50
56
  cancelled = true;
51
57
  };
52
58
  }, [id, model]);
59
+ const navigateToList = () => {
60
+ navigateTo(`/admin/${model}`);
61
+ };
53
62
  const handleSubmit = async (values) => {
54
63
  setBusy(true);
55
64
  setError(undefined);
@@ -61,11 +70,11 @@ export function CreateEditPage({ model, id }) {
61
70
  else {
62
71
  await api.create(model, values);
63
72
  }
64
- router.push(`/admin/${model}`);
65
- router.refresh();
73
+ navigateToList();
66
74
  }
67
75
  catch (e) {
68
- setFieldErrors(e.fields ?? []);
76
+ const extracted = extractFieldErrors(e);
77
+ setFieldErrors(extracted);
69
78
  setError(e?.message || 'Save failed');
70
79
  }
71
80
  finally {
@@ -74,5 +83,5 @@ export function CreateEditPage({ model, id }) {
74
83
  };
75
84
  if (!schema)
76
85
  return _jsx("div", { className: "text-danger text-sm", children: "Schema not found" });
77
- return (_jsxs("div", { className: "grid gap-4 px-4", children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsx("h2", { className: "m-0 text-xl font-semibold", children: id ? `Edit ${schema.modelName}` : `Create ${schema.modelName}` }), _jsx("div", { className: "flex items-center gap-3", children: _jsx(Button, { as: Link, href: `/admin/${model}`, size: "sm", variant: "flat", color: "danger", isDisabled: busy, children: "Cancel" }) })] }), _jsx(Divider, { className: "my-3" }), error && _jsx("div", { className: "text-danger text-sm", children: error }), fieldErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-danger-200 bg-danger-50 px-3 py-2 text-warning-800 text-sm", children: _jsx("ul", { className: "list-disc pl-5", children: fieldErrors.map((fe, idx) => (_jsxs("li", { children: [_jsx("span", { className: "font-medium", children: fe.field }), ": ", fe.message] }, `${fe.field}-${idx}`))) }) })), _jsx(SchemaForm, { model: model, schemaOverride: schema, initialValues: initialValues, submitLabel: id ? 'Save' : 'Create', busy: busy, onSubmit: handleSubmit, fieldErrors: fieldErrors })] }));
86
+ return (_jsxs("div", { className: "grid gap-4 px-4", children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsx("h2", { className: "m-0 text-xl font-semibold", children: id ? `Edit ${schema.modelName}` : `Create ${schema.modelName}` }), _jsx("div", { className: "flex items-center gap-3", children: _jsx(Button, { size: "sm", variant: "flat", color: "danger", isDisabled: busy, onPress: navigateToList, children: "Cancel" }) })] }), _jsx(Divider, { className: "my-3" }), error && _jsx("div", { className: "text-danger text-sm", children: error }), fieldErrors.length > 0 && (_jsx("div", { className: "rounded-md border border-danger-200 bg-danger-50 px-3 py-2 text-warning-800 text-sm", children: _jsx("ul", { className: "list-disc pl-5", children: fieldErrors.map((fe, idx) => (_jsxs("li", { children: [_jsx("span", { className: "font-medium", children: fe.field }), ": ", fe.message] }, `${fe.field}-${idx}`))) }) })), _jsx(SchemaForm, { model: model, schemaOverride: schema, initialValues: initialValues, submitLabel: id ? 'Save' : 'Create', busy: busy, onSubmit: handleSubmit, fieldErrors: fieldErrors, isEdit: !!id })] }));
78
87
  }
@@ -38,7 +38,7 @@ export function DashboardPage() {
38
38
  if (!c.model)
39
39
  return [c.key, null];
40
40
  try {
41
- const res = await api.list(c.model, { page: 1, limit: 1 });
41
+ const res = await api.list(c.model, { page: 1, limit: 1, select: 'id' });
42
42
  const total = res?.pagination?.totalRows ?? res?.data?.length ?? 0;
43
43
  return [c.key, total];
44
44
  }
@@ -13,6 +13,8 @@ export function ListPage({ model }) {
13
13
  const [page, setPage] = useState(1); // 1-based
14
14
  const [pageSize, setPageSize] = useState(10);
15
15
  const [filters, setFilters] = useState({});
16
+ const [sort, setSort] = useState('createdAt');
17
+ const [sortType, setSortType] = useState('desc');
16
18
  // schema
17
19
  const { items } = useSelector((s) => s.schemas);
18
20
  const schema = useMemo(() => items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model]);
@@ -40,12 +42,9 @@ export function ListPage({ model }) {
40
42
  setPage(1);
41
43
  setPageSize(10);
42
44
  setFilters({});
45
+ setSort('createdAt');
46
+ setSortType('desc');
43
47
  }, [model]);
44
- // data
45
- const [{ rows, total, loading, forbidden, err }, refetch] = useListData(model, page, pageSize, {
46
- q: filters.q,
47
- searchKey: filters.searchKey,
48
- });
49
48
  // columns (ALWAYS call hooks; derive from schema or empty object)
50
49
  const attributes = (viewSchema?.attributes ?? {});
51
50
  // Generic password detectors (hide from table columns as well)
@@ -117,6 +116,35 @@ export function ListPage({ model }) {
117
116
  }
118
117
  return visible;
119
118
  }, [allColumns, visibleColumns, schema]);
119
+ const selectProjection = useMemo(() => {
120
+ const keys = new Set(['id', '_id', ...tableColumns]);
121
+ if (attributes?.slug) {
122
+ keys.add('slug');
123
+ }
124
+ if (schema?.actions && Array.isArray(schema.actions)) {
125
+ for (const act of schema.actions) {
126
+ const str = String(act?.url || act?.urlTemplate || act.href || '');
127
+ const matches = str.match(/\{([a-zA-Z0-9_]+)\}/g);
128
+ if (matches) {
129
+ for (const m of matches) {
130
+ const field = m.replace(/[{}]/g, '');
131
+ if (attributes[field]) {
132
+ keys.add(field);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ return Array.from(keys).join(',');
139
+ }, [tableColumns, attributes, schema]);
140
+ // data
141
+ const [{ rows, total, loading, forbidden, err }, refetch] = useListData(model, page, pageSize, {
142
+ q: filters.q,
143
+ searchKey: filters.searchKey,
144
+ sort,
145
+ sortType,
146
+ select: selectProjection,
147
+ });
120
148
  const baseHref = useMemo(() => `/admin/${model}`, [model]);
121
149
  const handleRefetch = useCallback(async () => {
122
150
  await refetch();
@@ -154,7 +182,10 @@ export function ListPage({ model }) {
154
182
  schema: viewSchema,
155
183
  data: dataToView,
156
184
  });
185
+ }, sort: sort, sortType: sortType, onSortChange: (col, dir) => {
186
+ setSort(col);
187
+ setSortType(dir);
157
188
  } })] }));
158
189
  }
159
- return (_jsxs("div", { className: "grid gap-3 px-4", children: [_jsx(ListHeader, { title: schema?.modelName ?? model, createHref: `${baseHref}/create`, loading: loading }), content] }));
190
+ return (_jsxs("div", { className: "grid gap-3 px-4", children: [_jsx(ListHeader, { title: schema?.modelName ?? model, createHref: `${baseHref}/create`, hideCreate: !schema?.allowedMethods?.create, loading: loading }), content] }));
160
191
  }
@@ -105,6 +105,6 @@ export function SettingsEdit() {
105
105
  return (_jsxs("div", { className: "px-4", children: [_jsx(ListHeader, { title: title, createHref: "", loading: false }), _jsx(NoAccess, { message: forbidden })] }));
106
106
  }
107
107
  const hasCleanupData = cleanupReport && Object.keys(cleanupReport).length > 0;
108
- return (_jsxs("div", { className: "grid gap-3 px-4", children: [_jsx(ListHeader, { title: title, hideCreate: true, createHref: "", loading: loading || saving || cleaning, children: _jsx(Button, { size: "sm", color: "danger", variant: "flat", isLoading: cleaning, onPress: handleCleanup, children: "Cleanup Database" }) }), error && _jsx("div", { className: "text-danger text-sm", children: error }), hasCleanupData && (_jsxs("div", { className: "p-3 bg-success-50 rounded-lg border border-success-200", children: [_jsx("div", { className: "font-semibold text-success-700 text-sm mb-1", children: "Cleanup Summary:" }), _jsx("ul", { className: "text-xs text-success-600 list-disc list-inside", children: Object.entries(cleanupReport).map(([model, fields]) => (_jsxs("li", { children: [_jsx("span", { className: "font-medium", children: model }), ": ", fields.join(', ')] }, model))) })] })), !error && (_jsx(SchemaForm, { model: MODEL, schemaOverride: schema, initialValues: doc ?? undefined, submitLabel: "Save", busy: loading || saving || cleaning, onSubmit: handleSubmit, showReset: false }))] }));
108
+ return (_jsxs("div", { className: "grid gap-3 px-4", children: [_jsx(ListHeader, { title: title, hideCreate: true, createHref: "", loading: loading || saving || cleaning, children: _jsx(Button, { size: "sm", color: "danger", variant: "flat", isLoading: cleaning, onPress: handleCleanup, children: "Cleanup Database" }) }), error && _jsx("div", { className: "text-danger text-sm", children: error }), hasCleanupData && (_jsxs("div", { className: "p-3 bg-success-50 rounded-lg border border-success-200", children: [_jsx("div", { className: "font-semibold text-success-700 text-sm mb-1", children: "Cleanup Summary:" }), _jsx("ul", { className: "text-xs text-success-600 list-disc list-inside", children: Object.entries(cleanupReport).map(([model, fields]) => (_jsxs("li", { children: [_jsx("span", { className: "font-medium", children: model }), ": ", fields.join(', ')] }, model))) })] })), !error && (_jsx(SchemaForm, { model: MODEL, schemaOverride: schema, initialValues: doc ?? undefined, submitLabel: "Save", busy: loading || saving || cleaning, onSubmit: handleSubmit, showReset: false, isEdit: true }))] }));
109
109
  }
110
110
  export default SettingsEdit;
@@ -13,15 +13,15 @@ type Props = {
13
13
  baseHref: string;
14
14
  loading?: boolean;
15
15
  error?: string | null;
16
- /** optional hook so parent can refetch or optimistically remove */
17
- onDeleted?: (id: string) => void;
18
- /** optional schema for dynamic viewing */
16
+ onDeleted?: (id?: string) => void;
19
17
  schema?: SchemaDef;
20
- /** request to view a row in a drawer */
21
18
  onRequestView?: (row: any) => void;
19
+ sort?: string;
20
+ sortType?: 'asc' | 'desc';
21
+ onSortChange?: (field: string, direction: 'asc' | 'desc') => void;
22
22
  };
23
23
  /** ---------- truncation helper ---------- */
24
24
  export declare function truncateText(text: unknown, maxLength?: number): string;
25
25
  /** ---------- component ---------- */
26
- export declare function DataTableHero({ modelName, columns, rows, total, page, pageSize, onPageChange, onPageSizeChange, baseHref, loading, error, onDeleted, topContent, schema, onRequestView, }: Props): import("react/jsx-runtime").JSX.Element;
26
+ export declare function DataTableHero({ modelName, columns, rows, total, page, pageSize, onPageChange, onPageSizeChange, baseHref, loading, error, onDeleted, topContent, schema, onRequestView, sort, sortType, onSortChange, }: Props): import("react/jsx-runtime").JSX.Element;
27
27
  export {};