@airoom/nextmin-react 1.4.4 → 1.4.6
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/dist/components/RefMultiSelect.js +5 -1
- package/dist/components/RefSingleSelect.js +67 -9
- package/dist/components/SchemaForm.js +104 -4
- package/dist/components/editor/components/DistrictGridModal.js +1 -1
- package/dist/components/editor/components/SchemaInsertionModal.js +1 -1
- package/dist/lib/types.d.ts +1 -0
- package/dist/state/schemasSlice.js +31 -2
- package/package.json +1 -1
|
@@ -54,6 +54,8 @@ export const RefMultiSelect = ({ name, label, refModel, showKey = 'name', value,
|
|
|
54
54
|
try {
|
|
55
55
|
const params = {
|
|
56
56
|
limit: pageSize,
|
|
57
|
+
sort: showKey,
|
|
58
|
+
sortType: 'asc',
|
|
57
59
|
...(q ? { q, searchKey: showKey } : {}),
|
|
58
60
|
};
|
|
59
61
|
const res = await api.list?.(modelSlug, 0, pageSize, params);
|
|
@@ -91,10 +93,12 @@ export const RefMultiSelect = ({ name, label, refModel, showKey = 'name', value,
|
|
|
91
93
|
if (keys === 'all')
|
|
92
94
|
return;
|
|
93
95
|
onChange(Array.from(keys).map(String));
|
|
94
|
-
}, 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: {
|
|
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: {
|
|
95
97
|
topContent: topSearch,
|
|
96
98
|
emptyContent: loading ? undefined : 'No results',
|
|
97
99
|
className: '',
|
|
100
|
+
}, scrollShadowProps: {
|
|
101
|
+
isEnabled: false,
|
|
98
102
|
},
|
|
99
103
|
// chips like docs; items is an array (SelectedItems<T>)
|
|
100
104
|
renderValue: (items) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import { Select, SelectItem } from '@heroui/react';
|
|
4
|
+
import { Select, SelectItem, Input } from '@heroui/react';
|
|
5
5
|
import { api } from '../lib/api';
|
|
6
6
|
function stripPointerNone(cls) {
|
|
7
7
|
return (cls ?? '').replace(/\bpointer-events-none\b/g, '').trim();
|
|
@@ -10,25 +10,75 @@ export function RefSingleSelect({ name, label, refModel, showKey = 'name', descr
|
|
|
10
10
|
const [loading, setLoading] = React.useState(false);
|
|
11
11
|
const [items, setItems] = React.useState([]);
|
|
12
12
|
const [error, setError] = React.useState(null);
|
|
13
|
+
const [query, setQuery] = React.useState('');
|
|
14
|
+
const [open, setOpen] = React.useState(false);
|
|
13
15
|
const refModelLC = React.useMemo(() => refModel.toLowerCase(), [refModel]);
|
|
14
|
-
|
|
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) => {
|
|
15
25
|
try {
|
|
16
26
|
setLoading(true);
|
|
17
27
|
setError(null);
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
+
});
|
|
20
56
|
}
|
|
21
57
|
catch (e) {
|
|
22
58
|
setError(e?.message || 'Failed to load options');
|
|
23
|
-
setItems([]);
|
|
24
59
|
}
|
|
25
60
|
finally {
|
|
26
61
|
setLoading(false);
|
|
27
62
|
}
|
|
28
|
-
}, [refModelLC, pageSize]);
|
|
63
|
+
}, [refModelLC, pageSize, showKey, normalize]);
|
|
64
|
+
// load on open
|
|
29
65
|
React.useEffect(() => {
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
32
82
|
const getId = (r) => (typeof r.id === 'string' && r.id) ||
|
|
33
83
|
(typeof r._id === 'string' && r._id) ||
|
|
34
84
|
'';
|
|
@@ -36,14 +86,22 @@ export function RefSingleSelect({ name, label, refModel, showKey = 'name', descr
|
|
|
36
86
|
const v = r?.[showKey];
|
|
37
87
|
return v == null ? getId(r) : String(v);
|
|
38
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() }) }));
|
|
39
90
|
return (_jsx("div", { className: "w-full", children: _jsx(Select, { name: name, label: label, placeholder: label, labelPlacement: "outside", className: className, classNames: {
|
|
40
91
|
// Make sure the whole trigger is clickable
|
|
41
92
|
trigger: `cursor-pointer ${stripPointerNone(classNames?.trigger)}`,
|
|
42
|
-
}, variant: "bordered", isDisabled: disabled || loading, isLoading: loading, isRequired: required, description: error || description, items: items, selectionMode: "single", selectedKeys: value ? new Set([String(value)]) : new Set(), onSelectionChange: (keys) => {
|
|
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) => {
|
|
43
94
|
if (keys === 'all')
|
|
44
95
|
return;
|
|
45
96
|
const v = Array.from(keys)[0];
|
|
46
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)));
|
|
47
105
|
}, children: (opt) => {
|
|
48
106
|
const key = getId(opt);
|
|
49
107
|
const text = getLabel(opt);
|
|
@@ -11,6 +11,7 @@ import { PhoneInput } from './PhoneInput';
|
|
|
11
11
|
import { FileUploader } from './FileUploader';
|
|
12
12
|
import AddressAutocompleteGoogle from './AddressAutocomplete';
|
|
13
13
|
import { useGoogleMapsKey } from '../hooks/useGoogleMapsKey';
|
|
14
|
+
import { api } from '../lib/api';
|
|
14
15
|
import { parseDate, parseDateTime, parseTime, } from '@internationalized/date';
|
|
15
16
|
// import RichTextEditor from './editor/RichTextEditor';
|
|
16
17
|
// import ShadcnEditor from './editor/ShadcnEditor';
|
|
@@ -258,6 +259,11 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
258
259
|
const sessionRole = useMemo(() => {
|
|
259
260
|
if (!effectiveUser)
|
|
260
261
|
return [];
|
|
262
|
+
// 1. Check direct roleName field if backend provided it
|
|
263
|
+
const rn = effectiveUser.roleName;
|
|
264
|
+
if (typeof rn === 'string' && rn)
|
|
265
|
+
return [rn.toLowerCase()];
|
|
266
|
+
// 2. Fallback to existing logic
|
|
261
267
|
const raw = effectiveUser.roles ?? effectiveUser.role;
|
|
262
268
|
const toName = (r) => {
|
|
263
269
|
if (!r)
|
|
@@ -269,14 +275,38 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
269
275
|
return null;
|
|
270
276
|
};
|
|
271
277
|
if (Array.isArray(raw)) {
|
|
272
|
-
return raw.map(toName).filter((s) => !!s);
|
|
278
|
+
return raw.map(toName).filter((s) => !!s).map(s => s.toLowerCase());
|
|
273
279
|
}
|
|
274
280
|
const single = toName(raw);
|
|
275
|
-
return single ? [single] : [];
|
|
281
|
+
return single ? [single.toLowerCase()] : [];
|
|
276
282
|
}, [effectiveUser]);
|
|
277
283
|
const schema = useMemo(() => schemaOverride ??
|
|
278
284
|
items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model, schemaOverride]);
|
|
279
285
|
const [form, setForm] = useState(initialValues ?? {});
|
|
286
|
+
// For auto-slugging
|
|
287
|
+
const [manuallyEditedSlugs, setManuallyEditedSlugs] = useState(new Set());
|
|
288
|
+
const [pendingSlugChecks, setPendingSlugChecks] = useState({});
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const timer = setTimeout(async () => {
|
|
291
|
+
const entries = Object.entries(pendingSlugChecks);
|
|
292
|
+
if (entries.length === 0)
|
|
293
|
+
return;
|
|
294
|
+
const newValues = {};
|
|
295
|
+
for (const [targetName, baseSlug] of entries) {
|
|
296
|
+
if (!baseSlug)
|
|
297
|
+
continue;
|
|
298
|
+
const uniqueSlug = await findUniqueSlug(model, baseSlug, form.id || form._id);
|
|
299
|
+
if (uniqueSlug !== form[targetName]) {
|
|
300
|
+
newValues[targetName] = uniqueSlug;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (Object.keys(newValues).length > 0) {
|
|
304
|
+
setForm((f) => ({ ...f, ...newValues }));
|
|
305
|
+
}
|
|
306
|
+
setPendingSlugChecks({});
|
|
307
|
+
}, 600); // 600ms debounce
|
|
308
|
+
return () => clearTimeout(timer);
|
|
309
|
+
}, [pendingSlugChecks, model, form.id, form._id, form]);
|
|
280
310
|
const [error, setError] = useState();
|
|
281
311
|
const isEdit = useMemo(() => !!(initialValues &&
|
|
282
312
|
(initialValues.id || initialValues._id)), [initialValues]);
|
|
@@ -318,7 +348,44 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
318
348
|
.filter(([name, attr]) => !(isEdit && isPasswordAttr(name, attr)))
|
|
319
349
|
.map(([name, attr]) => ({ name, attr })), [schema, canBypassPrivacy, isEdit]);
|
|
320
350
|
const gridClass = 'grid gap-4 grid-cols-2';
|
|
321
|
-
const handleChange = (name, value) =>
|
|
351
|
+
const handleChange = (name, value) => {
|
|
352
|
+
setForm((f) => {
|
|
353
|
+
const next = { ...f, [name]: value };
|
|
354
|
+
if (schema) {
|
|
355
|
+
// 1. If we edited a target field directly, mark it as manually edited
|
|
356
|
+
const attr = schema.attributes[name];
|
|
357
|
+
const head = Array.isArray(attr) ? attr[0] : attr;
|
|
358
|
+
if (head && head.populateSlugFrom) {
|
|
359
|
+
setManuallyEditedSlugs((prev) => new Set(prev).add(name));
|
|
360
|
+
}
|
|
361
|
+
// 2. If we edited a source field, update its targets
|
|
362
|
+
for (const [fname, fattr] of Object.entries(schema.attributes)) {
|
|
363
|
+
const fhead = Array.isArray(fattr) ? fattr[0] : fattr;
|
|
364
|
+
const populateFrom = fhead?.populateSlugFrom;
|
|
365
|
+
if (populateFrom) {
|
|
366
|
+
const sources = String(populateFrom)
|
|
367
|
+
.split(',')
|
|
368
|
+
.map((s) => s.trim());
|
|
369
|
+
if (sources.includes(name) && !manuallyEditedSlugs.has(fname)) {
|
|
370
|
+
const baseText = sources
|
|
371
|
+
.map((s) => next[s] || '')
|
|
372
|
+
.filter(Boolean)
|
|
373
|
+
.join(' ');
|
|
374
|
+
if (baseText) {
|
|
375
|
+
const baseSlug = slugify(baseText);
|
|
376
|
+
next[fname] = baseSlug;
|
|
377
|
+
setPendingSlugChecks((prev) => ({ ...prev, [fname]: baseSlug }));
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
next[fname] = '';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return next;
|
|
387
|
+
});
|
|
388
|
+
};
|
|
322
389
|
const handleSubmit = async (e) => {
|
|
323
390
|
e.preventDefault();
|
|
324
391
|
setError(undefined);
|
|
@@ -421,7 +488,9 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
421
488
|
function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, mapsKey, availableSchemas, formState, }) {
|
|
422
489
|
const id = `${uid}-${name}`;
|
|
423
490
|
const label = attr?.label ?? formatLabel(name);
|
|
424
|
-
const required = !!attr?.required
|
|
491
|
+
const required = !!attr?.required ||
|
|
492
|
+
name.toLowerCase() === 'slug' ||
|
|
493
|
+
!!attr?.populateSlugFrom;
|
|
425
494
|
const description = attr?.description;
|
|
426
495
|
// Prefer ref over enum. Only use enum when there is NO ref.
|
|
427
496
|
const enumVals = attr?.ref
|
|
@@ -718,3 +787,34 @@ function getPopulateTarget(attr) {
|
|
|
718
787
|
const t = head?.populate ?? attr?.populate;
|
|
719
788
|
return typeof t === 'string' && t.trim() ? t.trim() : null;
|
|
720
789
|
}
|
|
790
|
+
function slugify(text) {
|
|
791
|
+
return text
|
|
792
|
+
.toLowerCase()
|
|
793
|
+
.trim()
|
|
794
|
+
.replace(/[\s_]+/g, '-')
|
|
795
|
+
.replace(/[^\w\-]+/g, '')
|
|
796
|
+
.replace(/-+/g, '-')
|
|
797
|
+
.replace(/^-+|-+$/g, '');
|
|
798
|
+
}
|
|
799
|
+
async function findUniqueSlug(model, baseSlug, currentId) {
|
|
800
|
+
let attempt = 0;
|
|
801
|
+
while (true) {
|
|
802
|
+
const checkSlug = attempt === 0 ? baseSlug : `${baseSlug}-${attempt}`;
|
|
803
|
+
try {
|
|
804
|
+
// Check if slug exists in the model
|
|
805
|
+
const res = await api.list(model, 0, 1, { slug: checkSlug });
|
|
806
|
+
const existing = res.data?.[0];
|
|
807
|
+
if (!existing ||
|
|
808
|
+
(currentId && String(existing.id || existing._id) === String(currentId))) {
|
|
809
|
+
return checkSlug;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
catch (e) {
|
|
813
|
+
console.error('Slug check failed', e);
|
|
814
|
+
return checkSlug; // Fallback to current if API fails
|
|
815
|
+
}
|
|
816
|
+
attempt++;
|
|
817
|
+
if (attempt > 100)
|
|
818
|
+
return checkSlug; // Safety break
|
|
819
|
+
}
|
|
820
|
+
}
|
|
@@ -68,5 +68,5 @@ export const DistrictGridModal = ({ isOpen, onClose, onInsert, currentSpeciality
|
|
|
68
68
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
69
69
|
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900"), children: "Doctors" }), _jsx("button", { onClick: () => setBaseType('hospitals'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", baseType === 'hospitals'
|
|
70
70
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
71
|
-
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900"), children: "Hospitals" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(
|
|
71
|
+
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900"), children: "Hospitals" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(RefMultiSelect, { name: "districts", label: "Search & Select Districts", refModel: "Districts", value: selectedDistricts, onChange: (ids) => setSelectedDistricts(ids), pageSize: 1000 }), _jsx("p", { className: "text-xs text-gray-400", children: "Select one or more districts to create grid items." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(RefSingleSelect, { name: "speciality", label: "Search & Select Speciality", refModel: "Specialities", showKey: "name", value: selectedSpecialityId, onChange: (id) => setSelectedSpecialityId(id || ''), pageSize: 10000 }), _jsx("p", { className: "text-xs text-gray-400", children: "Leave empty to create URLs without speciality." }), specialitySlug && (_jsx("div", { className: "mt-1 px-2 py-1 bg-primary-50 dark:bg-primary-900/20 rounded border border-primary-200 dark:border-primary-800", children: _jsxs("p", { className: "text-xs text-primary-700 dark:text-primary-400", children: ["Slug: ", _jsx("code", { className: "font-mono", children: specialitySlug })] }) }))] }), selectedDistricts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "URL Preview" }), _jsxs("div", { className: "px-3 py-2 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 max-h-32 overflow-y-auto", children: [_jsx("p", { className: "text-xs text-gray-500 mb-2", children: "Example URL pattern (actual district slugs will be used):" }), _jsx("code", { className: "text-xs text-gray-700 dark:text-gray-300", children: getPreviewUrl('[district-slug]') })] })] }))] }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "flat", onPress: onClose, children: "Cancel" }), _jsx(Button, { color: "primary", onPress: handleInsert, isDisabled: selectedDistricts.length === 0, children: "Insert Grid" })] })] }) }) }));
|
|
72
72
|
};
|
|
@@ -83,7 +83,7 @@ export const SchemaInsertionModal = ({ isOpen, onClose, onInsert, schema, availa
|
|
|
83
83
|
base: "bg-white dark:bg-zinc-950 border border-gray-200 dark:border-gray-800",
|
|
84
84
|
header: "border-b border-gray-200 dark:border-gray-800",
|
|
85
85
|
footer: "border-t border-gray-200 dark:border-gray-800",
|
|
86
|
-
}, children: _jsx(ModalContent, { children: _jsxs(_Fragment, { children: [_jsxs(ModalHeader, { children: ["Insert ", schema.modelName] }), _jsxs(ModalBody, { className: "py-6 flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(
|
|
86
|
+
}, children: _jsx(ModalContent, { children: _jsxs(_Fragment, { children: [_jsxs(ModalHeader, { children: ["Insert ", schema.modelName] }), _jsxs(ModalBody, { className: "py-6 flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(RefMultiSelect, { name: "items", label: "Search & Select", refModel: schema.modelName, value: selectedIds, onChange: (ids) => setSelectedIds(ids), pageSize: 10000 }), _jsx("p", { className: "text-xs text-gray-400", children: "Leave empty to fetch latest items automatically." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "View Type" }), _jsxs("div", { className: "flex gap-4", children: [_jsx("button", { onClick: () => setViewType('grid'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", viewType === 'grid'
|
|
87
87
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
88
88
|
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50"), children: "Grid (Pills)" }), _jsx("button", { onClick: () => setViewType('list'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", viewType === 'list'
|
|
89
89
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -14,12 +14,41 @@ export const fetchSchemas = createAsyncThunk('schemas/fetch', async () => {
|
|
|
14
14
|
return status === 'idle';
|
|
15
15
|
},
|
|
16
16
|
});
|
|
17
|
+
const mergeSchemas = (existing, incoming) => {
|
|
18
|
+
if (!existing.length)
|
|
19
|
+
return incoming;
|
|
20
|
+
return incoming.map((newItem) => {
|
|
21
|
+
const oldItem = existing.find((s) => s.modelName === newItem.modelName);
|
|
22
|
+
if (!oldItem)
|
|
23
|
+
return newItem;
|
|
24
|
+
// Perform a smart merge of attributes
|
|
25
|
+
const mergedAttrs = { ...newItem.attributes };
|
|
26
|
+
let restoredCount = 0;
|
|
27
|
+
for (const [key, oldAttrVal] of Object.entries(oldItem.attributes)) {
|
|
28
|
+
const oldAttr = Array.isArray(oldAttrVal) ? oldAttrVal[0] : oldAttrVal;
|
|
29
|
+
const newAttrVal = mergedAttrs[key];
|
|
30
|
+
const newAttr = Array.isArray(newAttrVal) ? newAttrVal[0] : newAttrVal;
|
|
31
|
+
const isOldPrivate = oldAttr && oldAttr.private;
|
|
32
|
+
const isNewPrivate = newAttr && newAttr.private;
|
|
33
|
+
// If the old one had the private field and the new one doesn't,
|
|
34
|
+
// the new one is likely a "stripped" public schema. Restore the private one.
|
|
35
|
+
if (isOldPrivate && !isNewPrivate) {
|
|
36
|
+
mergedAttrs[key] = oldAttrVal;
|
|
37
|
+
restoredCount++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (restoredCount > 0) {
|
|
41
|
+
console.log(`[NextMin] Restored ${restoredCount} private fields for ${newItem.modelName} from previous high-quality state.`);
|
|
42
|
+
}
|
|
43
|
+
return { ...newItem, attributes: mergedAttrs };
|
|
44
|
+
}).concat(existing.filter((o) => !incoming.find((n) => n.modelName === o.modelName)));
|
|
45
|
+
};
|
|
17
46
|
const schemasSlice = createSlice({
|
|
18
47
|
name: 'schemas',
|
|
19
48
|
initialState,
|
|
20
49
|
reducers: {
|
|
21
50
|
setSchemas(state, action) {
|
|
22
|
-
state.items = action.payload;
|
|
51
|
+
state.items = mergeSchemas(state.items, action.payload);
|
|
23
52
|
state.status = 'succeeded';
|
|
24
53
|
},
|
|
25
54
|
},
|
|
@@ -31,7 +60,7 @@ const schemasSlice = createSlice({
|
|
|
31
60
|
})
|
|
32
61
|
.addCase(fetchSchemas.fulfilled, (state, action) => {
|
|
33
62
|
state.status = 'succeeded';
|
|
34
|
-
state.items = action.payload;
|
|
63
|
+
state.items = mergeSchemas(state.items, action.payload);
|
|
35
64
|
})
|
|
36
65
|
.addCase(fetchSchemas.rejected, (state, action) => {
|
|
37
66
|
state.status = 'failed';
|