@airoom/nextmin-react 2.0.1 → 2.0.3

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 (54) hide show
  1. package/README.md +57 -0
  2. package/dist/components/AddressAutocomplete.d.ts +4 -1
  3. package/dist/components/AddressAutocomplete.js +2 -2
  4. package/dist/components/AdminApp.js +86 -1
  5. package/dist/components/FileUploader.d.ts +3 -0
  6. package/dist/components/FileUploader.js +84 -28
  7. package/dist/components/PasswordInput.d.ts +2 -0
  8. package/dist/components/PasswordInput.js +2 -2
  9. package/dist/components/PhoneInput.d.ts +2 -0
  10. package/dist/components/PhoneInput.js +2 -2
  11. package/dist/components/QRCodeModal.d.ts +10 -0
  12. package/dist/components/QRCodeModal.js +153 -0
  13. package/dist/components/RefSelect.d.ts +4 -1
  14. package/dist/components/RefSelect.js +163 -65
  15. package/dist/components/SchemaForm.d.ts +2 -1
  16. package/dist/components/SchemaForm.js +132 -55
  17. package/dist/components/Sidebar.js +96 -7
  18. package/dist/components/TableFilters.js +1 -1
  19. package/dist/components/editor/TiptapEditor.js +46 -13
  20. package/dist/components/editor/Toolbar.js +195 -62
  21. package/dist/components/editor/components/CustomFontSizeModal.d.ts +13 -0
  22. package/dist/components/editor/components/CustomFontSizeModal.js +81 -0
  23. package/dist/components/editor/components/DistrictGridModal.js +1 -1
  24. package/dist/components/editor/components/SchemaInsertionModal.js +1 -1
  25. package/dist/components/editor/editor.css +2 -0
  26. package/dist/components/editor/extensions/FontSize.d.ts +19 -0
  27. package/dist/components/editor/extensions/FontSize.js +41 -0
  28. package/dist/components/viewer/DynamicViewer.js +50 -15
  29. package/dist/components/viewer/useViewDrawer.js +42 -2
  30. package/dist/hooks/useAppNavigation.d.ts +3 -0
  31. package/dist/hooks/useAppNavigation.js +37 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +1 -0
  34. package/dist/lib/QueryBuilder.d.ts +3 -1
  35. package/dist/lib/QueryBuilder.js +10 -0
  36. package/dist/lib/api.d.ts +6 -0
  37. package/dist/lib/api.js +45 -24
  38. package/dist/lib/schemaUtils.js +1 -1
  39. package/dist/lib/types.d.ts +6 -1
  40. package/dist/nextmin.css +1 -1
  41. package/dist/providers/NextMinProvider.d.ts +5 -1
  42. package/dist/providers/NextMinProvider.js +12 -2
  43. package/dist/providers/themeResolver.d.ts +25 -0
  44. package/dist/providers/themeResolver.js +174 -0
  45. package/dist/router/NextMinRouter.d.ts +0 -1
  46. package/dist/router/NextMinRouter.js +0 -1
  47. package/dist/views/CreateEditPage.js +20 -11
  48. package/dist/views/DashboardPage.js +1 -1
  49. package/dist/views/ListPage.js +37 -6
  50. package/dist/views/SettingsEdit.js +1 -1
  51. package/dist/views/list/DataTableHero.d.ts +5 -5
  52. package/dist/views/list/DataTableHero.js +154 -41
  53. package/dist/views/list/ListHeader.js +9 -1
  54. package/package.json +5 -2
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  export type RefSelectProps = {
2
3
  name: string;
3
4
  label: string;
@@ -12,5 +13,7 @@ export type RefSelectProps = {
12
13
  className?: string;
13
14
  pageSize?: number;
14
15
  where?: Record<string, any>;
16
+ isInvalid?: boolean;
17
+ errorMessage?: React.ReactNode;
15
18
  };
16
- export declare function RefSelect({ name, label, refModel, showKey, value, onChange, multiple, description, disabled, required, className, pageSize, where, }: RefSelectProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function RefSelect({ name, label, refModel, showKey, value, onChange, multiple, description, disabled, required, className, pageSize, where, isInvalid: isInvalidProp, errorMessage, }: RefSelectProps): import("react/jsx-runtime").JSX.Element;
@@ -1,93 +1,159 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import { Select, SelectItem, Chip, Spinner, Input } from '@heroui/react';
5
5
  import { useInfiniteScroll } from '@heroui/use-infinite-scroll';
6
6
  import { api } from '../lib/api';
7
- export function RefSelect({ name, label, refModel, showKey = 'name', value, onChange, multiple = false, description, disabled, required, className, pageSize = 20, where, }) {
7
+ export function RefSelect({ name, label, refModel, showKey = 'name', value, onChange, multiple = false, description, disabled, required, className, pageSize = 20, where, isInvalid: isInvalidProp, errorMessage, }) {
8
8
  const [isOpen, setIsOpen] = useState(false);
9
9
  const [items, setItems] = useState([]);
10
10
  const [hasMore, setHasMore] = useState(true);
11
11
  const [isLoading, setIsLoading] = useState(false);
12
+ const [isHydrating, setIsHydrating] = useState(false);
12
13
  const [offset, setOffset] = useState(0);
13
14
  const [searchFilter, setSearchFilter] = useState('');
14
15
  const [allSeenItems, setAllSeenItems] = useState({});
15
- const refModelLC = useMemo(() => refModel.toLowerCase(), [refModel]);
16
- // Normalize IDs to strings
17
- const normalizeId = (it) => {
16
+ const refModelLC = useMemo(() => (refModel ? String(refModel).toLowerCase() : ''), [refModel]);
17
+ // Normalize IDs to strings safely without producing "[object Object]"
18
+ const normalizeId = useCallback((it) => {
18
19
  if (it == null)
19
20
  return '';
20
21
  if (typeof it === 'string')
21
- return it;
22
+ return it.trim();
22
23
  if (typeof it === 'number')
23
24
  return String(it);
24
- // Handle objects like MongoDB ObjectIds
25
- const id = it?.id ?? it?._id ?? it;
26
- if (id == null)
25
+ // Handle objects like MongoDB ObjectIds or document objects
26
+ if (typeof it === 'object') {
27
+ if (typeof it.toHexString === 'function')
28
+ return it.toHexString();
29
+ const id = it?.id ?? it?._id;
30
+ if (id != null && id !== it) {
31
+ return normalizeId(id);
32
+ }
33
+ if (typeof it.toString === 'function' && it.toString !== Object.prototype.toString) {
34
+ const str = it.toString();
35
+ if (str && str !== '[object Object]')
36
+ return str;
37
+ }
27
38
  return '';
28
- if (typeof id === 'string')
29
- return id;
30
- if (typeof id.toHexString === 'function')
31
- return id.toHexString();
32
- if (typeof id.toString === 'function' && id !== it)
33
- return id.toString();
34
- return String(id);
35
- };
39
+ }
40
+ return String(it).trim();
41
+ }, []);
36
42
  const getLabel = useCallback((it) => {
37
- return String(it?.[showKey] ?? it?.name ?? it?.title ?? normalizeId(it));
38
- }, [showKey]);
39
- // Hydration: Fetch labels for initial values if not in list
43
+ if (it == null)
44
+ return '';
45
+ if (typeof it === 'string' || typeof it === 'number')
46
+ return String(it);
47
+ const val = it?.[showKey] ?? it?.name ?? it?.title ?? it?.label;
48
+ if (val != null && typeof val !== 'object')
49
+ return String(val);
50
+ const sid = normalizeId(it);
51
+ return sid;
52
+ }, [showKey, normalizeId]);
53
+ // Track which IDs have already been hydrated / in-flight to avoid duplicate fetches & loops
54
+ const hydratedIdsRef = React.useRef(new Set());
55
+ // Hydration: eagerly fetch labels for pre-selected values not yet in cache
40
56
  useEffect(() => {
57
+ const rawList = Array.isArray(value) ? value : value ? [value] : [];
58
+ if (rawList.length === 0)
59
+ return;
60
+ // Pre-seed any hydrated objects passed directly in value prop
61
+ const newSeen = {};
62
+ for (const v of rawList) {
63
+ if (v && typeof v === 'object') {
64
+ const sid = normalizeId(v);
65
+ const lbl = getLabel(v);
66
+ if (sid && lbl) {
67
+ newSeen[sid] = { id: sid, label: lbl, ...v };
68
+ hydratedIdsRef.current.add(sid);
69
+ }
70
+ }
71
+ }
72
+ if (Object.keys(newSeen).length > 0) {
73
+ setAllSeenItems(prev => ({ ...prev, ...newSeen }));
74
+ }
75
+ if (!refModelLC)
76
+ return;
77
+ // Only fetch IDs not already cached, pre-seeded, or previously fetched
78
+ const missingIds = rawList
79
+ .map(normalizeId)
80
+ .filter(id => !!id && !allSeenItems[id] && !newSeen[id] && !hydratedIdsRef.current.has(id));
81
+ if (missingIds.length === 0)
82
+ return;
83
+ // Mark as in-flight immediately to prevent duplicate requests
84
+ for (const id of missingIds)
85
+ hydratedIdsRef.current.add(id);
86
+ let isMounted = true;
41
87
  const fetchLabels = async () => {
42
- const ids = Array.isArray(value) ? value : value ? [value] : [];
43
- if (ids.length === 0)
44
- return;
45
- // Filter out IDs we already have labels for
46
- const missingIds = ids.filter(id => !items.some(it => it.id === id));
47
- if (missingIds.length === 0)
48
- return;
49
88
  try {
50
- const res = await api.list(refModelLC, {
51
- where: { id: { $in: missingIds } },
52
- limit: missingIds.length,
53
- });
54
- const docs = (res?.data ?? res);
55
- if (Array.isArray(docs)) {
56
- const newItems = docs.map(d => ({ id: normalizeId(d), label: getLabel(d), ...d }));
89
+ // Use a high limit so all selected IDs are fetched in one shot.
90
+ // Batch in chunks of 200 to stay within safe query limits.
91
+ const CHUNK = 200;
92
+ const selectProjection = Array.from(new Set(['id', '_id', showKey, 'name', 'title'])).join(',');
93
+ for (let i = 0; i < missingIds.length; i += CHUNK) {
94
+ const chunk = missingIds.slice(i, i + CHUNK);
95
+ const res = await api.list(refModelLC, {
96
+ where: { id: { $in: chunk } },
97
+ limit: chunk.length + 1, // +1 ensures no off-by-one truncation
98
+ page: 1,
99
+ select: selectProjection,
100
+ });
101
+ if (!isMounted)
102
+ return;
103
+ const docs = (res?.data ?? res);
104
+ if (!Array.isArray(docs))
105
+ continue;
106
+ const newItems = docs.map(d => ({
107
+ id: normalizeId(d),
108
+ label: getLabel(d),
109
+ ...d,
110
+ })).filter(d => Boolean(d.id));
57
111
  setAllSeenItems(prev => {
58
112
  const next = { ...prev };
59
113
  for (const item of newItems) {
60
- next[item.id] = item;
114
+ if (item.id)
115
+ next[item.id] = item;
61
116
  }
62
117
  return next;
63
118
  });
64
119
  setItems(prev => {
65
120
  const existingIds = new Set(prev.map(i => i.id));
66
- const uniqueNew = newItems.filter(i => !existingIds.has(i.id));
67
- return [...prev, ...uniqueNew];
121
+ const uniqueNew = newItems.filter(i => i.id && !existingIds.has(i.id));
122
+ return uniqueNew.length ? [...prev, ...uniqueNew] : prev;
68
123
  });
69
124
  }
70
125
  }
71
126
  catch (err) {
72
- console.error('Failed to hydrate RefSelect labels', err);
127
+ // Log warning and retain IDs in hydrated set to prevent infinite retry render loops
128
+ console.warn(`RefSelect: failed to hydrate labels for model "${refModel}"`, err);
73
129
  }
74
130
  };
75
- if (refModelLC) {
76
- fetchLabels();
77
- }
78
- }, [value, refModelLC, getLabel]); // Run once on value change if items missing
131
+ setIsHydrating(true);
132
+ fetchLabels().finally(() => {
133
+ if (isMounted)
134
+ setIsHydrating(false);
135
+ });
136
+ return () => {
137
+ isMounted = false;
138
+ };
139
+ // eslint-disable-next-line react-hooks/exhaustive-deps
140
+ }, [value, refModelLC, showKey, getLabel, normalizeId]);
79
141
  const loadMore = useCallback(async (isSearch = false) => {
80
142
  if (isLoading || (!hasMore && !isSearch))
81
143
  return;
144
+ if (!refModelLC)
145
+ return;
82
146
  setIsLoading(true);
83
147
  try {
84
148
  const effectivePageSize = Number(pageSize) || 20;
85
149
  const currentOffset = isSearch ? 0 : offset;
150
+ const selectProjection = Array.from(new Set(['id', '_id', showKey, 'name', 'title'])).join(',');
86
151
  const params = {
87
152
  limit: effectivePageSize,
88
153
  page: Math.floor(currentOffset / effectivePageSize) + 1,
89
154
  sort: showKey,
90
155
  sortType: 'asc',
156
+ select: selectProjection,
91
157
  };
92
158
  if (searchFilter.length >= 1) {
93
159
  params.where = {
@@ -101,12 +167,13 @@ export function RefSelect({ name, label, refModel, showKey = 'name', value, onCh
101
167
  const res = await api.list(refModelLC, params);
102
168
  const docs = (res?.data ?? res);
103
169
  const normalized = Array.isArray(docs)
104
- ? docs.map(d => ({ id: normalizeId(d), label: getLabel(d), ...d }))
170
+ ? docs.map(d => ({ id: normalizeId(d), label: getLabel(d), ...d })).filter(d => Boolean(d.id))
105
171
  : [];
106
172
  setAllSeenItems(prev => {
107
173
  const next = { ...prev };
108
174
  for (const item of normalized) {
109
- next[item.id] = item;
175
+ if (item.id)
176
+ next[item.id] = item;
110
177
  }
111
178
  return next;
112
179
  });
@@ -114,19 +181,19 @@ export function RefSelect({ name, label, refModel, showKey = 'name', value, onCh
114
181
  if (isSearch)
115
182
  return normalized;
116
183
  const existingIds = new Set(prev.map(i => i.id));
117
- const uniqueNew = normalized.filter(i => !existingIds.has(i.id));
184
+ const uniqueNew = normalized.filter(i => i.id && !existingIds.has(i.id));
118
185
  return [...prev, ...uniqueNew];
119
186
  });
120
187
  setHasMore(normalized.length === effectivePageSize);
121
188
  setOffset(prev => isSearch ? effectivePageSize : prev + effectivePageSize);
122
189
  }
123
190
  catch (err) {
124
- console.error('Failed to load RefSelect items', err);
191
+ console.warn(`Failed to load RefSelect items for model "${refModel}"`, err);
125
192
  }
126
193
  finally {
127
194
  setIsLoading(false);
128
195
  }
129
- }, [isLoading, hasMore, offset, pageSize, searchFilter, refModelLC, showKey, getLabel]);
196
+ }, [isLoading, hasMore, offset, pageSize, searchFilter, refModelLC, showKey, getLabel, normalizeId, where, refModel]);
130
197
  const [, scrollerRef] = useInfiniteScroll({
131
198
  hasMore,
132
199
  isEnabled: isOpen,
@@ -154,7 +221,7 @@ export function RefSelect({ name, label, refModel, showKey = 'name', value, onCh
154
221
  const onSelectionChange = (keys) => {
155
222
  if (keys === 'all')
156
223
  return;
157
- const selectedIds = Array.from(keys).map(String);
224
+ const selectedIds = Array.from(keys).map(String).filter(Boolean);
158
225
  if (multiple) {
159
226
  onChange(selectedIds);
160
227
  }
@@ -164,24 +231,39 @@ export function RefSelect({ name, label, refModel, showKey = 'name', value, onCh
164
231
  };
165
232
  const removeValue = (idToRemove) => {
166
233
  if (Array.isArray(value)) {
167
- onChange(value.filter(id => id !== idToRemove));
234
+ onChange(value.filter(id => normalizeId(id) !== idToRemove));
168
235
  }
169
236
  };
170
237
  const selectedKeys = useMemo(() => {
238
+ const rawIds = Array.isArray(value) ? value : value ? [value] : [];
239
+ const normIds = rawIds.map(normalizeId).filter(Boolean);
171
240
  if (multiple) {
172
- return new Set(Array.isArray(value) ? value.map(String) : []);
241
+ return new Set(normIds);
173
242
  }
174
- return (value ? new Set([String(value)]) : new Set());
175
- }, [value, multiple]);
243
+ return (normIds.length > 0 ? new Set([normIds[0]]) : new Set());
244
+ }, [value, multiple, normalizeId]);
176
245
  const selectedItems = useMemo(() => {
177
246
  const ids = Array.isArray(value) ? value : value ? [value] : [];
178
247
  return ids.map(id => {
179
- const found = items.find(it => it.id === id);
248
+ const sid = normalizeId(id);
249
+ if (!sid)
250
+ return { id: '', label: '' };
251
+ // Check cache first (populated by hydration fetch / object pre-seeding)
252
+ if (allSeenItems[sid])
253
+ return allSeenItems[sid];
254
+ // Check if object has embedded label
255
+ if (id && typeof id === 'object') {
256
+ const lbl = getLabel(id);
257
+ if (lbl)
258
+ return { id: sid, label: lbl, ...id };
259
+ }
260
+ // Then check current page items
261
+ const found = items.find(it => it.id === sid);
180
262
  if (found)
181
263
  return found;
182
- return { id: String(id), label: String(id) };
183
- });
184
- }, [value, items]);
264
+ return { id: sid, label: sid };
265
+ }).filter(it => Boolean(it.id));
266
+ }, [value, items, allSeenItems, getLabel, normalizeId]);
185
267
  const isValueEmpty = useMemo(() => {
186
268
  if (multiple)
187
269
  return !Array.isArray(value) || value.length === 0;
@@ -193,10 +275,15 @@ export function RefSelect({ name, label, refModel, showKey = 'name', value, onCh
193
275
  // 1. Add currently selected items (from allSeenItems cache)
194
276
  const selectedIds = Array.isArray(value) ? value : value ? [value] : [];
195
277
  for (const id of selectedIds) {
196
- const stringId = String(id);
278
+ const stringId = normalizeId(id);
279
+ if (!stringId)
280
+ continue;
197
281
  if (allSeenItems[stringId]) {
198
282
  itemMap.set(stringId, allSeenItems[stringId]);
199
283
  }
284
+ else if (id && typeof id === 'object') {
285
+ itemMap.set(stringId, { id: stringId, label: getLabel(id), ...id });
286
+ }
200
287
  else {
201
288
  // Fallback for cases where label isn't loaded yet
202
289
  itemMap.set(stringId, { id: stringId, label: stringId });
@@ -204,22 +291,33 @@ export function RefSelect({ name, label, refModel, showKey = 'name', value, onCh
204
291
  }
205
292
  // 2. Add current results (highest priority for labels/data)
206
293
  for (const item of items) {
207
- itemMap.set(item.id, item);
294
+ if (item.id) {
295
+ itemMap.set(item.id, item);
296
+ }
208
297
  }
209
298
  return Array.from(itemMap.values());
210
- }, [items, value, allSeenItems]);
211
- return (_jsx("div", { className: `flex flex-col gap-2 ${className}`, children: _jsx(Select, { label: label, placeholder: `Select ${refModel}...`, variant: "bordered", labelPlacement: "outside", isMultiline: multiple, items: mergedDisplayItems, isLoading: isLoading, scrollRef: scrollerRef, selectionMode: multiple ? 'multiple' : 'single', selectedKeys: selectedKeys, onSelectionChange: onSelectionChange, onOpenChange: onOpenChange, isDisabled: disabled, isRequired: required, isInvalid: required && isValueEmpty, validationBehavior: "aria", description: description, className: "w-full", classNames: {
299
+ }, [items, value, allSeenItems, getLabel, normalizeId]);
300
+ const placeholderText = refModel ? `Select ${refModel}...` : `Select ${label}...`;
301
+ return (_jsx("div", { className: `flex flex-col gap-2 ${className}`, children: _jsx(Select, { label: label, placeholder: placeholderText, variant: "bordered", labelPlacement: "outside", isMultiline: multiple, items: mergedDisplayItems, isLoading: isLoading || isHydrating, scrollRef: scrollerRef, selectionMode: multiple ? 'multiple' : 'single', selectedKeys: selectedKeys, onSelectionChange: onSelectionChange, onOpenChange: onOpenChange, isDisabled: disabled, isRequired: required, isInvalid: isInvalidProp || (required && isValueEmpty), errorMessage: errorMessage, validationBehavior: "aria", description: description, className: "w-full", classNames: {
212
302
  trigger: 'min-h-12 py-2',
213
303
  }, scrollShadowProps: {
214
304
  isEnabled: false,
215
305
  }, listboxProps: {
216
306
  topContent: (_jsx("div", { className: "px-2 pt-2 pb-1 sticky top-0 z-20 bg-content1 shadow-sm pb-2 -mt-2 -mx-1", children: _jsx(Input, { placeholder: "Search...", size: "sm", variant: "bordered", value: searchFilter, onChange: onInputChange, onClear: () => setSearchFilter(''), isClearable: true,
217
- // Prevent closing when clicking search
218
- onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), endContent: isLoading ? _jsx(Spinner, { size: "sm" }) : null }) })),
307
+ // Prevent closing when clicking search without triggering React Spectrum warning
308
+ onClick: (e) => {
309
+ if (typeof e?.continuePropagation === 'function')
310
+ return;
311
+ e?.stopPropagation?.();
312
+ }, onKeyDown: (e) => {
313
+ if (typeof e?.continuePropagation === 'function')
314
+ return;
315
+ e?.stopPropagation?.();
316
+ }, endContent: (isLoading || isHydrating) ? _jsx(Spinner, { size: "sm" }) : null }) })),
219
317
  }, renderValue: (items) => {
220
318
  if (multiple) {
221
- return (_jsx("div", { className: "flex flex-wrap gap-1", children: items.map((item) => (_jsx(Chip, { size: "sm", variant: "flat", children: item.textValue }, item.key))) }));
319
+ return (_jsx("div", { className: "flex flex-wrap gap-1", children: items.map((item) => (_jsx(Chip, { size: "sm", variant: "flat", children: item.textValue || item.key }, item.key))) }));
222
320
  }
223
- return items[0].textValue;
224
- }, children: (item) => (_jsx(SelectItem, { textValue: item.label, children: item.label }, item.id)) }) }));
321
+ return items[0]?.textValue ?? '';
322
+ }, children: (item) => (_jsx(SelectItem, { textValue: item.label || item.id, children: item.label || item.id }, item.id)) }) }));
225
323
  }
@@ -13,7 +13,8 @@ type SchemaFormProps = {
13
13
  showReset?: boolean;
14
14
  onSubmit: (values: Record<string, any>) => void | Promise<void>;
15
15
  fieldErrors?: FieldErrorItems[] | undefined;
16
+ isEdit?: boolean;
16
17
  };
17
18
  /** --------------------------------------------------------------------------------------- **/
18
- export declare function SchemaForm({ model, schemaOverride, initialValues, submitLabel, busy, showReset, onSubmit, fieldErrors, }: SchemaFormProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function SchemaForm({ model, schemaOverride, initialValues, submitLabel, busy, showReset, onSubmit, fieldErrors, isEdit: isEditProp, }: SchemaFormProps): import("react/jsx-runtime").JSX.Element;
19
20
  export {};