@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.
@@ -1,4 +1,3 @@
1
- // packages/nextmin-react/src/components/FileUploader.tsx
2
1
  'use client';
3
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
4
3
  import { useEffect, useMemo, useRef, useState } from 'react';
@@ -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, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import React from 'react';
4
- import { Button, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger, Spinner, } from '@heroui/react';
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 = 20, }) => {
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
- const selectedKeys = React.useMemo(() => new Set((value ?? []).map((v) => v)), [value]);
13
- const selectionCount = selectedKeys.size;
14
- const summaryText = selectionCount
15
- ? `You have ${selectionCount} selected`
16
- : `Select ${refModel}`;
17
- const labelOf = React.useCallback((opt) => String((showKey && opt?.[showKey]) ??
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 viewItems = React.useMemo(() => options.map((opt) => {
25
- const text = labelOf(opt);
26
- const key = String(opt.id ?? opt._id ?? text);
27
- return { key, text };
28
- }), [options, labelOf]);
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
- // One list call for the current search page
33
- const params = q
34
- ? { q, searchKey: showKey || 'name' }
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 = (list || []).map((it) => ({
44
- id: (typeof it?.id === 'string' && it.id) ||
45
- (typeof it?._id === 'string' && it._id) ||
46
- String(it?.id ?? ''),
47
- ...it,
48
- }));
49
- setOptions(normalized);
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
- // Load when menu opens
77
+ }, [modelSlug, pageSize, showKey, normalize]);
78
+ // load on open + debounced search
59
79
  React.useEffect(() => {
60
80
  if (open)
61
- void loadOptions(query);
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 loadOptions(query), 250);
86
+ const t = setTimeout(() => void load(query), 250);
68
87
  return () => clearTimeout(t);
69
- }, [open, query, loadOptions]);
70
- return (_jsxs("div", { className: "w-full", children: [_jsxs("label", { className: "mb-1 block text-sm font-medium", children: [label, " ", required ? _jsx("span", { className: "text-danger", children: "*" }) : null] }), _jsxs(Popover, { placement: "bottom-start", offset: 6, isOpen: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { children: _jsxs(Button, { variant: "bordered", className: "w-full justify-between", isDisabled: disabled, "aria-label": label, name: name, children: [_jsx("span", { className: "truncate", children: summaryText }), _jsx("span", { className: "text-xs text-default-500", children: selectionCount ? `${selectionCount} selected` : '' })] }) }), _jsx(PopoverContent, { className: "w-auto p-2", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Input, { size: "sm", variant: "bordered", "aria-label": `Search ${refModel}`, placeholder: `Search ${refModel}…`, fullWidth: true, value: query, onChange: (e) => setQuery(e.target.value) }), _jsx("div", { className: "max-h-72 overflow-auto w-full", children: _jsx(Listbox, { "aria-label": `${label} options`, selectionMode: "multiple", selectedKeys: selectedKeys, onSelectionChange: (keys) => {
71
- if (keys === 'all')
72
- return;
73
- const ids = Array.from(keys).map(String);
74
- onChange(ids);
75
- }, items: viewItems, children: (item) => (_jsx(ListboxItem, { textValue: item.text, children: item.text }, item.key)) }) }), loading && (_jsx("div", { className: "grid place-items-center py-2", children: _jsx(Spinner, { size: "sm" }) }))] }) })] }), description ? (_jsx("p", { className: "mt-1 text-xs text-default-500", children: description })) : null] }));
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
- export function RefSingleSelect({ name, label, refModel, showKey = 'name', description, value, onChange, disabled, required, className, classNames, pageSize, }) {
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(); // eager-load on mount
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: classNames, variant: "bordered", selectionMode: "single", isDisabled: disabled || loading, isLoading: loading, isRequired: required, description: error || description, items: items, selectedKeys: value ? new Set([String(value)]) : new Set(), onSelectionChange: (keys) => {
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, ...payload } = form;
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: normalizeIdsArray(form[name]), onChange: (ids) => handleChange(name, ids), disabled: busy, required: !!attr?.required, pageSize: attr?.pageSize }) }, name));
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
- if (json)
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
- const res = await fetch(`${API_BASE}${path}`, {
37
- method,
38
- headers,
39
- body: body && json ? JSON.stringify(body) : body,
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
- // Normalize common auth/permission errors for the UI
55
- if (status === 401)
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
  }