@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.
Files changed (56) hide show
  1. package/README.md +29 -3
  2. package/dist/auth/SignInForm.js +4 -2
  3. package/dist/components/AdminApp.js +15 -38
  4. package/dist/components/ArchitectureDemo.d.ts +1 -0
  5. package/dist/components/ArchitectureDemo.js +45 -0
  6. package/dist/components/PhoneInput.d.ts +3 -0
  7. package/dist/components/PhoneInput.js +23 -19
  8. package/dist/components/RefSelect.d.ts +16 -0
  9. package/dist/components/RefSelect.js +225 -0
  10. package/dist/components/SchemaForm.js +131 -51
  11. package/dist/components/Sidebar.js +6 -13
  12. package/dist/components/TableFilters.js +2 -0
  13. package/dist/components/editor/TiptapEditor.js +1 -1
  14. package/dist/components/editor/Toolbar.js +13 -2
  15. package/dist/components/editor/components/DistrictGridModal.js +2 -3
  16. package/dist/components/editor/components/SchemaInsertionModal.js +2 -2
  17. package/dist/components/viewer/DynamicViewer.js +70 -9
  18. package/dist/hooks/useRealtime.d.ts +8 -0
  19. package/dist/hooks/useRealtime.js +30 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +6 -0
  22. package/dist/lib/AuthClient.d.ts +15 -0
  23. package/dist/lib/AuthClient.js +63 -0
  24. package/dist/lib/QueryBuilder.d.ts +29 -0
  25. package/dist/lib/QueryBuilder.js +74 -0
  26. package/dist/lib/RealtimeClient.d.ts +16 -0
  27. package/dist/lib/RealtimeClient.js +56 -0
  28. package/dist/lib/api.d.ts +15 -3
  29. package/dist/lib/api.js +71 -58
  30. package/dist/lib/auth.js +7 -2
  31. package/dist/lib/types.d.ts +16 -0
  32. package/dist/nextmin.css +1 -1
  33. package/dist/providers/NextMinProvider.d.ts +8 -1
  34. package/dist/providers/NextMinProvider.js +40 -8
  35. package/dist/router/NextMinRouter.d.ts +1 -1
  36. package/dist/router/NextMinRouter.js +1 -1
  37. package/dist/state/schemasSlice.js +8 -2
  38. package/dist/views/DashboardPage.js +56 -42
  39. package/dist/views/ListPage.js +34 -4
  40. package/dist/views/SettingsEdit.js +25 -2
  41. package/dist/views/list/DataTableHero.js +103 -46
  42. package/dist/views/list/ListHeader.d.ts +3 -1
  43. package/dist/views/list/ListHeader.js +2 -2
  44. package/dist/views/list/jsonSummary.d.ts +3 -3
  45. package/dist/views/list/jsonSummary.js +47 -20
  46. package/dist/views/list/useListData.js +5 -1
  47. package/package.json +8 -4
  48. package/dist/components/RefMultiSelect.d.ts +0 -22
  49. package/dist/components/RefMultiSelect.js +0 -113
  50. package/dist/components/RefSingleSelect.d.ts +0 -17
  51. package/dist/components/RefSingleSelect.js +0 -110
  52. package/dist/lib/schemaService.d.ts +0 -2
  53. package/dist/lib/schemaService.js +0 -39
  54. package/dist/state/schemaLive.d.ts +0 -2
  55. package/dist/state/schemaLive.js +0 -19
  56. /package/dist/{editor.css → components/editor/editor.css} +0 -0
@@ -1,5 +1,12 @@
1
1
  import React from 'react';
2
- export declare function NextMinProvider({ children, navigate }: {
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 { startSchemaLive } from '../state/schemaLive'; // socket
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
- // start socket once (dev/prod both OK; you can guard by NODE_ENV if you want)
16
- startSchemaLive(nextminstore);
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()); // one-time HTTP load as fallback/initial
26
+ dispatch(fetchSchemas());
21
27
  }
22
28
  }, [status, dispatch]);
23
29
  return null;
24
30
  }
25
- export function NextMinProvider({ children, navigate }) {
26
- return (_jsx(Provider, { store: nextminstore, children: _jsxs(HeroUIProvider, { navigate: navigate, children: [_jsx(Bootstrapper, {}), children] }) }));
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 '@airoom/nextmin-react/editor.css';
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 '@airoom/nextmin-react/editor.css';
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.items);
9
- const status = useSelector((s) => s.schemas.status);
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
- useEffect(() => {
27
- let cancelled = false;
28
- async function load() {
29
- if (status !== 'succeeded')
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
- setError(null);
33
- try {
34
- const entries = await Promise.all(cards.map(async (c) => {
35
- if (!c.model)
36
- return [c.key, null]; // schema missing
37
- try {
38
- const res = await api.list(c.model, 0, 1);
39
- const total = res?.pagination?.totalRows ?? res?.data?.length ?? 0;
40
- return [c.key, total];
41
- }
42
- catch (e) {
43
- return [c.key, null];
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
- load();
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
- }, [cards, status]);
67
- return (_jsxs("div", { className: "grid gap-4 px-4", children: [_jsx("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: "Dashboard" }) }), _jsx(Divider, { className: "my-3" }), (error || loading) && (_jsx("div", { className: 'rounded-md px-3 py-2 ' +
68
- (error
69
- ? 'bg-red-100 text-red-700'
70
- : 'bg-default-100 text-default-700'), children: error || 'Loading summary…' })), _jsx("div", { className: "rounded-md px-3 py-2 text-2xl text-center bg-default-50 text-foreground/80", children: "Welcome to your dashboard!" }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4", children: cards.map((c) => {
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-default-200", children: _jsx(CardBody, { className: "flex flex-row items-center justify-start gap-3", children: _jsxs("div", { children: [_jsx("div", { className: "text-sm text-foreground/60", children: c.label }), _jsx("div", { className: "text-2xl font-semibold mt-1", children: value == null ? '—' : value })] }) }) }, c.key));
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
  }
@@ -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 = (schema?.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: first 4 + 'createdAt' if present (stable via useMemo)
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(() => allColumns.filter((c) => visibleColumns.has(c)), [allColumns, visibleColumns]);
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
- return (_jsxs("div", { className: "grid gap-3 px-4", children: [_jsx(ListHeader, { title: title, hideCreate: true, createHref: "", loading: loading || saving }), error ? (_jsx("div", { className: "text-danger text-sm", children: error })) : (_jsx(SchemaForm, { model: MODEL, schemaOverride: schema, initialValues: doc ?? undefined, submitLabel: "Save", busy: loading || saving, onSubmit: handleSubmit, showReset: false }))] }));
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;