@airoom/nextmin-react 0.1.7 → 0.1.9
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/FileUploader.js +0 -1
- package/dist/components/RefMultiSelect.d.ts +10 -2
- package/dist/components/RefMultiSelect.js +78 -45
- package/dist/components/RefSingleSelect.js +11 -11
- package/dist/components/SchemaForm.d.ts +7 -1
- package/dist/components/SchemaForm.js +110 -4
- package/dist/lib/api.js +22 -15
- package/dist/nextmin.css +1 -1
- package/dist/views/CreateEditPage.js +15 -1
- package/package.json +1 -1
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
type RefOption = {
|
|
3
|
+
id?: string;
|
|
4
|
+
_id?: string;
|
|
5
|
+
[k: string]: any;
|
|
6
|
+
};
|
|
2
7
|
export type RefMultiSelectProps = {
|
|
3
8
|
name: string;
|
|
4
9
|
label: string;
|
|
5
10
|
refModel: string;
|
|
6
11
|
showKey?: string;
|
|
7
|
-
value: string[];
|
|
12
|
+
value: RefOption[] | string[];
|
|
8
13
|
onChange: (ids: string[]) => void;
|
|
9
14
|
description?: string;
|
|
10
15
|
disabled?: boolean;
|
|
11
16
|
required?: boolean;
|
|
12
|
-
pageSize?: number;
|
|
17
|
+
pageSize?: number | undefined;
|
|
18
|
+
error?: boolean | undefined;
|
|
19
|
+
errorMessage?: string | undefined;
|
|
13
20
|
};
|
|
14
21
|
export declare const RefMultiSelect: React.FC<RefMultiSelectProps>;
|
|
22
|
+
export {};
|
|
@@ -1,38 +1,61 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Select, SelectItem, Input, Chip } from '@heroui/react';
|
|
5
5
|
import { api } from '../lib/api';
|
|
6
|
-
export const RefMultiSelect = ({ name, label, refModel, showKey = 'name', value, onChange, description, disabled, required, pageSize =
|
|
7
|
-
const [open, setOpen] = React.useState(false);
|
|
8
|
-
const [query, setQuery] = React.useState('');
|
|
9
|
-
const [loading, setLoading] = React.useState(false);
|
|
10
|
-
const [options, setOptions] = React.useState([]);
|
|
6
|
+
export const RefMultiSelect = ({ name, label, refModel, showKey = 'name', value, onChange, description, disabled, required, pageSize = 10000, error, errorMessage, }) => {
|
|
11
7
|
const modelSlug = React.useMemo(() => String(refModel).trim().toLowerCase(), [refModel]);
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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] ??
|
|
18
42
|
opt?.name ??
|
|
19
43
|
opt?.title ??
|
|
20
44
|
opt?.key ??
|
|
21
45
|
opt?.id ??
|
|
22
46
|
opt?._id ??
|
|
23
47
|
''), [showKey]);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const loadOptions = React.useCallback(async (q) => {
|
|
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) => {
|
|
30
53
|
setLoading(true);
|
|
31
54
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
? { q, searchKey: showKey
|
|
35
|
-
|
|
55
|
+
const params = {
|
|
56
|
+
limit: pageSize,
|
|
57
|
+
...(q ? { q, searchKey: showKey } : {}),
|
|
58
|
+
};
|
|
36
59
|
const res = await api.list?.(modelSlug, 0, pageSize, params);
|
|
37
60
|
const payload = res?.data ?? res;
|
|
38
61
|
const list = payload?.items ??
|
|
@@ -40,37 +63,47 @@ export const RefMultiSelect = ({ name, label, refModel, showKey = 'name', value,
|
|
|
40
63
|
payload?.results ??
|
|
41
64
|
payload?.list ??
|
|
42
65
|
(Array.isArray(payload) ? payload : []);
|
|
43
|
-
const normalized =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
...
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
setOptions([]);
|
|
66
|
+
const normalized = list.map(normalize);
|
|
67
|
+
// merge by id (keep preselected if present)
|
|
68
|
+
setOptions((prev) => {
|
|
69
|
+
const map = new Map(prev.map((o) => [o.id, o]));
|
|
70
|
+
normalized.forEach((o) => map.set(o.id, { ...(map.get(o.id) || {}), ...o }));
|
|
71
|
+
return Array.from(map.values());
|
|
72
|
+
});
|
|
53
73
|
}
|
|
54
74
|
finally {
|
|
55
75
|
setLoading(false);
|
|
56
76
|
}
|
|
57
|
-
}, [modelSlug, showKey]);
|
|
58
|
-
//
|
|
77
|
+
}, [modelSlug, pageSize, showKey, normalize]);
|
|
78
|
+
// load on open + debounced search
|
|
59
79
|
React.useEffect(() => {
|
|
60
80
|
if (open)
|
|
61
|
-
void
|
|
81
|
+
void load(query);
|
|
62
82
|
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
63
|
-
// Debounced search
|
|
64
83
|
React.useEffect(() => {
|
|
65
84
|
if (!open)
|
|
66
85
|
return;
|
|
67
|
-
const t = setTimeout(() => void
|
|
86
|
+
const t = setTimeout(() => void load(query), 250);
|
|
68
87
|
return () => clearTimeout(t);
|
|
69
|
-
}, [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
}, [query, open, load]);
|
|
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), autoFocus: true }) }));
|
|
90
|
+
return (_jsx("div", { className: "w-full", children: _jsx(Select, { name: name, label: label, "aria-label": label, isDisabled: disabled, selectionMode: "multiple", selectedKeys: selectedKeys, onSelectionChange: (keys) => {
|
|
91
|
+
if (keys === 'all')
|
|
92
|
+
return;
|
|
93
|
+
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: {
|
|
95
|
+
topContent: topSearch,
|
|
96
|
+
emptyContent: loading ? undefined : 'No results',
|
|
97
|
+
className: '',
|
|
98
|
+
},
|
|
99
|
+
// chips like docs; items is an array (SelectedItems<T>)
|
|
100
|
+
renderValue: (items) => {
|
|
101
|
+
const arr = Array.isArray(items) ? items : [];
|
|
102
|
+
// first render: items may be empty → fall back to selectedIds mapped from preseeded options
|
|
103
|
+
if (!arr.length && selectedIds.length) {
|
|
104
|
+
const byId = new Map(options.map((o) => [String(o.id), o]));
|
|
105
|
+
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))) }));
|
|
106
|
+
}
|
|
107
|
+
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)))) }));
|
|
108
|
+
}, children: (item) => (_jsx(SelectItem, { textValue: labelOf(item), children: labelOf(item) }, item.id)) }) }));
|
|
76
109
|
};
|
|
@@ -3,7 +3,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { Select, SelectItem } from '@heroui/react';
|
|
5
5
|
import { api } from '../lib/api';
|
|
6
|
-
|
|
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, }) {
|
|
7
10
|
const [loading, setLoading] = React.useState(false);
|
|
8
11
|
const [items, setItems] = React.useState([]);
|
|
9
12
|
const [error, setError] = React.useState(null);
|
|
@@ -12,7 +15,6 @@ export function RefSingleSelect({ name, label, refModel, showKey = 'name', descr
|
|
|
12
15
|
try {
|
|
13
16
|
setLoading(true);
|
|
14
17
|
setError(null);
|
|
15
|
-
// small page size is enough for pickers; adjust if needed
|
|
16
18
|
const res = await api.list(refModelLC, 0, pageSize, {});
|
|
17
19
|
setItems(Array.isArray(res.data) ? res.data : []);
|
|
18
20
|
}
|
|
@@ -23,9 +25,9 @@ export function RefSingleSelect({ name, label, refModel, showKey = 'name', descr
|
|
|
23
25
|
finally {
|
|
24
26
|
setLoading(false);
|
|
25
27
|
}
|
|
26
|
-
}, [refModelLC]);
|
|
28
|
+
}, [refModelLC, pageSize]);
|
|
27
29
|
React.useEffect(() => {
|
|
28
|
-
load();
|
|
30
|
+
void load();
|
|
29
31
|
}, [load]);
|
|
30
32
|
const getId = (r) => (typeof r.id === 'string' && r.id) ||
|
|
31
33
|
(typeof r._id === 'string' && r._id) ||
|
|
@@ -34,17 +36,15 @@ export function RefSingleSelect({ name, label, refModel, showKey = 'name', descr
|
|
|
34
36
|
const v = r?.[showKey];
|
|
35
37
|
return v == null ? getId(r) : String(v);
|
|
36
38
|
};
|
|
37
|
-
return (_jsx("div", { className: "w-full", children: _jsx(Select, { name: name, label: label, placeholder: label, labelPlacement: "outside", className: className, classNames:
|
|
39
|
+
return (_jsx("div", { className: "w-full", children: _jsx(Select, { name: name, label: label, placeholder: label, labelPlacement: "outside", className: className, classNames: {
|
|
40
|
+
// Make sure the whole trigger is clickable
|
|
41
|
+
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) => {
|
|
38
43
|
if (keys === 'all')
|
|
39
44
|
return;
|
|
40
45
|
const v = Array.from(keys)[0];
|
|
41
46
|
onChange(v ?? null);
|
|
42
|
-
},
|
|
43
|
-
// Re-fetch on open to ensure newest data (e.g., newly created roles)
|
|
44
|
-
onOpenChange: (open) => {
|
|
45
|
-
if (open)
|
|
46
|
-
void load();
|
|
47
|
-
}, disallowEmptySelection: false, "aria-label": label, children: (opt) => {
|
|
47
|
+
}, children: (opt) => {
|
|
48
48
|
const key = getId(opt);
|
|
49
49
|
const text = getLabel(opt);
|
|
50
50
|
return (_jsx(SelectItem, { textValue: text, children: text }, key));
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { SchemaDef } from '../lib/types';
|
|
2
|
+
type FieldErrorItems = {
|
|
3
|
+
error: boolean;
|
|
4
|
+
field: string;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
2
7
|
type SchemaFormProps = {
|
|
3
8
|
model: string;
|
|
4
9
|
schemaOverride?: SchemaDef;
|
|
@@ -7,7 +12,8 @@ type SchemaFormProps = {
|
|
|
7
12
|
busy?: boolean;
|
|
8
13
|
showReset?: boolean;
|
|
9
14
|
onSubmit: (values: Record<string, any>) => void | Promise<void>;
|
|
15
|
+
fieldErrors?: FieldErrorItems[] | undefined;
|
|
10
16
|
};
|
|
11
17
|
/** --------------------------------------------------------------------------------------- **/
|
|
12
|
-
export declare function SchemaForm({ model, schemaOverride, initialValues, submitLabel, busy, showReset, onSubmit, }: SchemaFormProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function SchemaForm({ model, schemaOverride, initialValues, submitLabel, busy, showReset, onSubmit, fieldErrors, }: SchemaFormProps): import("react/jsx-runtime").JSX.Element;
|
|
13
19
|
export {};
|
|
@@ -180,8 +180,71 @@ function normalizeTimeRangeLoose(value) {
|
|
|
180
180
|
}
|
|
181
181
|
return {};
|
|
182
182
|
}
|
|
183
|
+
function getJsonGroupSpec(attr) {
|
|
184
|
+
// 1) Preferred: string field with format: 'json-group'
|
|
185
|
+
if (attr && typeof attr === 'object' && !Array.isArray(attr)) {
|
|
186
|
+
const a = attr;
|
|
187
|
+
const type = String(a.type ?? '').toLowerCase();
|
|
188
|
+
const fmt = String(a.format ?? '').toLowerCase();
|
|
189
|
+
if (fmt === 'json-group' &&
|
|
190
|
+
a.attributes &&
|
|
191
|
+
typeof a.attributes === 'object') {
|
|
192
|
+
const spec = {
|
|
193
|
+
attributes: a.attributes,
|
|
194
|
+
minItems: a.minItems,
|
|
195
|
+
maxItems: a.maxItems,
|
|
196
|
+
label: a.label,
|
|
197
|
+
required: !!a.required,
|
|
198
|
+
repeat: !!a.repeat, // repeat => render as repeater
|
|
199
|
+
};
|
|
200
|
+
return { kind: spec.repeat ? 'array' : 'single', spec };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 2) Back-compat (if you still have it somewhere): array-head style
|
|
204
|
+
if (Array.isArray(attr) &&
|
|
205
|
+
attr.length > 0 &&
|
|
206
|
+
attr[0] &&
|
|
207
|
+
typeof attr[0] === 'object') {
|
|
208
|
+
const head = attr[0];
|
|
209
|
+
const t = String(head.type ?? head.format ?? '').toLowerCase();
|
|
210
|
+
if ((t === 'json-group' || head.group === true) &&
|
|
211
|
+
head.attributes &&
|
|
212
|
+
typeof head.attributes === 'object') {
|
|
213
|
+
return {
|
|
214
|
+
kind: 'array',
|
|
215
|
+
spec: {
|
|
216
|
+
attributes: head.attributes,
|
|
217
|
+
minItems: head.minItems,
|
|
218
|
+
maxItems: head.maxItems,
|
|
219
|
+
label: head.label,
|
|
220
|
+
required: !!head.required,
|
|
221
|
+
repeat: true,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function parseJsonGroupValue(raw, kind) {
|
|
229
|
+
if (raw == null || raw === '')
|
|
230
|
+
return (kind === 'array' ? [] : {});
|
|
231
|
+
if (typeof raw === 'string') {
|
|
232
|
+
try {
|
|
233
|
+
return JSON.parse(raw);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return (kind === 'array' ? [] : {});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return raw;
|
|
240
|
+
}
|
|
241
|
+
function toJsonString(v) {
|
|
242
|
+
if (v == null || v === '')
|
|
243
|
+
return '';
|
|
244
|
+
return typeof v === 'string' ? v : JSON.stringify(v);
|
|
245
|
+
}
|
|
183
246
|
/** --------------------------------------------------------------------------------------- **/
|
|
184
|
-
export function SchemaForm({ model, schemaOverride, initialValues, submitLabel = 'Save', busy = false, showReset = true, onSubmit, }) {
|
|
247
|
+
export function SchemaForm({ model, schemaOverride, initialValues, submitLabel = 'Save', busy = false, showReset = true, onSubmit, fieldErrors, }) {
|
|
185
248
|
const mapsKey = useGoogleMapsKey();
|
|
186
249
|
const formUid = useId();
|
|
187
250
|
const { items } = useSelector((s) => s.schemas);
|
|
@@ -256,7 +319,15 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
256
319
|
e.preventDefault();
|
|
257
320
|
setError(undefined);
|
|
258
321
|
try {
|
|
259
|
-
const { createdAt, updatedAt, baseId, exId, __childId, ...
|
|
322
|
+
const { createdAt, updatedAt, baseId, exId, __childId, ...rest } = form;
|
|
323
|
+
const payload = { ...rest };
|
|
324
|
+
if (schema) {
|
|
325
|
+
for (const [fname, fattr] of Object.entries(schema.attributes)) {
|
|
326
|
+
const jg = getJsonGroupSpec(fattr);
|
|
327
|
+
if (jg)
|
|
328
|
+
payload[fname] = toJsonString(payload[fname]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
260
331
|
await onSubmit(payload);
|
|
261
332
|
}
|
|
262
333
|
catch (err) {
|
|
@@ -279,10 +350,45 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
279
350
|
};
|
|
280
351
|
return (_jsxs(Form, { className: gridClass, onSubmit: handleSubmit, onReset: handleReset, validationBehavior: "native", encType: "multipart/form-data", children: [fields.map(({ name, attr }) => {
|
|
281
352
|
const colClass = 'col-span-1';
|
|
353
|
+
const jsonGroup = getJsonGroupSpec(attr);
|
|
354
|
+
if (jsonGroup?.kind === 'array') {
|
|
355
|
+
const { spec } = jsonGroup;
|
|
356
|
+
const itemSchema = spec.attributes;
|
|
357
|
+
const items = Array.isArray(form[name])
|
|
358
|
+
? form[name]
|
|
359
|
+
: parseJsonGroupValue(form[name], 'array');
|
|
360
|
+
const canAdd = spec.maxItems == null || items.length < spec.maxItems;
|
|
361
|
+
const canRemove = spec.minItems == null || items.length > spec.minItems;
|
|
362
|
+
return (_jsxs("div", { className: "col-span-2", children: [_jsxs("label", { className: "text-sm font-medium", children: [attr?.label || spec.label || formatLabel(name), attr?.required ? ' *' : ''] }), _jsxs("div", { className: "flex flex-col gap-4 mt-2", children: [items.map((it, idx) => (_jsxs("div", { className: "grid grid-cols-2 gap-3 p-3 rounded-lg border border-default-200", children: [Object.entries(itemSchema).map(([k, a]) => (_jsx(Input, { variant: "bordered", classNames: inputClassNames, id: `${name}-${idx}-${k}`, name: `${name}.${idx}.${k}`, label: a?.label ?? formatLabel(k), labelPlacement: "outside-top", type: "text", value: typeof it?.[k] === 'string' ? it[k] : '', onChange: (e) => {
|
|
363
|
+
const next = items.slice();
|
|
364
|
+
next[idx] = {
|
|
365
|
+
...(next[idx] || {}),
|
|
366
|
+
[k]: e.target.value,
|
|
367
|
+
};
|
|
368
|
+
handleChange(name, next);
|
|
369
|
+
}, isDisabled: busy, description: a?.description, className: "w-full", isRequired: !!a?.required }, `${name}-${idx}-${k}`))), _jsx("div", { className: "col-span-2 flex justify-between", children: _jsx(Button, { size: "sm", variant: "flat", color: "danger", isDisabled: !canRemove, onPress: () => {
|
|
370
|
+
const next = items.slice();
|
|
371
|
+
next.splice(idx, 1);
|
|
372
|
+
handleChange(name, next);
|
|
373
|
+
}, children: "Remove" }) })] }, idx))), _jsxs(Button, { size: "sm", variant: "solid", isDisabled: !canAdd, onPress: () => handleChange(name, [...items, {}]), children: ["Add ", attr?.label || spec.label || 'Item'] })] })] }, name));
|
|
374
|
+
}
|
|
375
|
+
if (jsonGroup?.kind === 'single') {
|
|
376
|
+
const { spec } = jsonGroup;
|
|
377
|
+
const itemSchema = spec.attributes;
|
|
378
|
+
const obj = form[name] &&
|
|
379
|
+
typeof form[name] === 'object' &&
|
|
380
|
+
!Array.isArray(form[name])
|
|
381
|
+
? form[name]
|
|
382
|
+
: parseJsonGroupValue(form[name], 'single');
|
|
383
|
+
return (_jsxs("div", { className: "col-span-2", children: [_jsxs("label", { className: "text-sm font-medium", children: [attr?.label || spec.label || formatLabel(name), attr?.required ? ' *' : ''] }), _jsx("div", { className: "grid grid-cols-2 gap-3 p-3 rounded-lg border border-default-200 mt-2", children: Object.entries(itemSchema).map(([k, a]) => (_jsx(Input, { variant: "bordered", classNames: inputClassNames, id: `${name}-${k}`, name: `${name}.${k}`, label: a?.label ?? formatLabel(k), labelPlacement: "outside-top", type: "text", value: typeof obj?.[k] === 'string' ? obj[k] : '', onChange: (e) => {
|
|
384
|
+
const next = { ...(obj || {}), [k]: e.target.value };
|
|
385
|
+
handleChange(name, next);
|
|
386
|
+
}, isDisabled: busy, description: a?.description, className: "w-full", isRequired: !!a?.required }, `${name}-${k}`))) })] }, name));
|
|
387
|
+
}
|
|
282
388
|
// --- 1) Array of references → multi select (1 column)
|
|
283
389
|
const refArray = getRefArraySpec(attr);
|
|
284
390
|
if (refArray) {
|
|
285
|
-
return (_jsx("div", { className: colClass, children: _jsx(RefMultiSelect, { name: name, label: attr?.label ?? formatLabel(name), refModel: refArray.ref, showKey: refArray.show ?? 'name', description: attr?.description, value:
|
|
391
|
+
return (_jsx("div", { className: colClass, children: _jsx(RefMultiSelect, { name: name, label: attr[0]?.label ?? formatLabel(name), refModel: refArray.ref, showKey: refArray.show ?? 'name', description: attr?.description, value: form[name], onChange: (ids) => handleChange(name, ids), disabled: busy, required: attr[0]?.required, pageSize: attr[0]?.pageSize }) }, name));
|
|
286
392
|
}
|
|
287
393
|
// --- 2) Single reference → single select (1 column)
|
|
288
394
|
const refSingle = getRefSingleSpec(attr);
|
|
@@ -433,7 +539,7 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
|
|
|
433
539
|
return (_jsx(Radio, { value: opt, children: opt }, opt));
|
|
434
540
|
}) }));
|
|
435
541
|
}
|
|
436
|
-
return (_jsx(Select, { variant: "bordered", classNames: selectClassNames, labelPlacement: "outside", id: id, name: name, label: label, selectedKeys: value ? new Set([String(value)]) : new Set(), onSelectionChange: (keys) => {
|
|
542
|
+
return (_jsx(Select, { isVirtualized: true, variant: "bordered", classNames: selectClassNames, labelPlacement: "outside", id: id, name: name, label: label, selectedKeys: value ? new Set([String(value)]) : new Set(), onSelectionChange: (keys) => {
|
|
437
543
|
if (keys === 'all')
|
|
438
544
|
return;
|
|
439
545
|
const v = Array.from(keys)[0];
|
package/dist/lib/api.js
CHANGED
|
@@ -28,16 +28,29 @@ function modelPath(modelName) {
|
|
|
28
28
|
async function request(path, opts = {}) {
|
|
29
29
|
const { method = 'GET', body, token, json = true, auth = true } = opts;
|
|
30
30
|
const headers = { 'x-api-key': API_KEY };
|
|
31
|
-
|
|
31
|
+
// Don't force JSON when sending FormData/Blob
|
|
32
|
+
const isForm = typeof FormData !== 'undefined' && body instanceof FormData;
|
|
33
|
+
if (json && !isForm)
|
|
32
34
|
headers['Content-Type'] = 'application/json';
|
|
33
35
|
const bearer = token ?? tokenFromStorage();
|
|
34
36
|
if (auth && bearer)
|
|
35
37
|
headers.Authorization = `Bearer ${bearer}`;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await fetch(`${API_BASE}${path}`, {
|
|
41
|
+
method,
|
|
42
|
+
headers,
|
|
43
|
+
body: body && json && !isForm
|
|
44
|
+
? JSON.stringify(body)
|
|
45
|
+
: body,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (networkErr) {
|
|
49
|
+
// Network/DNS/CORS timeouts etc.
|
|
50
|
+
const err = new ApiError(networkErr?.message || 'Network error', 0);
|
|
51
|
+
err.request = { url: `${API_BASE}${path}`, method, headers };
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
41
54
|
const raw = await res.text();
|
|
42
55
|
let payload = {};
|
|
43
56
|
try {
|
|
@@ -50,15 +63,9 @@ async function request(path, opts = {}) {
|
|
|
50
63
|
const status = res.status;
|
|
51
64
|
const serverMsg = payload?.message ??
|
|
52
65
|
payload?.error ??
|
|
53
|
-
res.statusText
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new ApiError('Not authenticated', 401, payload);
|
|
57
|
-
if (status === 403)
|
|
58
|
-
throw new ApiError('Not permitted', 403, payload);
|
|
59
|
-
throw new ApiError(typeof serverMsg === 'string' && serverMsg.trim()
|
|
60
|
-
? serverMsg
|
|
61
|
-
: 'API error', status, payload);
|
|
66
|
+
res.statusText ??
|
|
67
|
+
'API error';
|
|
68
|
+
throw payload;
|
|
62
69
|
}
|
|
63
70
|
return payload;
|
|
64
71
|
}
|