@airoom/nextmin-react 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/RefMultiSelect.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 +9 -9
- 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 {};
|
|
@@ -181,7 +181,7 @@ function normalizeTimeRangeLoose(value) {
|
|
|
181
181
|
return {};
|
|
182
182
|
}
|
|
183
183
|
/** --------------------------------------------------------------------------------------- **/
|
|
184
|
-
export function SchemaForm({ model, schemaOverride, initialValues, submitLabel = 'Save', busy = false, showReset = true, onSubmit, }) {
|
|
184
|
+
export function SchemaForm({ model, schemaOverride, initialValues, submitLabel = 'Save', busy = false, showReset = true, onSubmit, fieldErrors, }) {
|
|
185
185
|
const mapsKey = useGoogleMapsKey();
|
|
186
186
|
const formUid = useId();
|
|
187
187
|
const { items } = useSelector((s) => s.schemas);
|
|
@@ -245,7 +245,7 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
245
245
|
const fields = useMemo(() => Object.entries(schema.attributes)
|
|
246
246
|
.filter(([name]) => !AUDIT_FIELDS.has(name))
|
|
247
247
|
// Force-hide any linkage field named "baseId"
|
|
248
|
-
.filter(([name]) => name !== 'baseId')
|
|
248
|
+
.filter(([name]) => name !== 'baseId' && name !== 'exId')
|
|
249
249
|
.filter(([, attr]) => canBypassPrivacy ? true : !attr?.private)
|
|
250
250
|
.filter(([, attr]) => !isHiddenAttr(attr, isEdit))
|
|
251
251
|
.filter(([name, attr]) => !(isEdit && isPasswordAttr(name, attr)))
|
|
@@ -256,7 +256,7 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
256
256
|
e.preventDefault();
|
|
257
257
|
setError(undefined);
|
|
258
258
|
try {
|
|
259
|
-
const { createdAt, updatedAt, ...payload } = form;
|
|
259
|
+
const { createdAt, updatedAt, baseId, exId, __childId, ...payload } = form;
|
|
260
260
|
await onSubmit(payload);
|
|
261
261
|
}
|
|
262
262
|
catch (err) {
|
|
@@ -282,7 +282,7 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
282
282
|
// --- 1) Array of references → multi select (1 column)
|
|
283
283
|
const refArray = getRefArraySpec(attr);
|
|
284
284
|
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:
|
|
285
|
+
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
286
|
}
|
|
287
287
|
// --- 2) Single reference → single select (1 column)
|
|
288
288
|
const refSingle = getRefSingleSpec(attr);
|
|
@@ -433,7 +433,7 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
|
|
|
433
433
|
return (_jsx(Radio, { value: opt, children: opt }, opt));
|
|
434
434
|
}) }));
|
|
435
435
|
}
|
|
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) => {
|
|
436
|
+
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
437
|
if (keys === 'all')
|
|
438
438
|
return;
|
|
439
439
|
const v = Array.from(keys)[0];
|
|
@@ -531,19 +531,18 @@ function normalizeIdsArray(raw) {
|
|
|
531
531
|
if (!raw)
|
|
532
532
|
return [];
|
|
533
533
|
if (Array.isArray(raw)) {
|
|
534
|
-
return raw
|
|
535
|
-
.map((v) => {
|
|
534
|
+
return raw.map((v) => {
|
|
536
535
|
if (typeof v === 'string')
|
|
537
536
|
return v;
|
|
538
537
|
if (v && typeof v === 'object') {
|
|
539
538
|
const anyv = v;
|
|
540
539
|
return ((typeof anyv.id === 'string' && anyv.id) ||
|
|
541
540
|
(typeof anyv._id === 'string' && anyv._id) ||
|
|
541
|
+
(typeof anyv.exId === 'string' && anyv.exId) ||
|
|
542
542
|
null);
|
|
543
543
|
}
|
|
544
544
|
return null;
|
|
545
|
-
})
|
|
546
|
-
.filter((x) => !!x);
|
|
545
|
+
});
|
|
547
546
|
}
|
|
548
547
|
return [];
|
|
549
548
|
}
|
|
@@ -556,6 +555,7 @@ function normalizeId(raw) {
|
|
|
556
555
|
const anyv = raw;
|
|
557
556
|
const id = (typeof anyv.id === 'string' && anyv.id) ||
|
|
558
557
|
(typeof anyv._id === 'string' && anyv._id) ||
|
|
558
|
+
(typeof anyv.exId === 'string' && anyv.exId) ||
|
|
559
559
|
null;
|
|
560
560
|
return id;
|
|
561
561
|
}
|
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
|
}
|