@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.
@@ -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
- const load = React.useCallback(async () => {
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 res = await api.list(refModelLC, 0, pageSize, {});
19
- setItems(Array.isArray(res.data) ? res.data : []);
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
- void load();
31
- }, [load]);
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) => setForm((f) => ({ ...f, [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("label", { className: "text-sm font-medium", children: "Select Districts" }), _jsx(RefMultiSelect, { name: "districts", label: "Search & Select Districts", refModel: "Districts", value: selectedDistricts, onChange: (ids) => setSelectedDistricts(ids), pageSize: 64 }), _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("label", { className: "text-sm font-medium", children: "Select Speciality (Optional)" }), _jsx(RefSingleSelect, { name: "speciality", label: "Search & Select Speciality", refModel: "Specialities", showKey: "name", value: selectedSpecialityId, onChange: (id) => setSelectedSpecialityId(id || ''), pageSize: 50 }), _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" })] })] }) }) }));
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("label", { className: "text-sm font-medium", children: "Select Items" }), _jsx(RefMultiSelect, { name: "items", label: "Search & Select", refModel: schema.modelName, value: selectedIds, onChange: (ids) => setSelectedIds(ids), pageSize: 20 }), _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'
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"
@@ -6,6 +6,7 @@ export type AttributeBase = {
6
6
  ref?: string;
7
7
  longtext?: boolean;
8
8
  show?: string;
9
+ populateSlugFrom?: string;
9
10
  };
10
11
  export type ArrayAttribute = AttributeBase[];
11
12
  export type Attribute = AttributeBase | ArrayAttribute;
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-react",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",