@airoom/nextmin-react 2.0.2 → 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.
- package/README.md +57 -0
- package/dist/components/AddressAutocomplete.d.ts +4 -1
- package/dist/components/AddressAutocomplete.js +2 -2
- package/dist/components/AdminApp.js +86 -1
- package/dist/components/FileUploader.d.ts +3 -0
- package/dist/components/FileUploader.js +84 -28
- package/dist/components/PasswordInput.d.ts +2 -0
- package/dist/components/PasswordInput.js +2 -2
- package/dist/components/PhoneInput.d.ts +2 -0
- package/dist/components/PhoneInput.js +2 -2
- package/dist/components/QRCodeModal.d.ts +10 -0
- package/dist/components/QRCodeModal.js +153 -0
- package/dist/components/RefSelect.d.ts +4 -1
- package/dist/components/RefSelect.js +163 -65
- package/dist/components/SchemaForm.d.ts +2 -1
- package/dist/components/SchemaForm.js +132 -55
- package/dist/components/Sidebar.js +96 -7
- package/dist/components/TableFilters.js +1 -1
- package/dist/components/editor/TiptapEditor.js +43 -12
- package/dist/components/editor/Toolbar.js +195 -70
- package/dist/components/editor/components/CustomFontSizeModal.d.ts +13 -0
- package/dist/components/editor/components/CustomFontSizeModal.js +81 -0
- package/dist/components/editor/components/DistrictGridModal.js +1 -1
- package/dist/components/editor/components/SchemaInsertionModal.js +1 -1
- package/dist/components/editor/editor.css +2 -0
- package/dist/components/editor/extensions/FontSize.d.ts +19 -0
- package/dist/components/editor/extensions/FontSize.js +41 -0
- package/dist/components/viewer/DynamicViewer.js +50 -15
- package/dist/components/viewer/useViewDrawer.js +42 -2
- package/dist/hooks/useAppNavigation.d.ts +3 -0
- package/dist/hooks/useAppNavigation.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/lib/QueryBuilder.d.ts +3 -1
- package/dist/lib/QueryBuilder.js +10 -0
- package/dist/lib/api.d.ts +6 -0
- package/dist/lib/api.js +45 -24
- package/dist/lib/schemaUtils.js +1 -1
- package/dist/lib/types.d.ts +6 -1
- package/dist/nextmin.css +1 -1
- package/dist/providers/NextMinProvider.d.ts +5 -1
- package/dist/providers/NextMinProvider.js +12 -2
- package/dist/providers/themeResolver.d.ts +25 -0
- package/dist/providers/themeResolver.js +174 -0
- package/dist/router/NextMinRouter.d.ts +0 -1
- package/dist/router/NextMinRouter.js +0 -1
- package/dist/views/CreateEditPage.js +20 -11
- package/dist/views/DashboardPage.js +1 -1
- package/dist/views/ListPage.js +37 -6
- package/dist/views/SettingsEdit.js +1 -1
- package/dist/views/list/DataTableHero.d.ts +5 -5
- package/dist/views/list/DataTableHero.js +154 -41
- package/dist/views/list/ListHeader.js +9 -1
- package/package.json +4 -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
|
-
|
|
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
|
+
}
|
|
@@ -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:
|
|
12
|
-
const data = err?.response?.data ?? err?.data ?? err;
|
|
13
|
-
const
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
65
|
-
router.refresh();
|
|
73
|
+
navigateToList();
|
|
66
74
|
}
|
|
67
75
|
catch (e) {
|
|
68
|
-
|
|
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, {
|
|
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
|
}
|
package/dist/views/ListPage.js
CHANGED
|
@@ -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
|
-
|
|
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 {};
|