@airoom/nextmin-react 1.4.5 → 2.0.1
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 +29 -3
- package/dist/auth/SignInForm.js +4 -2
- package/dist/components/AdminApp.js +15 -38
- package/dist/components/ArchitectureDemo.d.ts +1 -0
- package/dist/components/ArchitectureDemo.js +45 -0
- package/dist/components/PhoneInput.d.ts +3 -0
- package/dist/components/PhoneInput.js +23 -19
- package/dist/components/RefSelect.d.ts +16 -0
- package/dist/components/RefSelect.js +225 -0
- package/dist/components/SchemaForm.js +131 -51
- package/dist/components/Sidebar.js +6 -13
- package/dist/components/TableFilters.js +2 -0
- package/dist/components/editor/TiptapEditor.js +1 -1
- package/dist/components/editor/Toolbar.js +13 -2
- package/dist/components/editor/components/DistrictGridModal.js +2 -3
- package/dist/components/editor/components/SchemaInsertionModal.js +2 -2
- package/dist/components/viewer/DynamicViewer.js +70 -9
- package/dist/hooks/useRealtime.d.ts +8 -0
- package/dist/hooks/useRealtime.js +30 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/AuthClient.d.ts +15 -0
- package/dist/lib/AuthClient.js +63 -0
- package/dist/lib/QueryBuilder.d.ts +29 -0
- package/dist/lib/QueryBuilder.js +74 -0
- package/dist/lib/RealtimeClient.d.ts +16 -0
- package/dist/lib/RealtimeClient.js +56 -0
- package/dist/lib/api.d.ts +15 -3
- package/dist/lib/api.js +71 -58
- package/dist/lib/auth.js +7 -2
- package/dist/lib/types.d.ts +16 -0
- package/dist/nextmin.css +1 -1
- package/dist/providers/NextMinProvider.d.ts +8 -1
- package/dist/providers/NextMinProvider.js +40 -8
- package/dist/router/NextMinRouter.d.ts +1 -1
- package/dist/router/NextMinRouter.js +1 -1
- package/dist/state/schemasSlice.js +8 -2
- package/dist/views/DashboardPage.js +56 -42
- package/dist/views/ListPage.js +34 -4
- package/dist/views/SettingsEdit.js +25 -2
- package/dist/views/list/DataTableHero.js +103 -46
- package/dist/views/list/ListHeader.d.ts +3 -1
- package/dist/views/list/ListHeader.js +2 -2
- package/dist/views/list/jsonSummary.d.ts +3 -3
- package/dist/views/list/jsonSummary.js +47 -20
- package/dist/views/list/useListData.js +5 -1
- package/package.json +8 -4
- package/dist/components/RefMultiSelect.d.ts +0 -22
- package/dist/components/RefMultiSelect.js +0 -113
- package/dist/components/RefSingleSelect.d.ts +0 -17
- package/dist/components/RefSingleSelect.js +0 -110
- package/dist/lib/schemaService.d.ts +0 -2
- package/dist/lib/schemaService.js +0 -39
- package/dist/state/schemaLive.d.ts +0 -2
- package/dist/state/schemaLive.js +0 -19
- /package/dist/{editor.css → components/editor/editor.css} +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
2
|
+
import { NextMinClient } from '../lib/api';
|
|
3
|
+
export interface NextMinContextType {
|
|
4
|
+
client: NextMinClient;
|
|
5
|
+
}
|
|
6
|
+
export declare function useNextMin(): NextMinContextType;
|
|
7
|
+
export declare function NextMinProvider({ children, navigate, apiUrl, apiKey }: {
|
|
3
8
|
children: React.ReactNode;
|
|
4
9
|
navigate?: (url: string) => void;
|
|
10
|
+
apiUrl?: string;
|
|
11
|
+
apiKey?: string;
|
|
5
12
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,27 +1,59 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import React, { useEffect, useRef } from 'react';
|
|
4
4
|
import { HeroUIProvider } from '@heroui/react';
|
|
5
5
|
import { Provider, useDispatch, useSelector } from 'react-redux';
|
|
6
6
|
import { nextminstore } from '../state/store';
|
|
7
|
-
import { fetchSchemas } from '../state/schemasSlice';
|
|
8
|
-
import {
|
|
7
|
+
import { fetchSchemas, setSchemas } from '../state/schemasSlice';
|
|
8
|
+
import { RealtimeClient } from '../lib/RealtimeClient';
|
|
9
|
+
import { AuthClient } from '../lib/AuthClient';
|
|
9
10
|
function Bootstrapper() {
|
|
10
11
|
const dispatch = useDispatch();
|
|
11
12
|
const status = useSelector((s) => s.schemas.status);
|
|
12
13
|
const booted = useRef(false);
|
|
13
14
|
useEffect(() => {
|
|
14
15
|
if (!booted.current) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const rt = RealtimeClient.getInstance();
|
|
17
|
+
// Connect if we have a token OR an API key (for public/playground access)
|
|
18
|
+
if (AuthClient.getToken() || rt.apiKey) {
|
|
19
|
+
rt.connect();
|
|
20
|
+
rt.on('schemasData', (data) => dispatch(setSchemas(data)));
|
|
21
|
+
rt.on('schemasUpdated', (data) => dispatch(setSchemas(data)));
|
|
22
|
+
}
|
|
17
23
|
booted.current = true;
|
|
18
24
|
}
|
|
19
25
|
if (status === 'idle') {
|
|
20
|
-
dispatch(fetchSchemas());
|
|
26
|
+
dispatch(fetchSchemas());
|
|
21
27
|
}
|
|
22
28
|
}, [status, dispatch]);
|
|
23
29
|
return null;
|
|
24
30
|
}
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
import { NextMinClient } from '../lib/api';
|
|
32
|
+
const NextMinContext = React.createContext(undefined);
|
|
33
|
+
export function useNextMin() {
|
|
34
|
+
const context = React.useContext(NextMinContext);
|
|
35
|
+
if (!context)
|
|
36
|
+
throw new Error('useNextMin must be used within NextMinProvider');
|
|
37
|
+
return context;
|
|
38
|
+
}
|
|
39
|
+
export function NextMinProvider({ children, navigate, apiUrl, apiKey }) {
|
|
40
|
+
const client = React.useMemo(() => {
|
|
41
|
+
if (apiUrl)
|
|
42
|
+
AuthClient.setBaseURL(apiUrl);
|
|
43
|
+
if (apiKey)
|
|
44
|
+
AuthClient.setApiKey(apiKey);
|
|
45
|
+
const rt = RealtimeClient.getInstance();
|
|
46
|
+
if (apiUrl) {
|
|
47
|
+
// Ensure absolute URL for socket.io if possible
|
|
48
|
+
const base = apiUrl.startsWith('http') ? apiUrl.replace('/rest', '') : typeof window !== 'undefined' ? window.location.origin : '';
|
|
49
|
+
rt.setBaseURL(base);
|
|
50
|
+
}
|
|
51
|
+
if (apiKey)
|
|
52
|
+
rt.setApiKey(apiKey);
|
|
53
|
+
return new NextMinClient({
|
|
54
|
+
baseURL: apiUrl || '/api',
|
|
55
|
+
apiKey: apiKey
|
|
56
|
+
});
|
|
57
|
+
}, [apiUrl, apiKey]);
|
|
58
|
+
return (_jsx(Provider, { store: nextminstore, children: _jsx(NextMinContext.Provider, { value: { client }, children: _jsxs(HeroUIProvider, { navigate: navigate, children: [_jsx(Bootstrapper, {}), children] }) }) }));
|
|
27
59
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import '
|
|
1
|
+
import '../components/editor/editor.css';
|
|
2
2
|
export declare function NextMinRouter(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -10,7 +10,7 @@ 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 '
|
|
13
|
+
import '../components/editor/editor.css';
|
|
14
14
|
export function NextMinRouter() {
|
|
15
15
|
const pathname = usePathname();
|
|
16
16
|
const router = useRouter();
|
|
@@ -14,12 +14,18 @@ export const fetchSchemas = createAsyncThunk('schemas/fetch', async () => {
|
|
|
14
14
|
return status === 'idle';
|
|
15
15
|
},
|
|
16
16
|
});
|
|
17
|
+
const mergeSchemas = (existing, incoming) => {
|
|
18
|
+
const map = new Map();
|
|
19
|
+
existing.forEach((s) => map.set(s.modelName, s));
|
|
20
|
+
incoming.forEach((s) => map.set(s.modelName, s));
|
|
21
|
+
return Array.from(map.values());
|
|
22
|
+
};
|
|
17
23
|
const schemasSlice = createSlice({
|
|
18
24
|
name: 'schemas',
|
|
19
25
|
initialState,
|
|
20
26
|
reducers: {
|
|
21
27
|
setSchemas(state, action) {
|
|
22
|
-
state.items = action.payload;
|
|
28
|
+
state.items = mergeSchemas(state.items, action.payload);
|
|
23
29
|
state.status = 'succeeded';
|
|
24
30
|
},
|
|
25
31
|
},
|
|
@@ -31,7 +37,7 @@ const schemasSlice = createSlice({
|
|
|
31
37
|
})
|
|
32
38
|
.addCase(fetchSchemas.fulfilled, (state, action) => {
|
|
33
39
|
state.status = 'succeeded';
|
|
34
|
-
state.items = action.payload;
|
|
40
|
+
state.items = mergeSchemas(state.items, action.payload);
|
|
35
41
|
})
|
|
36
42
|
.addCase(fetchSchemas.rejected, (state, action) => {
|
|
37
43
|
state.status = 'failed';
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import { useSelector } from 'react-redux';
|
|
5
5
|
import { api } from '../lib/api';
|
|
6
6
|
import { Divider, Card, CardBody } from '@heroui/react';
|
|
7
|
+
import { useRealtime } from '../hooks/useRealtime';
|
|
7
8
|
export function DashboardPage() {
|
|
8
|
-
const schemas = useSelector((s) => s.schemas
|
|
9
|
-
const
|
|
9
|
+
const { items: schemas, status } = useSelector((s) => s.schemas);
|
|
10
|
+
const { isConnected, lastEvent } = useRealtime();
|
|
10
11
|
// Build cards dynamically for schemas with showCount === true
|
|
11
12
|
const cards = useMemo(() => {
|
|
12
13
|
return (schemas || [])
|
|
@@ -23,52 +24,65 @@ export function DashboardPage() {
|
|
|
23
24
|
const [counts, setCounts] = useState({});
|
|
24
25
|
const [loading, setLoading] = useState(false);
|
|
25
26
|
const [error, setError] = useState(null);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return;
|
|
27
|
+
const load = React.useCallback(async (isCancelled = () => false, specificModel) => {
|
|
28
|
+
if (status !== 'succeeded')
|
|
29
|
+
return;
|
|
30
|
+
if (!specificModel)
|
|
31
31
|
setLoading(true);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
}));
|
|
46
|
-
if (!cancelled) {
|
|
47
|
-
const map = {};
|
|
48
|
-
for (const [k, v] of entries)
|
|
49
|
-
map[k] = v;
|
|
50
|
-
setCounts(map);
|
|
32
|
+
setError(null);
|
|
33
|
+
try {
|
|
34
|
+
const targets = specificModel
|
|
35
|
+
? cards.filter(c => c.model === specificModel)
|
|
36
|
+
: cards;
|
|
37
|
+
const entries = await Promise.all(targets.map(async (c) => {
|
|
38
|
+
if (!c.model)
|
|
39
|
+
return [c.key, null];
|
|
40
|
+
try {
|
|
41
|
+
const res = await api.list(c.model, { page: 1, limit: 1 });
|
|
42
|
+
const total = res?.pagination?.totalRows ?? res?.data?.length ?? 0;
|
|
43
|
+
return [c.key, total];
|
|
51
44
|
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return [c.key, null];
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
if (!isCancelled()) {
|
|
50
|
+
setCounts((prev) => {
|
|
51
|
+
const newMap = { ...prev };
|
|
52
|
+
for (const [k, v] of entries)
|
|
53
|
+
newMap[k] = v;
|
|
54
|
+
return newMap;
|
|
55
|
+
});
|
|
52
56
|
}
|
|
53
|
-
catch (e) {
|
|
54
|
-
if (!cancelled)
|
|
55
|
-
setError(e?.message || 'Failed to load stats');
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
if (!cancelled)
|
|
59
|
-
setLoading(false);
|
|
60
|
-
}
|
|
61
57
|
}
|
|
62
|
-
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (!isCancelled())
|
|
60
|
+
setError(e?.message || 'Failed to load stats');
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (!isCancelled())
|
|
64
|
+
setLoading(false);
|
|
65
|
+
}
|
|
66
|
+
}, [cards, status]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
let cancelled = false;
|
|
69
|
+
load(() => cancelled);
|
|
63
70
|
return () => {
|
|
64
71
|
cancelled = true;
|
|
65
72
|
};
|
|
66
|
-
}, [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
}, [load]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (lastEvent) {
|
|
76
|
+
const { event, payload } = lastEvent;
|
|
77
|
+
const model = (payload?.model || payload?.modelName)?.toLowerCase();
|
|
78
|
+
// Refresh specific model count if it changed
|
|
79
|
+
if (model && (event.includes('created') || event.includes('deleted') || event.includes('create') || event.includes('delete'))) {
|
|
80
|
+
load(() => false, model);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, [lastEvent, status]);
|
|
84
|
+
return (_jsxs("div", { className: "grid gap-6 px-4 pb-8", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsx("h2", { className: "m-0 text-2xl font-bold text-gray-800", children: "Admin Overview" }) }), _jsx(Divider, {}), (error || loading) && (_jsxs("div", { className: "rounded-xl bg-blue-50/50 border border-blue-100 p-4 text-blue-700 text-sm font-medium flex items-center gap-3", children: [_jsxs("svg", { className: "animate-spin h-4 w-4", viewBox: "0 0 24 24", children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4", fill: "none" }), _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" })] }), error || 'Synchronizing statistics...'] })), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4", children: cards.map((c) => {
|
|
71
85
|
const value = counts[c.key];
|
|
72
|
-
return (_jsx(Card, { className: "shadow-sm border border-
|
|
86
|
+
return (_jsx(Card, { className: "shadow-sm border border-gray-100 hover:border-blue-200 transition-all duration-200", children: _jsxs(CardBody, { className: "flex flex-row items-center justify-start gap-4 p-5", children: [_jsx("div", { className: "p-3 rounded-xl bg-blue-50 text-blue-600", children: _jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }), _jsx("circle", { cx: "9", cy: "7", r: "4" }), _jsx("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }), _jsx("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })] }) }), _jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1", children: c.label }), _jsx("div", { className: "text-2xl font-bold text-gray-900 leading-none", children: value == null ? '—' : value.toLocaleString() })] })] }) }, c.key));
|
|
73
87
|
}) })] }));
|
|
74
88
|
}
|
package/dist/views/ListPage.js
CHANGED
|
@@ -47,7 +47,7 @@ export function ListPage({ model }) {
|
|
|
47
47
|
searchKey: filters.searchKey,
|
|
48
48
|
});
|
|
49
49
|
// columns (ALWAYS call hooks; derive from schema or empty object)
|
|
50
|
-
const attributes = (
|
|
50
|
+
const attributes = (viewSchema?.attributes ?? {});
|
|
51
51
|
// Generic password detectors (hide from table columns as well)
|
|
52
52
|
const normKey = (k) => k.toLowerCase().replace(/[^a-z]/g, '');
|
|
53
53
|
const isPasswordKeyRaw = (k) => {
|
|
@@ -67,8 +67,15 @@ export function ListPage({ model }) {
|
|
|
67
67
|
return !isPriv && !isPasswordKeyRaw(k) && !isPasswordByAttr(a);
|
|
68
68
|
})
|
|
69
69
|
.map(([k]) => k), [attributes]);
|
|
70
|
-
// default visible:
|
|
70
|
+
// default visible columns: respect columnsSelector if present, else use heuristic
|
|
71
71
|
const defaultVisible = useMemo(() => {
|
|
72
|
+
// 1) Explicit selector from schema
|
|
73
|
+
const selector = schema?.columnsSelector;
|
|
74
|
+
if (selector && Array.isArray(selector)) {
|
|
75
|
+
// only include columns that actually exist in the attributes
|
|
76
|
+
return selector.filter(c => allColumns.includes(c));
|
|
77
|
+
}
|
|
78
|
+
// 2) Heuristic fallback
|
|
72
79
|
const set = new Set();
|
|
73
80
|
// first 4 non-private fields
|
|
74
81
|
allColumns.slice(0, 4).forEach((c) => set.add(c));
|
|
@@ -79,14 +86,37 @@ export function ListPage({ model }) {
|
|
|
79
86
|
if (allColumns.includes('createdAt'))
|
|
80
87
|
set.add('createdAt');
|
|
81
88
|
return Array.from(set);
|
|
82
|
-
}, [allColumns]);
|
|
89
|
+
}, [allColumns, schema]);
|
|
83
90
|
// visible columns state (init once, update when model/defaultVisible change)
|
|
84
91
|
const [visibleColumns, setVisibleColumns] = useState(() => new Set(defaultVisible));
|
|
85
92
|
useEffect(() => {
|
|
86
93
|
// whenever model or defaultVisible changes, reset visible columns
|
|
87
94
|
setVisibleColumns(new Set(defaultVisible));
|
|
88
95
|
}, [model, defaultVisible]);
|
|
89
|
-
const tableColumns = useMemo(() =>
|
|
96
|
+
const tableColumns = useMemo(() => {
|
|
97
|
+
const visible = allColumns.filter((c) => visibleColumns.has(c));
|
|
98
|
+
const selector = schema?.columnsSelector;
|
|
99
|
+
if (selector && Array.isArray(selector)) {
|
|
100
|
+
// Order based on selector
|
|
101
|
+
const ordered = [];
|
|
102
|
+
const visited = new Set();
|
|
103
|
+
// 1) Add columns from selector if they are visible
|
|
104
|
+
selector.forEach((c) => {
|
|
105
|
+
if (visibleColumns.has(c)) {
|
|
106
|
+
ordered.push(c);
|
|
107
|
+
visited.add(c);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// 2) Add any other visible columns not in the selector (safety)
|
|
111
|
+
visible.forEach((c) => {
|
|
112
|
+
if (!visited.has(c)) {
|
|
113
|
+
ordered.push(c);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return ordered;
|
|
117
|
+
}
|
|
118
|
+
return visible;
|
|
119
|
+
}, [allColumns, visibleColumns, schema]);
|
|
90
120
|
const baseHref = useMemo(() => `/admin/${model}`, [model]);
|
|
91
121
|
const handleRefetch = useCallback(async () => {
|
|
92
122
|
await refetch();
|
|
@@ -5,6 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|
|
5
5
|
import { ListHeader } from './list/ListHeader';
|
|
6
6
|
import { NoAccess } from '../components/NoAccess';
|
|
7
7
|
import { SchemaForm } from '../components/SchemaForm';
|
|
8
|
+
import { Button } from '@heroui/react';
|
|
8
9
|
import { api, ApiError } from '../lib/api';
|
|
9
10
|
import { systemLoaded } from '../state/nextMinSlice';
|
|
10
11
|
const MODEL = 'settings'; // use lowercase to match router/api paths
|
|
@@ -16,15 +17,17 @@ export function SettingsEdit() {
|
|
|
16
17
|
const [doc, setDoc] = useState(null);
|
|
17
18
|
const [loading, setLoading] = useState(true);
|
|
18
19
|
const [saving, setSaving] = useState(false);
|
|
20
|
+
const [cleaning, setCleaning] = useState(false);
|
|
19
21
|
const [forbidden, setForbidden] = useState(null);
|
|
20
22
|
const [error, setError] = useState(null);
|
|
23
|
+
const [cleanupReport, setCleanupReport] = useState(null);
|
|
21
24
|
const fetchSingleton = useCallback(async () => {
|
|
22
25
|
setLoading(true);
|
|
23
26
|
setForbidden(null);
|
|
24
27
|
setError(null);
|
|
25
28
|
try {
|
|
26
29
|
// Only one Settings row exists; fetch first page with 1 item
|
|
27
|
-
const resp = await api.list(MODEL, 0, 1);
|
|
30
|
+
const resp = await api.list(MODEL, { page: 0, limit: 1 });
|
|
28
31
|
const first = resp?.data?.[0] ?? null;
|
|
29
32
|
setDoc(first);
|
|
30
33
|
const normalized = first == null
|
|
@@ -75,6 +78,25 @@ export function SettingsEdit() {
|
|
|
75
78
|
setSaving(false);
|
|
76
79
|
}
|
|
77
80
|
}, [doc, fetchSingleton]);
|
|
81
|
+
const handleCleanup = useCallback(async () => {
|
|
82
|
+
if (!confirm('WARNING: This will permanently delete all database fields that are not defined in your schemas. Are you sure you want to proceed?')) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
setCleaning(true);
|
|
86
|
+
setError(null);
|
|
87
|
+
setCleanupReport(null);
|
|
88
|
+
try {
|
|
89
|
+
const resp = await api.cleanup();
|
|
90
|
+
setCleanupReport(resp.data);
|
|
91
|
+
alert('Database cleanup completed successfully.');
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
setError(e?.message || 'Cleanup failed');
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
setCleaning(false);
|
|
98
|
+
}
|
|
99
|
+
}, []);
|
|
78
100
|
const title = schema?.modelName ?? 'Settings';
|
|
79
101
|
if (!schema) {
|
|
80
102
|
return (_jsx("div", { className: "p-4 text-danger text-sm", children: "Settings schema not found." }));
|
|
@@ -82,6 +104,7 @@ export function SettingsEdit() {
|
|
|
82
104
|
if (forbidden) {
|
|
83
105
|
return (_jsxs("div", { className: "px-4", children: [_jsx(ListHeader, { title: title, createHref: "", loading: false }), _jsx(NoAccess, { message: forbidden })] }));
|
|
84
106
|
}
|
|
85
|
-
|
|
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 }))] }));
|
|
86
109
|
}
|
|
87
110
|
export default SettingsEdit;
|