@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,113 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Select, SelectItem, Input, Chip } from '@heroui/react';
|
|
5
|
-
import { api } from '../lib/api';
|
|
6
|
-
export const RefMultiSelect = ({ name, label, refModel, showKey = 'name', value, onChange, description, disabled, required, pageSize = 10000, error, errorMessage, }) => {
|
|
7
|
-
const modelSlug = React.useMemo(() => String(refModel).trim().toLowerCase(), [refModel]);
|
|
8
|
-
// normalize an option to always have string `id`
|
|
9
|
-
const normalize = React.useCallback((it) => {
|
|
10
|
-
const { id: rawId, _id: raw_Id, ...rest } = it || {};
|
|
11
|
-
const id = (typeof rawId === 'string' && rawId) ||
|
|
12
|
-
(typeof raw_Id === 'string' && raw_Id) ||
|
|
13
|
-
String(rawId ?? raw_Id ?? '');
|
|
14
|
-
return { id, ...rest };
|
|
15
|
-
}, []);
|
|
16
|
-
// from props.value (objects or ids) build selected ids + pre-seeded options
|
|
17
|
-
const preselected = React.useMemo(() => {
|
|
18
|
-
if (Array.isArray(value) && value.length && typeof value[0] === 'object') {
|
|
19
|
-
return value.map(normalize);
|
|
20
|
-
}
|
|
21
|
-
return [];
|
|
22
|
-
}, [value, normalize]);
|
|
23
|
-
const selectedIds = React.useMemo(() => {
|
|
24
|
-
if (preselected.length)
|
|
25
|
-
return preselected.map((o) => o.id);
|
|
26
|
-
return (value ?? []).map(String);
|
|
27
|
-
}, [value, preselected]);
|
|
28
|
-
// options start with preselected objects so chips show immediately & no warning
|
|
29
|
-
const [options, setOptions] = React.useState(preselected);
|
|
30
|
-
// keep options in sync if parent passes new objects later
|
|
31
|
-
React.useEffect(() => {
|
|
32
|
-
if (!preselected.length)
|
|
33
|
-
return;
|
|
34
|
-
setOptions((prev) => {
|
|
35
|
-
const map = new Map(prev.map((o) => [o.id, o]));
|
|
36
|
-
preselected.forEach((o) => map.set(o.id, o));
|
|
37
|
-
return Array.from(map.values());
|
|
38
|
-
});
|
|
39
|
-
}, [preselected]);
|
|
40
|
-
const selectedKeys = React.useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
41
|
-
const labelOf = React.useCallback((opt) => String(opt?.[showKey] ??
|
|
42
|
-
opt?.name ??
|
|
43
|
-
opt?.title ??
|
|
44
|
-
opt?.key ??
|
|
45
|
-
opt?.id ??
|
|
46
|
-
opt?._id ??
|
|
47
|
-
''), [showKey]);
|
|
48
|
-
const [open, setOpen] = React.useState(false);
|
|
49
|
-
const [query, setQuery] = React.useState('');
|
|
50
|
-
const [loading, setLoading] = React.useState(false);
|
|
51
|
-
// fetch list (large page) and merge with preseeded
|
|
52
|
-
const load = React.useCallback(async (q) => {
|
|
53
|
-
setLoading(true);
|
|
54
|
-
try {
|
|
55
|
-
const params = {
|
|
56
|
-
limit: pageSize,
|
|
57
|
-
sort: showKey,
|
|
58
|
-
sortType: 'asc',
|
|
59
|
-
...(q ? { q, searchKey: showKey } : {}),
|
|
60
|
-
};
|
|
61
|
-
const res = await api.list?.(modelSlug, 0, pageSize, params);
|
|
62
|
-
const payload = res?.data ?? res;
|
|
63
|
-
const list = payload?.items ??
|
|
64
|
-
payload?.docs ??
|
|
65
|
-
payload?.results ??
|
|
66
|
-
payload?.list ??
|
|
67
|
-
(Array.isArray(payload) ? payload : []);
|
|
68
|
-
const normalized = list.map(normalize);
|
|
69
|
-
// merge by id (keep preselected if present)
|
|
70
|
-
setOptions((prev) => {
|
|
71
|
-
const map = new Map(prev.map((o) => [o.id, o]));
|
|
72
|
-
normalized.forEach((o) => map.set(o.id, { ...(map.get(o.id) || {}), ...o }));
|
|
73
|
-
return Array.from(map.values());
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
finally {
|
|
77
|
-
setLoading(false);
|
|
78
|
-
}
|
|
79
|
-
}, [modelSlug, pageSize, showKey, normalize]);
|
|
80
|
-
// load on open + debounced search
|
|
81
|
-
React.useEffect(() => {
|
|
82
|
-
if (open)
|
|
83
|
-
void load(query);
|
|
84
|
-
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
85
|
-
React.useEffect(() => {
|
|
86
|
-
if (!open)
|
|
87
|
-
return;
|
|
88
|
-
const t = setTimeout(() => void load(query), 250);
|
|
89
|
-
return () => clearTimeout(t);
|
|
90
|
-
}, [query, open, load]);
|
|
91
|
-
const topSearch = (_jsx("div", { className: "p-2", children: _jsx(Input, { size: "sm", variant: "bordered", "aria-label": `Search ${refModel}`, placeholder: `Search ${refModel}…`, value: query, onChange: (e) => setQuery(e.target.value), autoFocus: true }) }));
|
|
92
|
-
return (_jsx("div", { className: "w-full", children: _jsx(Select, { name: name, label: label, "aria-label": label, isDisabled: disabled, selectionMode: "multiple", selectedKeys: selectedKeys, onSelectionChange: (keys) => {
|
|
93
|
-
if (keys === 'all')
|
|
94
|
-
return;
|
|
95
|
-
onChange(Array.from(keys).map(String));
|
|
96
|
-
}, isClearable: true, items: options, isVirtualized: true, isMultiline: true, isInvalid: !!error, errorMessage: errorMessage || '', variant: "bordered", maxListboxHeight: 288, itemHeight: 36, className: "w-full", placeholder: `Select ${refModel}`, onOpenChange: setOpen, isLoading: loading, description: description || null, listboxProps: {
|
|
97
|
-
topContent: topSearch,
|
|
98
|
-
emptyContent: loading ? undefined : 'No results',
|
|
99
|
-
className: '',
|
|
100
|
-
}, scrollShadowProps: {
|
|
101
|
-
isEnabled: false,
|
|
102
|
-
},
|
|
103
|
-
// chips like docs; items is an array (SelectedItems<T>)
|
|
104
|
-
renderValue: (items) => {
|
|
105
|
-
const arr = Array.isArray(items) ? items : [];
|
|
106
|
-
// first render: items may be empty → fall back to selectedIds mapped from preseeded options
|
|
107
|
-
if (!arr.length && selectedIds.length) {
|
|
108
|
-
const byId = new Map(options.map((o) => [String(o.id), o]));
|
|
109
|
-
return (_jsx("div", { className: "flex flex-wrap gap-1", children: selectedIds.map((id) => (_jsx(Chip, { size: "sm", variant: "flat", children: labelOf(byId.get(String(id))) }, id))) }));
|
|
110
|
-
}
|
|
111
|
-
return (_jsx("div", { className: "flex flex-wrap gap-1", children: arr.map((it) => (_jsx(Chip, { size: "sm", variant: "flat", children: String(it?.props?.textValue ?? it?.props?.children ?? '') }, String(it?.key)))) }));
|
|
112
|
-
}, children: (item) => (_jsx(SelectItem, { textValue: labelOf(item), children: labelOf(item) }, item.id)) }) }));
|
|
113
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export type RefSingleSelectProps = {
|
|
2
|
-
name: string;
|
|
3
|
-
label: string;
|
|
4
|
-
refModel: string;
|
|
5
|
-
showKey?: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
value?: string | null;
|
|
8
|
-
onChange: (id: string | null) => void;
|
|
9
|
-
disabled?: boolean;
|
|
10
|
-
required?: boolean;
|
|
11
|
-
className?: string;
|
|
12
|
-
classNames?: {
|
|
13
|
-
trigger?: string;
|
|
14
|
-
};
|
|
15
|
-
pageSize?: number;
|
|
16
|
-
};
|
|
17
|
-
export declare function RefSingleSelect({ name, label, refModel, showKey, description, value, onChange, disabled, required, className, classNames, pageSize, }: RefSingleSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
import { Select, SelectItem, Input } from '@heroui/react';
|
|
5
|
-
import { api } from '../lib/api';
|
|
6
|
-
function stripPointerNone(cls) {
|
|
7
|
-
return (cls ?? '').replace(/\bpointer-events-none\b/g, '').trim();
|
|
8
|
-
}
|
|
9
|
-
export function RefSingleSelect({ name, label, refModel, showKey = 'name', description, value, onChange, disabled, required, className, classNames, pageSize = 50, }) {
|
|
10
|
-
const [loading, setLoading] = React.useState(false);
|
|
11
|
-
const [items, setItems] = React.useState([]);
|
|
12
|
-
const [error, setError] = React.useState(null);
|
|
13
|
-
const [query, setQuery] = React.useState('');
|
|
14
|
-
const [open, setOpen] = React.useState(false);
|
|
15
|
-
const refModelLC = React.useMemo(() => refModel.toLowerCase(), [refModel]);
|
|
16
|
-
// normalize an option to always have string `id`
|
|
17
|
-
const normalize = React.useCallback((it) => {
|
|
18
|
-
const { id: rawId, _id: raw_Id, ...rest } = it || {};
|
|
19
|
-
const id = (typeof rawId === 'string' && rawId) ||
|
|
20
|
-
(typeof raw_Id === 'string' && raw_Id) ||
|
|
21
|
-
String(rawId ?? raw_Id ?? '');
|
|
22
|
-
return { id, ...rest };
|
|
23
|
-
}, []);
|
|
24
|
-
const load = React.useCallback(async (q) => {
|
|
25
|
-
try {
|
|
26
|
-
setLoading(true);
|
|
27
|
-
setError(null);
|
|
28
|
-
const params = {
|
|
29
|
-
limit: pageSize,
|
|
30
|
-
sort: showKey,
|
|
31
|
-
sortType: 'asc',
|
|
32
|
-
...(q ? { q, searchKey: showKey } : {}),
|
|
33
|
-
};
|
|
34
|
-
const res = await api.list(refModelLC, 0, pageSize, params);
|
|
35
|
-
const payload = res?.data ?? res;
|
|
36
|
-
const list = Array.isArray(payload)
|
|
37
|
-
? payload
|
|
38
|
-
: payload?.items ?? payload?.docs ?? [];
|
|
39
|
-
const normalized = list.map(normalize);
|
|
40
|
-
setItems((prev) => {
|
|
41
|
-
// Find currently selected item in previous list to ensure we don't lose it
|
|
42
|
-
// (which would break the label display)
|
|
43
|
-
const selectedId = value ? String(value) : null;
|
|
44
|
-
const selectedItem = selectedId
|
|
45
|
-
? prev.find((p) => String(p.id) === selectedId)
|
|
46
|
-
: null;
|
|
47
|
-
// Start with the new list/search results
|
|
48
|
-
const newItems = [...normalized];
|
|
49
|
-
// If we have a selected item and it's not in the new results, add it back
|
|
50
|
-
if (selectedItem &&
|
|
51
|
-
!newItems.some((i) => String(i.id) === String(selectedItem.id))) {
|
|
52
|
-
newItems.push(selectedItem);
|
|
53
|
-
}
|
|
54
|
-
return newItems;
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
catch (e) {
|
|
58
|
-
setError(e?.message || 'Failed to load options');
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
setLoading(false);
|
|
62
|
-
}
|
|
63
|
-
}, [refModelLC, pageSize, showKey, normalize]);
|
|
64
|
-
// load on open
|
|
65
|
-
React.useEffect(() => {
|
|
66
|
-
if (open)
|
|
67
|
-
void load(query);
|
|
68
|
-
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
69
|
-
// debounced search
|
|
70
|
-
React.useEffect(() => {
|
|
71
|
-
if (!open)
|
|
72
|
-
return;
|
|
73
|
-
const t = setTimeout(() => void load(query), 250);
|
|
74
|
-
return () => clearTimeout(t);
|
|
75
|
-
}, [query, open, load]);
|
|
76
|
-
// If we have a value but no items, try to load once to get the label
|
|
77
|
-
React.useEffect(() => {
|
|
78
|
-
if (value && items.length === 0 && !loading) {
|
|
79
|
-
void load('');
|
|
80
|
-
}
|
|
81
|
-
}, [value]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
82
|
-
const getId = (r) => (typeof r.id === 'string' && r.id) ||
|
|
83
|
-
(typeof r._id === 'string' && r._id) ||
|
|
84
|
-
'';
|
|
85
|
-
const getLabel = (r) => {
|
|
86
|
-
const v = r?.[showKey];
|
|
87
|
-
return v == null ? getId(r) : String(v);
|
|
88
|
-
};
|
|
89
|
-
const topSearch = (_jsx("div", { className: "p-2", children: _jsx(Input, { size: "sm", variant: "bordered", "aria-label": `Search ${refModel}`, placeholder: `Search ${refModel}…`, value: query, onChange: (e) => setQuery(e.target.value), isClearable: true, onClear: () => setQuery(''), autoFocus: true, onKeyDown: (e) => e.stopPropagation() }) }));
|
|
90
|
-
return (_jsx("div", { className: "w-full", children: _jsx(Select, { name: name, label: label, placeholder: label, labelPlacement: "outside", className: className, classNames: {
|
|
91
|
-
// Make sure the whole trigger is clickable
|
|
92
|
-
trigger: `cursor-pointer ${stripPointerNone(classNames?.trigger)}`,
|
|
93
|
-
}, variant: "bordered", isDisabled: disabled || (loading && !items.length), isLoading: loading, isRequired: required, description: error || description, items: items, selectionMode: "single", selectedKeys: value ? new Set([String(value)]) : new Set(), onSelectionChange: (keys) => {
|
|
94
|
-
if (keys === 'all')
|
|
95
|
-
return;
|
|
96
|
-
const v = Array.from(keys)[0];
|
|
97
|
-
onChange(v ?? null);
|
|
98
|
-
}, scrollShadowProps: {
|
|
99
|
-
isEnabled: false,
|
|
100
|
-
}, onOpenChange: setOpen, listboxProps: {
|
|
101
|
-
topContent: topSearch,
|
|
102
|
-
emptyContent: loading ? undefined : 'No results',
|
|
103
|
-
}, renderValue: (items) => {
|
|
104
|
-
return items.map((item) => (_jsx("span", { children: item.textValue }, item.key)));
|
|
105
|
-
}, children: (opt) => {
|
|
106
|
-
const key = getId(opt);
|
|
107
|
-
const text = getLabel(opt);
|
|
108
|
-
return (_jsx(SelectItem, { textValue: text, children: text }, key));
|
|
109
|
-
} }) }));
|
|
110
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Manager, } from 'socket.io-client';
|
|
2
|
-
const SOCKET_PATH = '/__nextmin__/schema';
|
|
3
|
-
const NAMESPACE = '/schema';
|
|
4
|
-
let schemaSocket = null;
|
|
5
|
-
export function getSchemaService() {
|
|
6
|
-
if (typeof window === 'undefined')
|
|
7
|
-
return null; // SSR guard
|
|
8
|
-
const urlStr = process.env.NEXT_PUBLIC_NEXTMIN_API_URL || 'http://localhost:8081/rest';
|
|
9
|
-
let origin = 'http://localhost:8081';
|
|
10
|
-
try {
|
|
11
|
-
const u = new URL(urlStr);
|
|
12
|
-
origin = u.origin;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
// keep default
|
|
16
|
-
}
|
|
17
|
-
// Keep a single Manager across HMR / route changes
|
|
18
|
-
const MGR_KEY = '__NM_SCHEMA_MGR__';
|
|
19
|
-
let mgr = window[MGR_KEY];
|
|
20
|
-
if (!mgr) {
|
|
21
|
-
const mgrOpts = {
|
|
22
|
-
path: SOCKET_PATH,
|
|
23
|
-
transports: ['websocket'], // avoid polling aborts on nav
|
|
24
|
-
reconnection: true,
|
|
25
|
-
reconnectionAttempts: Infinity,
|
|
26
|
-
reconnectionDelay: 500,
|
|
27
|
-
reconnectionDelayMax: 4000,
|
|
28
|
-
};
|
|
29
|
-
mgr = new Manager(origin, mgrOpts);
|
|
30
|
-
window[MGR_KEY] = mgr;
|
|
31
|
-
}
|
|
32
|
-
if (!schemaSocket) {
|
|
33
|
-
const sockOpts = {
|
|
34
|
-
auth: { apiKey: process.env.NEXT_PUBLIC_NEXTMIN_API_KEY || '' }, // <-- auth goes here
|
|
35
|
-
};
|
|
36
|
-
schemaSocket = mgr.socket(NAMESPACE, sockOpts);
|
|
37
|
-
}
|
|
38
|
-
return schemaSocket;
|
|
39
|
-
}
|
package/dist/state/schemaLive.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { setSchemas } from './schemasSlice';
|
|
3
|
-
import { getSchemaService } from '../lib/schemaService';
|
|
4
|
-
export function startSchemaLive(store) {
|
|
5
|
-
const sock = getSchemaService();
|
|
6
|
-
if (!sock)
|
|
7
|
-
return;
|
|
8
|
-
if (sock.__nm_bound)
|
|
9
|
-
return;
|
|
10
|
-
sock.__nm_bound = true;
|
|
11
|
-
const push = (all) => {
|
|
12
|
-
const data = Array.isArray(all) ? all : Object.values(all || {});
|
|
13
|
-
store.dispatch(setSchemas(data));
|
|
14
|
-
};
|
|
15
|
-
sock.on('schemasData', push);
|
|
16
|
-
sock.on('schemasUpdated', push);
|
|
17
|
-
sock.on('connect', () => console.log('[nextmin] schema service connected', sock.id));
|
|
18
|
-
sock.on('connect_error', (e) => console.warn('[nextmin] schema service connect_error:', e?.message));
|
|
19
|
-
}
|
|
File without changes
|