@airoom/nextmin-react 0.1.0
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/LICENSE +49 -0
- package/README.md +133 -0
- package/dist/auth/AuthPage.d.ts +1 -0
- package/dist/auth/AuthPage.js +23 -0
- package/dist/auth/ForgotPasswordForm.d.ts +1 -0
- package/dist/auth/ForgotPasswordForm.js +28 -0
- package/dist/auth/SignInForm.d.ts +6 -0
- package/dist/auth/SignInForm.js +38 -0
- package/dist/auth/SignUpForm.d.ts +3 -0
- package/dist/auth/SignUpForm.js +30 -0
- package/dist/components/AddressAutocomplete.d.ts +21 -0
- package/dist/components/AddressAutocomplete.js +182 -0
- package/dist/components/AdminApp.d.ts +1 -0
- package/dist/components/AdminApp.js +134 -0
- package/dist/components/ConfirmDialog.d.ts +12 -0
- package/dist/components/ConfirmDialog.js +6 -0
- package/dist/components/FileUploader.d.ts +32 -0
- package/dist/components/FileUploader.js +480 -0
- package/dist/components/NoAccess.d.ts +3 -0
- package/dist/components/NoAccess.js +5 -0
- package/dist/components/PasswordInput.d.ts +19 -0
- package/dist/components/PasswordInput.js +11 -0
- package/dist/components/PhoneInput.d.ts +23 -0
- package/dist/components/PhoneInput.js +147 -0
- package/dist/components/RefMultiSelect.d.ts +14 -0
- package/dist/components/RefMultiSelect.js +76 -0
- package/dist/components/RefSingleSelect.d.ts +17 -0
- package/dist/components/RefSingleSelect.js +52 -0
- package/dist/components/SchemaForm.d.ts +13 -0
- package/dist/components/SchemaForm.js +592 -0
- package/dist/components/SectionLoader.d.ts +3 -0
- package/dist/components/SectionLoader.js +7 -0
- package/dist/components/Sidebar.d.ts +1 -0
- package/dist/components/Sidebar.js +87 -0
- package/dist/components/TableFilters.d.ts +16 -0
- package/dist/components/TableFilters.js +69 -0
- package/dist/components/TableSkeleton.d.ts +7 -0
- package/dist/components/TableSkeleton.js +5 -0
- package/dist/hooks/useGoogleMapsKey.d.ts +5 -0
- package/dist/hooks/useGoogleMapsKey.js +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/api.d.ts +31 -0
- package/dist/lib/api.js +94 -0
- package/dist/lib/auth.d.ts +23 -0
- package/dist/lib/auth.js +51 -0
- package/dist/lib/googleLoader.d.ts +1 -0
- package/dist/lib/googleLoader.js +25 -0
- package/dist/lib/schemaService.d.ts +2 -0
- package/dist/lib/schemaService.js +39 -0
- package/dist/lib/schemaUtils.d.ts +4 -0
- package/dist/lib/schemaUtils.js +18 -0
- package/dist/lib/types.d.ts +50 -0
- package/dist/lib/types.js +1 -0
- package/dist/nextmin.css +1 -0
- package/dist/providers/NextMinProvider.d.ts +5 -0
- package/dist/providers/NextMinProvider.js +30 -0
- package/dist/router/AdminRouteNormalizer.d.ts +1 -0
- package/dist/router/AdminRouteNormalizer.js +32 -0
- package/dist/router/NextMinRouter.d.ts +1 -0
- package/dist/router/NextMinRouter.js +99 -0
- package/dist/state/nextMinSlice.d.ts +14 -0
- package/dist/state/nextMinSlice.js +34 -0
- package/dist/state/schemaLive.d.ts +2 -0
- package/dist/state/schemaLive.js +19 -0
- package/dist/state/schemasSlice.d.ts +20 -0
- package/dist/state/schemasSlice.js +43 -0
- package/dist/state/sessionSlice.d.ts +10 -0
- package/dist/state/sessionSlice.js +18 -0
- package/dist/state/store.d.ts +28 -0
- package/dist/state/store.js +7 -0
- package/dist/views/CreateEditPage.d.ts +4 -0
- package/dist/views/CreateEditPage.js +64 -0
- package/dist/views/DashboardPage.d.ts +1 -0
- package/dist/views/DashboardPage.js +107 -0
- package/dist/views/ListPage.d.ts +5 -0
- package/dist/views/ListPage.js +76 -0
- package/dist/views/NextNotFound.d.ts +1 -0
- package/dist/views/NextNotFound.js +6 -0
- package/dist/views/ProfilePage.d.ts +1 -0
- package/dist/views/ProfilePage.js +193 -0
- package/dist/views/SettingsEdit.d.ts +2 -0
- package/dist/views/SettingsEdit.js +87 -0
- package/dist/views/list/DataTableHero.d.ts +22 -0
- package/dist/views/list/DataTableHero.js +350 -0
- package/dist/views/list/ListHeader.d.ts +8 -0
- package/dist/views/list/ListHeader.js +7 -0
- package/dist/views/list/Pagination.d.ts +8 -0
- package/dist/views/list/Pagination.js +5 -0
- package/dist/views/list/formatters.d.ts +2 -0
- package/dist/views/list/formatters.js +62 -0
- package/dist/views/list/useListData.d.ts +10 -0
- package/dist/views/list/useListData.js +79 -0
- package/package.json +51 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
export function NoAccess({ message }) {
|
|
4
|
+
return (_jsxs("div", { className: "p-6 rounded-lg border bg-red-50 text-red-700", children: [_jsx("div", { className: "font-semibold", children: "Access denied" }), _jsx("div", { className: "text-sm mt-1", children: message ?? 'You are not permitted to view this resource.' })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export declare const EyeSlashFilledIcon: React.FC<React.SVGProps<SVGSVGElement>>;
|
|
3
|
+
export declare const EyeFilledIcon: React.FC<React.SVGProps<SVGSVGElement>>;
|
|
4
|
+
export type PasswordInputProps = {
|
|
5
|
+
id?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
description?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
classNames?: {
|
|
15
|
+
inputWrapper?: string;
|
|
16
|
+
};
|
|
17
|
+
autoComplete?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare const PasswordInput: React.FC<PasswordInputProps>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Input } from '@heroui/react';
|
|
5
|
+
export const EyeSlashFilledIcon = (props) => (_jsxs("svg", { "aria-hidden": "true", fill: "none", focusable: "false", height: "1em", role: "presentation", viewBox: "0 0 24 24", width: "1em", ...props, children: [_jsx("path", { d: "M21.2714 9.17834C20.9814 8.71834 20.6714 8.28834 20.3514 7.88834C19.9814 7.41834 19.2814 7.37834 18.8614 7.79834L15.8614 10.7983C16.0814 11.4583 16.1214 12.2183 15.9214 13.0083C15.5714 14.4183 14.4314 15.5583 13.0214 15.9083C12.2314 16.1083 11.4714 16.0683 10.8114 15.8483C10.8114 15.8483 9.38141 17.2783 8.35141 18.3083C7.85141 18.8083 8.01141 19.6883 8.68141 19.9483C9.75141 20.3583 10.8614 20.5683 12.0014 20.5683C13.7814 20.5683 15.5114 20.0483 17.0914 19.0783C18.7014 18.0783 20.1514 16.6083 21.3214 14.7383C22.2714 13.2283 22.2214 10.6883 21.2714 9.17834Z", fill: "currentColor" }), _jsx("path", { d: "M14.0206 9.98062L9.98062 14.0206C9.47062 13.5006 9.14062 12.7806 9.14062 12.0006C9.14062 10.4306 10.4206 9.14062 12.0006 9.14062C12.7806 9.14062 13.5006 9.47062 14.0206 9.98062Z", fill: "currentColor" }), _jsx("path", { d: "M18.25 5.74969L14.86 9.13969C14.13 8.39969 13.12 7.95969 12 7.95969C9.76 7.95969 7.96 9.76969 7.96 11.9997C7.96 13.1197 8.41 14.1297 9.14 14.8597L5.76 18.2497H5.75C4.64 17.3497 3.62 16.1997 2.75 14.8397C1.75 13.2697 1.75 10.7197 2.75 9.14969C3.91 7.32969 5.33 5.89969 6.91 4.91969C8.49 3.95969 10.22 3.42969 12 3.42969C14.23 3.42969 16.39 4.24969 18.25 5.74969Z", fill: "currentColor" }), _jsx("path", { d: "M14.8581 11.9981C14.8581 13.5681 13.5781 14.8581 11.9981 14.8581C11.9381 14.8581 11.8881 14.8581 11.8281 14.8381L14.8381 11.8281C14.8581 11.8881 14.8581 11.9381 14.8581 11.9981Z", fill: "currentColor" }), _jsx("path", { d: "M21.7689 2.22891C21.4689 1.92891 20.9789 1.92891 20.6789 2.22891L2.22891 20.6889C1.92891 20.9889 1.92891 21.4789 2.22891 21.7789C2.37891 21.9189 2.56891 21.9989 2.76891 21.9989C2.96891 21.9989 3.15891 21.9189 3.30891 21.7689L21.7689 3.30891C22.0789 3.00891 22.0789 2.52891 21.7689 2.22891Z", fill: "currentColor" })] }));
|
|
6
|
+
export const EyeFilledIcon = (props) => (_jsxs("svg", { "aria-hidden": "true", fill: "none", focusable: "false", height: "1em", role: "presentation", viewBox: "0 0 24 24", width: "1em", ...props, children: [_jsx("path", { d: "M21.25 9.14969C18.94 5.51969 15.56 3.42969 12 3.42969C10.22 3.42969 8.49 3.94969 6.91 4.91969C5.33 5.89969 3.91 7.32969 2.75 9.14969C1.75 10.7197 1.75 13.2697 2.75 14.8397C5.06 18.4797 8.44 20.5597 12 20.5597C13.78 20.5597 15.51 20.0397 17.09 19.0697C18.67 18.0897 20.09 16.6597 21.25 14.8397C22.25 13.2797 22.25 10.7197 21.25 9.14969ZM12 16.0397C9.76 16.0397 7.96 14.2297 7.96 11.9997C7.96 9.76969 9.76 7.95969 12 7.95969C14.24 7.95969 16.04 9.76969 16.04 11.9997C16.04 14.2297 14.24 16.0397 12 16.0397Z", fill: "currentColor" }), _jsx("path", { d: "M11.9984 9.14062C10.4284 9.14062 9.14844 10.4206 9.14844 12.0006C9.14844 13.5706 10.4284 14.8506 11.9984 14.8506C13.5684 14.8506 14.8584 13.5706 14.8584 12.0006C14.8584 10.4306 13.5684 9.14062 11.9984 9.14062Z", fill: "currentColor" })] }));
|
|
7
|
+
export const PasswordInput = ({ id, name = 'password', label = 'Password', value, onChange, disabled, required, description, className, classNames, autoComplete = 'new-password', }) => {
|
|
8
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
9
|
+
const toggleVisibility = () => setIsVisible((v) => !v);
|
|
10
|
+
return (_jsx(Input, { id: id, name: name, label: label, labelPlacement: "outside-top", type: isVisible ? 'text' : 'password', value: value, onChange: (e) => onChange(e.target.value), isDisabled: disabled, isRequired: required, description: description, variant: "bordered", className: className, classNames: classNames, autoComplete: autoComplete, endContent: _jsx("button", { type: "button", "aria-label": isVisible ? 'Hide password' : 'Show password', className: "focus:outline-solid outline-transparent", onClick: toggleVisibility, children: isVisible ? (_jsx(EyeSlashFilledIcon, { className: "text-2xl text-default-400 pointer-events-none" })) : (_jsx(EyeFilledIcon, { className: "text-2xl text-default-400 pointer-events-none" })) }) }));
|
|
11
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface PhoneInputProps {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Mask string using digit slots. Accepts x/X/9/#/_ as slots.
|
|
7
|
+
* Examples: "xxx-xxxx-xxxx", "(xx) xxxx-xxxx", "+xx-xxxx-xxxxxx"
|
|
8
|
+
*/
|
|
9
|
+
mask: string;
|
|
10
|
+
/** Raw digits only (no mask). Accepts string | number | null; will be coerced. */
|
|
11
|
+
value?: string | null;
|
|
12
|
+
/** Returns raw digits only (no mask). */
|
|
13
|
+
onChange: (rawDigits: string) => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
/** Shown under the field. If omitted, we show: `Format: +XXX-XXXX-XXXXXX` */
|
|
17
|
+
description?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
classNames?: {
|
|
20
|
+
inputWrapper?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare const PhoneInput: React.FC<PhoneInputProps>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useRef } from 'react';
|
|
4
|
+
import { Input } from '@heroui/react';
|
|
5
|
+
const SLOT = 'x';
|
|
6
|
+
const SLOT_PATTERN = /[Xx9#_]/g; // accept these as "digit slot" placeholders
|
|
7
|
+
const normalizeMaskSlots = (m) => m.replace(SLOT_PATTERN, SLOT);
|
|
8
|
+
// handle string | number | null | undefined safely
|
|
9
|
+
const onlyDigits = (s) => String(s ?? '').replace(/\D/g, '');
|
|
10
|
+
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
11
|
+
/** Indices in the mask where a digit goes (SLOT) */
|
|
12
|
+
const getSlotIndices = (mask) => {
|
|
13
|
+
const idxs = [];
|
|
14
|
+
for (let i = 0; i < mask.length; i++)
|
|
15
|
+
if (mask[i] === SLOT)
|
|
16
|
+
idxs.push(i);
|
|
17
|
+
return idxs;
|
|
18
|
+
};
|
|
19
|
+
const toRegexFromMask = (mask) => {
|
|
20
|
+
// e.g. "xxx-xxxx-xxxx" -> ^\d{3}-\d{4}-\d{4}$
|
|
21
|
+
let out = '';
|
|
22
|
+
for (let i = 0; i < mask.length;) {
|
|
23
|
+
if (mask[i] === SLOT) {
|
|
24
|
+
let n = 1;
|
|
25
|
+
while (i + n < mask.length && mask[i + n] === SLOT)
|
|
26
|
+
n++;
|
|
27
|
+
out += `\\d{${n}}`;
|
|
28
|
+
i += n;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
out += mask[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
+
i += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return `^${out}$`;
|
|
36
|
+
};
|
|
37
|
+
const formatDisplay = (rawDigits, normMask) => {
|
|
38
|
+
const slots = getSlotIndices(normMask);
|
|
39
|
+
const digits = onlyDigits(rawDigits).slice(0, slots.length);
|
|
40
|
+
if (digits.length === 0)
|
|
41
|
+
return '';
|
|
42
|
+
const out = normMask.split('');
|
|
43
|
+
for (let i = 0; i < digits.length; i++)
|
|
44
|
+
out[slots[i]] = digits[i];
|
|
45
|
+
// cut any trailing static chars after the last filled slot
|
|
46
|
+
const last = slots[Math.max(0, digits.length - 1)];
|
|
47
|
+
return out.slice(0, last + 1).join('');
|
|
48
|
+
};
|
|
49
|
+
const caretPosForDigitCount = (normMask, digitCount) => {
|
|
50
|
+
const slots = getSlotIndices(normMask);
|
|
51
|
+
if (slots.length === 0)
|
|
52
|
+
return 0;
|
|
53
|
+
const k = clamp(digitCount, 0, slots.length);
|
|
54
|
+
if (k === 0)
|
|
55
|
+
return slots[0]; // first slot (skip prefix like + or ()
|
|
56
|
+
return clamp(slots[k - 1] + 1, 0, normMask.length);
|
|
57
|
+
};
|
|
58
|
+
const digitsBeforeCaret = (maskedValue, caret) => onlyDigits(maskedValue.slice(0, clamp(caret, 0, maskedValue.length))).length;
|
|
59
|
+
export const PhoneInput = ({ id, name, label, mask, value, onChange, disabled, required, description, className, classNames, }) => {
|
|
60
|
+
const inputRef = useRef(null);
|
|
61
|
+
// Normalize the mask to use lowercase 'x' as slots for internal logic
|
|
62
|
+
const { normMask, maxDigits, placeholder, pattern } = useMemo(() => {
|
|
63
|
+
const nm = normalizeMaskSlots(String(mask || ''));
|
|
64
|
+
const slots = getSlotIndices(nm);
|
|
65
|
+
return {
|
|
66
|
+
normMask: nm,
|
|
67
|
+
maxDigits: slots.length,
|
|
68
|
+
// Always show placeholder with uppercase X for UX consistency
|
|
69
|
+
placeholder: nm.replace(/x/g, 'X'),
|
|
70
|
+
pattern: toRegexFromMask(nm),
|
|
71
|
+
};
|
|
72
|
+
}, [mask]);
|
|
73
|
+
const raw = onlyDigits(value).slice(0, maxDigits);
|
|
74
|
+
const masked = formatDisplay(raw, normMask);
|
|
75
|
+
const setCaret = (pos) => {
|
|
76
|
+
const el = inputRef.current;
|
|
77
|
+
if (!el)
|
|
78
|
+
return;
|
|
79
|
+
requestAnimationFrame(() => {
|
|
80
|
+
try {
|
|
81
|
+
el.setSelectionRange(pos, pos);
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
const handleChange = (e) => {
|
|
87
|
+
const inputVal = e.currentTarget.value;
|
|
88
|
+
const caret = e.currentTarget.selectionStart ?? inputVal.length;
|
|
89
|
+
const nextRaw = onlyDigits(inputVal).slice(0, maxDigits);
|
|
90
|
+
const digitsBefore = digitsBeforeCaret(inputVal, caret);
|
|
91
|
+
const nextMasked = formatDisplay(nextRaw, normMask);
|
|
92
|
+
const nextCaret = caretPosForDigitCount(normMask, digitsBefore);
|
|
93
|
+
onChange(nextRaw);
|
|
94
|
+
setCaret(clamp(nextCaret, 0, nextMasked.length));
|
|
95
|
+
};
|
|
96
|
+
const handleKeyDown = (e) => {
|
|
97
|
+
const el = e.currentTarget;
|
|
98
|
+
const selStart = el.selectionStart ?? 0;
|
|
99
|
+
const selEnd = el.selectionEnd ?? selStart;
|
|
100
|
+
if (selStart !== selEnd)
|
|
101
|
+
return;
|
|
102
|
+
if (e.key === 'Backspace') {
|
|
103
|
+
if (selStart <= 0)
|
|
104
|
+
return;
|
|
105
|
+
const leftChar = el.value[selStart - 1];
|
|
106
|
+
if (leftChar && /\D/.test(leftChar)) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
const currRaw = onlyDigits(el.value);
|
|
109
|
+
const countBefore = digitsBeforeCaret(el.value, selStart - 1);
|
|
110
|
+
const idxToRemove = countBefore - 1;
|
|
111
|
+
if (idxToRemove >= 0) {
|
|
112
|
+
const nextRaw = currRaw.slice(0, idxToRemove) + currRaw.slice(idxToRemove + 1);
|
|
113
|
+
onChange(nextRaw.slice(0, maxDigits));
|
|
114
|
+
const nextCaret = caretPosForDigitCount(normMask, countBefore - 1);
|
|
115
|
+
setCaret(nextCaret);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (e.key === 'Delete') {
|
|
121
|
+
const rightChar = el.value[selStart];
|
|
122
|
+
if (rightChar && /\D/.test(rightChar)) {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
const currRaw = onlyDigits(el.value);
|
|
125
|
+
const countBefore = digitsBeforeCaret(el.value, selStart);
|
|
126
|
+
const idxToRemove = countBefore;
|
|
127
|
+
if (idxToRemove < currRaw.length) {
|
|
128
|
+
const nextRaw = currRaw.slice(0, idxToRemove) + currRaw.slice(idxToRemove + 1);
|
|
129
|
+
onChange(nextRaw.slice(0, maxDigits));
|
|
130
|
+
const nextCaret = caretPosForDigitCount(normMask, countBefore);
|
|
131
|
+
setCaret(nextCaret);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const handlePaste = (e) => {
|
|
137
|
+
const text = e.clipboardData.getData('text') ?? '';
|
|
138
|
+
const pastedDigits = onlyDigits(text);
|
|
139
|
+
if (!pastedDigits)
|
|
140
|
+
return;
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
onChange(pastedDigits.slice(0, maxDigits));
|
|
143
|
+
const nextCaret = caretPosForDigitCount(normMask, Math.min(pastedDigits.length, maxDigits));
|
|
144
|
+
setCaret(nextCaret);
|
|
145
|
+
};
|
|
146
|
+
return (_jsx(Input, { ref: inputRef, variant: "bordered", classNames: classNames, id: id, name: name, label: label, labelPlacement: "outside-top", value: masked, onChange: handleChange, onKeyDown: handleKeyDown, onPaste: handlePaste, isDisabled: disabled, description: description ?? `Format: ${mask}`, className: className ?? 'w-full', isRequired: required, inputMode: "tel", pattern: pattern, maxLength: normMask.length, placeholder: placeholder }));
|
|
147
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type RefMultiSelectProps = {
|
|
3
|
+
name: string;
|
|
4
|
+
label: string;
|
|
5
|
+
refModel: string;
|
|
6
|
+
showKey?: string;
|
|
7
|
+
value: string[];
|
|
8
|
+
onChange: (ids: string[]) => void;
|
|
9
|
+
description?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const RefMultiSelect: React.FC<RefMultiSelectProps>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Button, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger, Spinner, } from '@heroui/react';
|
|
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([]);
|
|
11
|
+
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]) ??
|
|
18
|
+
opt?.name ??
|
|
19
|
+
opt?.title ??
|
|
20
|
+
opt?.key ??
|
|
21
|
+
opt?.id ??
|
|
22
|
+
opt?._id ??
|
|
23
|
+
''), [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) => {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
try {
|
|
32
|
+
// One list call for the current search page
|
|
33
|
+
const params = q
|
|
34
|
+
? { q, searchKey: showKey || 'name' }
|
|
35
|
+
: {};
|
|
36
|
+
const res = await api.list?.(modelSlug, 0, pageSize, params);
|
|
37
|
+
const payload = res?.data ?? res;
|
|
38
|
+
const list = payload?.items ??
|
|
39
|
+
payload?.docs ??
|
|
40
|
+
payload?.results ??
|
|
41
|
+
payload?.list ??
|
|
42
|
+
(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([]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
}, [modelSlug, showKey]);
|
|
58
|
+
// Load when menu opens
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
if (open)
|
|
61
|
+
void loadOptions(query);
|
|
62
|
+
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
63
|
+
// Debounced search
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (!open)
|
|
66
|
+
return;
|
|
67
|
+
const t = setTimeout(() => void loadOptions(query), 250);
|
|
68
|
+
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] }));
|
|
76
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type RefSingleSelectProps = {
|
|
2
|
+
name: string;
|
|
3
|
+
label: string;
|
|
4
|
+
refModel: string;
|
|
5
|
+
showKey?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
value?: string | null;
|
|
8
|
+
onChange: (id: string | null) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
classNames?: {
|
|
13
|
+
trigger?: string;
|
|
14
|
+
};
|
|
15
|
+
pageSize?: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function RefSingleSelect({ name, label, refModel, showKey, description, value, onChange, disabled, required, className, classNames, pageSize, }: RefSingleSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Select, SelectItem } from '@heroui/react';
|
|
5
|
+
import { api } from '../lib/api';
|
|
6
|
+
export function RefSingleSelect({ name, label, refModel, showKey = 'name', description, value, onChange, disabled, required, className, classNames, pageSize, }) {
|
|
7
|
+
const [loading, setLoading] = React.useState(false);
|
|
8
|
+
const [items, setItems] = React.useState([]);
|
|
9
|
+
const [error, setError] = React.useState(null);
|
|
10
|
+
const refModelLC = React.useMemo(() => refModel.toLowerCase(), [refModel]);
|
|
11
|
+
const load = React.useCallback(async () => {
|
|
12
|
+
try {
|
|
13
|
+
setLoading(true);
|
|
14
|
+
setError(null);
|
|
15
|
+
// small page size is enough for pickers; adjust if needed
|
|
16
|
+
const res = await api.list(refModelLC, 0, pageSize, {});
|
|
17
|
+
setItems(Array.isArray(res.data) ? res.data : []);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
setError(e?.message || 'Failed to load options');
|
|
21
|
+
setItems([]);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
}, [refModelLC]);
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
load(); // eager-load on mount
|
|
29
|
+
}, [load]);
|
|
30
|
+
const getId = (r) => (typeof r.id === 'string' && r.id) ||
|
|
31
|
+
(typeof r._id === 'string' && r._id) ||
|
|
32
|
+
'';
|
|
33
|
+
const getLabel = (r) => {
|
|
34
|
+
const v = r?.[showKey];
|
|
35
|
+
return v == null ? getId(r) : String(v);
|
|
36
|
+
};
|
|
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) => {
|
|
38
|
+
if (keys === 'all')
|
|
39
|
+
return;
|
|
40
|
+
const v = Array.from(keys)[0];
|
|
41
|
+
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) => {
|
|
48
|
+
const key = getId(opt);
|
|
49
|
+
const text = getLabel(opt);
|
|
50
|
+
return (_jsx(SelectItem, { textValue: text, children: text }, key));
|
|
51
|
+
} }) }));
|
|
52
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SchemaDef } from '../lib/types';
|
|
2
|
+
type SchemaFormProps = {
|
|
3
|
+
model: string;
|
|
4
|
+
schemaOverride?: SchemaDef;
|
|
5
|
+
initialValues?: Record<string, any>;
|
|
6
|
+
submitLabel?: string;
|
|
7
|
+
busy?: boolean;
|
|
8
|
+
showReset?: boolean;
|
|
9
|
+
onSubmit: (values: Record<string, any>) => void | Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
/** --------------------------------------------------------------------------------------- **/
|
|
12
|
+
export declare function SchemaForm({ model, schemaOverride, initialValues, submitLabel, busy, showReset, onSubmit, }: SchemaFormProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|