@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,592 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useState, useId } from 'react';
|
|
4
|
+
import { Form, Input, Textarea, Checkbox, RadioGroup, Radio, Select, SelectItem, Button, DatePicker, DateRangePicker, TimeInput, } from '@heroui/react';
|
|
5
|
+
import { useSelector } from 'react-redux';
|
|
6
|
+
import { inputTypeFor } from '../lib/schemaUtils';
|
|
7
|
+
import { RefMultiSelect } from './RefMultiSelect';
|
|
8
|
+
import { RefSingleSelect } from './RefSingleSelect';
|
|
9
|
+
import { PasswordInput } from './PasswordInput';
|
|
10
|
+
import { PhoneInput } from './PhoneInput';
|
|
11
|
+
import { FileUploader } from './FileUploader';
|
|
12
|
+
import AddressAutocompleteGoogle from './AddressAutocomplete';
|
|
13
|
+
import { useGoogleMapsKey } from '../hooks/useGoogleMapsKey';
|
|
14
|
+
import { parseDate, parseDateTime, parseTime, } from '@internationalized/date';
|
|
15
|
+
const inputClassNames = { inputWrapper: 'bg-transparent shadow-none' };
|
|
16
|
+
const selectClassNames = { trigger: 'bg-transparent shadow-none' };
|
|
17
|
+
const AUDIT_FIELDS = new Set(['createdAt', 'updatedAt']);
|
|
18
|
+
const digitsOnly = (s) => String(s ?? '').replace(/\D/g, '');
|
|
19
|
+
function readLocalUser() {
|
|
20
|
+
if (typeof window === 'undefined')
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = localStorage.getItem('nextmin.user');
|
|
24
|
+
return raw ? JSON.parse(raw) : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** ---------------------------- NEW HELPERS (DATE/TIME/RANGE) ---------------------------- **/
|
|
31
|
+
// Decide if an attribute is a date, time, or range, plus the withTime flag
|
|
32
|
+
function isDateType(attr) {
|
|
33
|
+
const t = String(attr?.type ?? '').toLowerCase();
|
|
34
|
+
const f = String(attr?.format ?? '').toLowerCase();
|
|
35
|
+
const it = String(inputTypeFor(attr) ?? '').toLowerCase(); // <-- force string
|
|
36
|
+
return (t === 'date' ||
|
|
37
|
+
f === 'date' ||
|
|
38
|
+
it === 'date' ||
|
|
39
|
+
it === 'datetime-local' ||
|
|
40
|
+
f === 'datetime' ||
|
|
41
|
+
t === 'datetime');
|
|
42
|
+
}
|
|
43
|
+
function isTimeType(attr) {
|
|
44
|
+
const t = String(attr?.type ?? '').toLowerCase();
|
|
45
|
+
const f = String(attr?.format ?? '').toLowerCase();
|
|
46
|
+
const it = String(inputTypeFor(attr) ?? '').toLowerCase(); // <-- force string
|
|
47
|
+
return t === 'time' || f === 'time' || it === 'time';
|
|
48
|
+
}
|
|
49
|
+
function isRangeType(attr) {
|
|
50
|
+
const t = String(attr?.type ?? '').toLowerCase();
|
|
51
|
+
const f = String(attr?.format ?? '').toLowerCase();
|
|
52
|
+
// support { type: 'range' }, { type: 'date-range' }, or { format: 'date-range' }
|
|
53
|
+
return (t === 'range' || t === 'date-range' || f === 'date-range' || f === 'range');
|
|
54
|
+
}
|
|
55
|
+
function withTimeEnabled(attr) {
|
|
56
|
+
return Boolean(attr?.withTime);
|
|
57
|
+
}
|
|
58
|
+
function parseTimeString(value) {
|
|
59
|
+
if (value == null)
|
|
60
|
+
return null;
|
|
61
|
+
let s = String(value).trim();
|
|
62
|
+
// accept 930 / 1530 → "09:30" / "15:30"
|
|
63
|
+
if (/^\d{3,4}$/.test(s)) {
|
|
64
|
+
const pad = s.padStart(4, '0');
|
|
65
|
+
s = `${pad.slice(0, 2)}:${pad.slice(2)}`;
|
|
66
|
+
}
|
|
67
|
+
// AM/PM path (e.g., "3:05 PM", "12:00 am")
|
|
68
|
+
const ampm = s.match(/\s*([AaPp][Mm])\s*$/);
|
|
69
|
+
if (ampm) {
|
|
70
|
+
const core = s.replace(/\s*[AaPp][Mm]\s*$/, '').trim();
|
|
71
|
+
const m = core.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
72
|
+
if (!m)
|
|
73
|
+
return null;
|
|
74
|
+
let h = Number(m[1]);
|
|
75
|
+
const min = Number(m[2]);
|
|
76
|
+
const sec = m[3] ? Number(m[3]) : 0;
|
|
77
|
+
const isPM = /^[Pp]/.test(ampm[1]);
|
|
78
|
+
if (isPM && h < 12)
|
|
79
|
+
h += 12; // 1..11 PM → 13..23
|
|
80
|
+
if (!isPM && h === 12)
|
|
81
|
+
h = 0; // 12 AM → 00
|
|
82
|
+
const hh = String(h).padStart(2, '0');
|
|
83
|
+
const mm = String(min).padStart(2, '0');
|
|
84
|
+
const ss = String(sec).padStart(2, '0');
|
|
85
|
+
try {
|
|
86
|
+
return parseTime(`${hh}:${mm}:${ss}`);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 24h path: "H:mm" / "HH:mm" / "HH:mm:ss"
|
|
93
|
+
const m24 = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
94
|
+
if (m24) {
|
|
95
|
+
const hh = String(Number(m24[1])).padStart(2, '0');
|
|
96
|
+
const mm = String(Number(m24[2])).padStart(2, '0');
|
|
97
|
+
const ss = String(m24[3] ? Number(m24[3]) : 0).padStart(2, '0');
|
|
98
|
+
try {
|
|
99
|
+
return parseTime(`${hh}:${mm}:${ss}`);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function serializeTime(t) {
|
|
108
|
+
// store canonical 24h "HH:mm"
|
|
109
|
+
return t ? t.toString().slice(0, 5) : '';
|
|
110
|
+
}
|
|
111
|
+
function parseDateString(v, withTime) {
|
|
112
|
+
if (!v || typeof v !== 'string')
|
|
113
|
+
return null;
|
|
114
|
+
try {
|
|
115
|
+
return withTime ? parseDateTime(v) : parseDate(v);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function normalizeRangeIn(value) {
|
|
122
|
+
if (!value)
|
|
123
|
+
return null;
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
const [s, e] = value;
|
|
126
|
+
return s && e ? { start: String(s), end: String(e) } : null;
|
|
127
|
+
}
|
|
128
|
+
if (typeof value === 'object') {
|
|
129
|
+
const anyv = value;
|
|
130
|
+
const s = anyv?.start ?? anyv?.from ?? anyv?.begin;
|
|
131
|
+
const e = anyv?.end ?? anyv?.to ?? anyv?.finish;
|
|
132
|
+
return s && e ? { start: String(s), end: String(e) } : null;
|
|
133
|
+
}
|
|
134
|
+
if (typeof value === 'string') {
|
|
135
|
+
const m = value.split('..');
|
|
136
|
+
return m.length === 2 && m[0] && m[1] ? { start: m[0], end: m[1] } : null;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function serializeDate(v, withTime) {
|
|
141
|
+
const raw = v.toString(); // "YYYY-MM-DD" or "YYYY-MM-DDThh:mm"
|
|
142
|
+
return withTime ? raw : raw.slice(0, 10);
|
|
143
|
+
}
|
|
144
|
+
function serializeRangeOut(start, end, withTime) {
|
|
145
|
+
if (!start || !end)
|
|
146
|
+
return null;
|
|
147
|
+
return {
|
|
148
|
+
start: serializeDate(start, withTime),
|
|
149
|
+
end: serializeDate(end, withTime),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function normalizeTimeRangeLoose(value) {
|
|
153
|
+
if (!value)
|
|
154
|
+
return {};
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
const [s, e] = value;
|
|
157
|
+
return {
|
|
158
|
+
start: s != null && s !== '' ? String(s) : undefined,
|
|
159
|
+
end: e != null && e !== '' ? String(e) : undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (typeof value === 'object') {
|
|
163
|
+
const anyv = value;
|
|
164
|
+
const s = anyv?.start ?? anyv?.from ?? anyv?.begin ?? '';
|
|
165
|
+
const e = anyv?.end ?? anyv?.to ?? anyv?.finish ?? '';
|
|
166
|
+
return {
|
|
167
|
+
start: s != null && s !== '' ? String(s) : undefined,
|
|
168
|
+
end: e != null && e !== '' ? String(e) : undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
const parts = value.split('..');
|
|
173
|
+
if (parts.length === 2) {
|
|
174
|
+
const [s, e] = parts;
|
|
175
|
+
return {
|
|
176
|
+
start: s != null && s !== '' ? s : undefined,
|
|
177
|
+
end: e != null && e !== '' ? e : undefined,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {};
|
|
182
|
+
}
|
|
183
|
+
/** --------------------------------------------------------------------------------------- **/
|
|
184
|
+
export function SchemaForm({ model, schemaOverride, initialValues, submitLabel = 'Save', busy = false, showReset = true, onSubmit, }) {
|
|
185
|
+
const formUid = useId();
|
|
186
|
+
const { items } = useSelector((s) => s.schemas);
|
|
187
|
+
const reduxUser = useSelector((s) => s?.session?.user ?? null) ??
|
|
188
|
+
null;
|
|
189
|
+
const effectiveUser = useMemo(() => reduxUser ?? readLocalUser(), [reduxUser]);
|
|
190
|
+
const sessionRole = useMemo(() => {
|
|
191
|
+
if (!effectiveUser)
|
|
192
|
+
return [];
|
|
193
|
+
const raw = effectiveUser.roles ?? effectiveUser.role;
|
|
194
|
+
const toName = (r) => {
|
|
195
|
+
if (!r)
|
|
196
|
+
return null;
|
|
197
|
+
if (typeof r === 'string')
|
|
198
|
+
return r;
|
|
199
|
+
if (typeof r?.name === 'string')
|
|
200
|
+
return r.name;
|
|
201
|
+
return null;
|
|
202
|
+
};
|
|
203
|
+
if (Array.isArray(raw)) {
|
|
204
|
+
return raw.map(toName).filter((s) => !!s);
|
|
205
|
+
}
|
|
206
|
+
const single = toName(raw);
|
|
207
|
+
return single ? [single] : [];
|
|
208
|
+
}, [effectiveUser]);
|
|
209
|
+
const schema = useMemo(() => schemaOverride ??
|
|
210
|
+
items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model, schemaOverride]);
|
|
211
|
+
const [form, setForm] = useState(initialValues ?? {});
|
|
212
|
+
const [error, setError] = useState();
|
|
213
|
+
const isEdit = useMemo(() => !!(initialValues &&
|
|
214
|
+
(initialValues.id || initialValues._id)), [initialValues]);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const next = { ...(initialValues ?? {}) };
|
|
217
|
+
if ('password' in next)
|
|
218
|
+
next.password = '';
|
|
219
|
+
if (schema) {
|
|
220
|
+
for (const [name, attr] of Object.entries(schema.attributes)) {
|
|
221
|
+
if (isPhoneAttr(name, attr) &&
|
|
222
|
+
typeof next[name] === 'string') {
|
|
223
|
+
next[name] = digitsOnly(next[name]);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
setForm(next);
|
|
228
|
+
}, [initialValues, schema]);
|
|
229
|
+
if (!schema)
|
|
230
|
+
return _jsx("div", { className: "text-danger text-sm", children: "Schema not found" });
|
|
231
|
+
const bypassRoles = useMemo(() => {
|
|
232
|
+
const raw = schema?.access?.bypassPrivacy?.roles ?? [];
|
|
233
|
+
const toName = (r) => typeof r === 'string' ? r : typeof r?.name === 'string' ? r.name : null;
|
|
234
|
+
const configured = raw
|
|
235
|
+
.map(toName)
|
|
236
|
+
.filter((s) => !!s)
|
|
237
|
+
.map((s) => s.toLowerCase());
|
|
238
|
+
return configured.length > 0 ? configured : ['admin', 'superadmin'];
|
|
239
|
+
}, [schema]);
|
|
240
|
+
const canBypassPrivacy = useMemo(() => {
|
|
241
|
+
const userRolesLC = sessionRole.map((r) => r.toLowerCase());
|
|
242
|
+
return userRolesLC.some((r) => bypassRoles.includes(r));
|
|
243
|
+
}, [sessionRole, bypassRoles]);
|
|
244
|
+
const fields = useMemo(() => Object.entries(schema.attributes)
|
|
245
|
+
.filter(([name]) => !AUDIT_FIELDS.has(name))
|
|
246
|
+
.filter(([, attr]) => canBypassPrivacy ? true : !attr?.private)
|
|
247
|
+
.filter(([, attr]) => !isHiddenAttr(attr, isEdit))
|
|
248
|
+
.filter(([name, attr]) => !(isEdit && isPasswordAttr(name, attr)))
|
|
249
|
+
.map(([name, attr]) => ({ name, attr })), [schema, canBypassPrivacy, isEdit]);
|
|
250
|
+
const gridClass = 'grid gap-4 grid-cols-2';
|
|
251
|
+
const handleChange = (name, value) => setForm((f) => ({ ...f, [name]: value }));
|
|
252
|
+
const handleSubmit = async (e) => {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
setError(undefined);
|
|
255
|
+
try {
|
|
256
|
+
const { createdAt, updatedAt, ...payload } = form;
|
|
257
|
+
await onSubmit(payload);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
setError(err?.message || 'Submission failed');
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const handleReset = () => {
|
|
264
|
+
const next = { ...(initialValues ?? {}) };
|
|
265
|
+
if ('password' in next)
|
|
266
|
+
next.password = '';
|
|
267
|
+
if (schema) {
|
|
268
|
+
for (const [name, attr] of Object.entries(schema.attributes)) {
|
|
269
|
+
if (isPhoneAttr(name, attr) &&
|
|
270
|
+
typeof next[name] === 'string') {
|
|
271
|
+
next[name] = digitsOnly(next[name]);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
setForm(next);
|
|
276
|
+
};
|
|
277
|
+
return (_jsxs(Form, { className: gridClass, onSubmit: handleSubmit, onReset: handleReset, validationBehavior: "native", encType: "multipart/form-data", children: [fields.map(({ name, attr }) => {
|
|
278
|
+
const colClass = 'col-span-1';
|
|
279
|
+
// --- 1) Array of references → multi select (1 column)
|
|
280
|
+
const refArray = getRefArraySpec(attr);
|
|
281
|
+
if (refArray) {
|
|
282
|
+
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));
|
|
283
|
+
}
|
|
284
|
+
// --- 2) Single reference → single select (1 column)
|
|
285
|
+
const refSingle = getRefSingleSpec(attr);
|
|
286
|
+
if (refSingle) {
|
|
287
|
+
return (_jsx("div", { className: colClass, children: _jsx(RefSingleSelect, { name: name, label: attr?.label ?? formatLabel(name), refModel: refSingle.ref, showKey: refSingle.show ?? 'name', description: attr?.description, value: normalizeId(form[name]), onChange: (id) => handleChange(name, id), disabled: busy, required: !!attr?.required, classNames: selectClassNames, pageSize: attr?.pageSize }) }, name));
|
|
288
|
+
}
|
|
289
|
+
// let password reflect local state, but never prefill
|
|
290
|
+
const baseValue = name.toLowerCase() === 'password'
|
|
291
|
+
? typeof form[name] === 'string'
|
|
292
|
+
? form[name]
|
|
293
|
+
: ''
|
|
294
|
+
: (form[name] ?? (Array.isArray(attr) ? [] : ''));
|
|
295
|
+
return (_jsx("div", { className: colClass, children: _jsx(SchemaField, { uid: formUid, name: name, attr: attr, value: baseValue, onChange: handleChange, disabled: busy, inputClassNames: inputClassNames, selectClassNames: selectClassNames }) }, name));
|
|
296
|
+
}), error && (_jsx("div", { className: "col-span-2", children: _jsx("div", { className: "text-danger text-sm", children: error }) })), _jsxs("div", { className: "flex gap-2 col-span-2", children: [_jsx(Button, { type: "submit", color: "primary", isLoading: busy, children: submitLabel }), showReset && (_jsx(Button, { type: "reset", variant: "flat", isDisabled: busy, children: "Reset" }))] })] }));
|
|
297
|
+
}
|
|
298
|
+
function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, }) {
|
|
299
|
+
const id = `${uid}-${name}`;
|
|
300
|
+
const label = attr?.label ?? formatLabel(name);
|
|
301
|
+
const required = !!attr?.required;
|
|
302
|
+
const description = attr?.description;
|
|
303
|
+
// Prefer ref over enum. Only use enum when there is NO ref.
|
|
304
|
+
const enumVals = attr?.ref
|
|
305
|
+
? undefined
|
|
306
|
+
: attr?.enum;
|
|
307
|
+
const isPasswordField = name.toLowerCase() === 'password' ||
|
|
308
|
+
attr?.format === 'password' ||
|
|
309
|
+
attr?.writeOnly === true;
|
|
310
|
+
const isPhoneField = isPhoneAttr(name, attr);
|
|
311
|
+
const rawMask = typeof attr?.mask === 'string' ? attr.mask : '';
|
|
312
|
+
const hasSlots = /[Xx9#_]/.test(rawMask);
|
|
313
|
+
const phoneMask = hasSlots ? rawMask : 'xxx-xxxx-xxxx';
|
|
314
|
+
const isFileField = String(attr?.format || '').toLowerCase() === 'file' ||
|
|
315
|
+
/^(file|image|avatar|photo|picture|logo|siteLogo|sitelogo|profilepicture|bannerImage)$/i.test(name);
|
|
316
|
+
if (isFileField) {
|
|
317
|
+
const head = Array.isArray(attr) ? attr[0] : attr;
|
|
318
|
+
const multiple = Array.isArray(attr) || Boolean(head?.multiple);
|
|
319
|
+
const fileTypes = head?.fileTypes;
|
|
320
|
+
const maxFileSize = typeof head?.maxFileSize === 'number' ? head.maxFileSize : undefined;
|
|
321
|
+
const maxFilesCount = multiple && typeof head?.maxFilesCount === 'number'
|
|
322
|
+
? Math.max(1, head.maxFilesCount)
|
|
323
|
+
: undefined;
|
|
324
|
+
const isRequired = !!attr?.required ||
|
|
325
|
+
(Array.isArray(attr) ? !!head?.required : false);
|
|
326
|
+
const controlledValue = multiple
|
|
327
|
+
? Array.isArray(value)
|
|
328
|
+
? value
|
|
329
|
+
: typeof value === 'string' && value
|
|
330
|
+
? [value]
|
|
331
|
+
: []
|
|
332
|
+
: typeof value === 'string'
|
|
333
|
+
? value
|
|
334
|
+
: null;
|
|
335
|
+
return (_jsx(FileUploader, { name: name, label: label, value: controlledValue, onChange: (v) => onChange(name, v), required: isRequired, disabled: disabled, accept: fileTypes, maxSizeBytes: maxFileSize, multiple: multiple, maxFilesCount: maxFilesCount, description: attr?.description, className: "w-full" }));
|
|
336
|
+
}
|
|
337
|
+
if (isPasswordField) {
|
|
338
|
+
return (_jsx(PasswordInput, { id: id, name: name, label: label, value: typeof value === 'string' ? value : '', onChange: (v) => onChange(name, v), disabled: disabled, required: required, description: description, className: "w-full", classNames: inputClassNames, autoComplete: "new-password" }));
|
|
339
|
+
}
|
|
340
|
+
if (isPhoneField) {
|
|
341
|
+
const rawDigits = typeof value === 'string' || typeof value === 'number'
|
|
342
|
+
? String(value).replace(/\D/g, '')
|
|
343
|
+
: '';
|
|
344
|
+
return (_jsx(PhoneInput, { id: id, name: name, label: label, mask: phoneMask, value: rawDigits, onChange: (raw) => onChange(name, raw), disabled: disabled, required: required, description: description, className: "w-full", classNames: inputClassNames }));
|
|
345
|
+
}
|
|
346
|
+
const mapsKey = useGoogleMapsKey();
|
|
347
|
+
const populateField = getPopulateTarget(attr);
|
|
348
|
+
// Address → Autocomplete
|
|
349
|
+
if (isAddressAttr(name, attr)) {
|
|
350
|
+
if (!mapsKey) {
|
|
351
|
+
return (_jsxs("div", { className: "text-danger text-xs", children: ["Google Maps API key is missing. Set ", _jsx("b", { children: "googleMapsKey" }), " in", ' ', _jsx("b", { children: "Settings" }), " or define ", _jsx("b", { children: "NEXT_PUBLIC_GOOGLE_MAPS_KEY" }), " in the environment file."] }));
|
|
352
|
+
}
|
|
353
|
+
return (_jsx(AddressAutocompleteGoogle, { apiKey: mapsKey, name: name, label: label, description: description, value: typeof value === 'string' ? value : '', onChange: (addr, latlng) => {
|
|
354
|
+
if (!latlng)
|
|
355
|
+
return;
|
|
356
|
+
onChange(name, addr);
|
|
357
|
+
if (populateField) {
|
|
358
|
+
onChange(populateField, `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`);
|
|
359
|
+
}
|
|
360
|
+
}, disabled: disabled, required: required, className: "w-full", classNames: inputClassNames, countryCodes: attr?.countryCodes, limit: attr?.limit ?? 8 }));
|
|
361
|
+
}
|
|
362
|
+
/** ----------------------- NEW: DATE / TIME / RANGE RENDERERS ----------------------- **/
|
|
363
|
+
// 1) TIME-ONLY RANGE → two TimeInput fields (AM/PM UI)
|
|
364
|
+
if (isRangeType(attr) && attr?.timeOnly === true) {
|
|
365
|
+
const raw = normalizeTimeRangeLoose(value);
|
|
366
|
+
const startT = parseTimeString(raw.start ?? null);
|
|
367
|
+
const endT = parseTimeString(raw.end ?? null);
|
|
368
|
+
const setStart = (t) => {
|
|
369
|
+
onChange(name, {
|
|
370
|
+
start: serializeTime(t),
|
|
371
|
+
end: endT ? serializeTime(endT) : '',
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
const setEnd = (t) => {
|
|
375
|
+
onChange(name, {
|
|
376
|
+
start: startT ? serializeTime(startT) : '00:00', // default start if user sets end first
|
|
377
|
+
end: serializeTime(t),
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
return (_jsx("div", { className: "w-full", children: _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(TimeInput, { variant: "bordered", label: `${label} (Start)`, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: startT ?? undefined, onChange: setStart, granularity: "minute", hourCycle: 12 }), _jsx(TimeInput, { variant: "bordered", label: `${label} (End)`, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: endT ?? undefined, onChange: setEnd, granularity: "minute", hourCycle: 12 })] }) }));
|
|
381
|
+
}
|
|
382
|
+
// 2) DATE RANGE (default; supports withTime)
|
|
383
|
+
if (isRangeType(attr)) {
|
|
384
|
+
const withTime = withTimeEnabled(attr);
|
|
385
|
+
const norm = normalizeRangeIn(value);
|
|
386
|
+
const startVal = norm?.start ? parseDateString(norm.start, withTime) : null;
|
|
387
|
+
const endVal = norm?.end ? parseDateString(norm.end, withTime) : null;
|
|
388
|
+
return (_jsx(DateRangePicker, { variant: "bordered", label: label, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: startVal && endVal ? { start: startVal, end: endVal } : null, onChange: (val) => {
|
|
389
|
+
if (!val)
|
|
390
|
+
return onChange(name, null);
|
|
391
|
+
const { start, end } = val;
|
|
392
|
+
onChange(name, serializeRangeOut(start, end, withTime));
|
|
393
|
+
}, granularity: withTime ? 'minute' : 'day', hourCycle: withTime ? 24 : undefined }));
|
|
394
|
+
}
|
|
395
|
+
// 3) SINGLE DATE (supports withTime)
|
|
396
|
+
if (isDateType(attr)) {
|
|
397
|
+
const withTime = withTimeEnabled(attr);
|
|
398
|
+
const parsed = parseDateString(value, withTime);
|
|
399
|
+
return (_jsx(DatePicker, { variant: "bordered", label: label, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: parsed ?? null, onChange: (v) => onChange(name, v
|
|
400
|
+
? serializeDate(v, withTime)
|
|
401
|
+
: ''), granularity: withTime ? 'minute' : 'day', hourCycle: withTime ? 24 : undefined }));
|
|
402
|
+
}
|
|
403
|
+
// 4) SINGLE TIME (uses your existing isTimeType)
|
|
404
|
+
if (isTimeType(attr)) {
|
|
405
|
+
const tVal = parseTimeString(value);
|
|
406
|
+
return (_jsx(TimeInput, { variant: "bordered", label: label, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: tVal ?? undefined, onChange: (t) => onChange(name, serializeTime(t)), granularity: "minute", hourCycle: 12 }));
|
|
407
|
+
}
|
|
408
|
+
const normalize = (s) => (s ?? '').toLowerCase().trim();
|
|
409
|
+
const canSeeOption = (opt) => {
|
|
410
|
+
const raw = typeof window !== 'undefined'
|
|
411
|
+
? localStorage.getItem('nextmin.user')
|
|
412
|
+
: null;
|
|
413
|
+
const currentUser = raw ? JSON.parse(raw) : null;
|
|
414
|
+
if (!currentUser)
|
|
415
|
+
return false;
|
|
416
|
+
const o = normalize(opt);
|
|
417
|
+
const roles = currentUser.role ?? currentUser.roles ?? [];
|
|
418
|
+
const roleNames = new Set(roles.map((r) => normalize(r.name)));
|
|
419
|
+
if (roleNames.has('superadmin'))
|
|
420
|
+
return true;
|
|
421
|
+
if (o === 'system')
|
|
422
|
+
return false;
|
|
423
|
+
return true;
|
|
424
|
+
};
|
|
425
|
+
// Enum → RadioGroup (≤4) or Select (>4)
|
|
426
|
+
if (enumVals && enumVals.length > 0) {
|
|
427
|
+
if (enumVals.length <= 4) {
|
|
428
|
+
return (_jsx(RadioGroup, { id: id, name: name, label: label, orientation: "horizontal", value: (value ?? ''), onValueChange: (v) => onChange(name, v), isDisabled: disabled, description: description, isRequired: required, className: "w-full", children: enumVals.map((opt) => {
|
|
429
|
+
if (!canSeeOption(opt))
|
|
430
|
+
return null;
|
|
431
|
+
return (_jsx(Radio, { value: opt, children: opt }, opt));
|
|
432
|
+
}) }));
|
|
433
|
+
}
|
|
434
|
+
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) => {
|
|
435
|
+
if (keys === 'all')
|
|
436
|
+
return;
|
|
437
|
+
const v = Array.from(keys)[0];
|
|
438
|
+
onChange(name, v != null ? String(v) : '');
|
|
439
|
+
}, isDisabled: disabled, description: description, className: "w-full", selectionMode: "single", isRequired: required, children: enumVals.map((opt) => (_jsx(SelectItem, { textValue: opt, children: opt }, opt))) }));
|
|
440
|
+
}
|
|
441
|
+
// Boolean → Checkbox
|
|
442
|
+
if (inputTypeFor(attr) === 'checkbox') {
|
|
443
|
+
return (_jsxs("div", { className: "w-full", children: [_jsx(Checkbox, { id: id, name: name, isSelected: !!value, onValueChange: (v) => onChange(name, v), isDisabled: disabled, isRequired: required, children: label }), description ? (_jsx("p", { className: "text-default-500 text-xs mt-1", children: description })) : null] }));
|
|
444
|
+
}
|
|
445
|
+
// Textarea
|
|
446
|
+
if (inputTypeFor(attr) === 'textarea') {
|
|
447
|
+
return (_jsx(Textarea, { variant: "bordered", classNames: inputClassNames, id: id, name: name, label: label, labelPlacement: "outside", value: value ?? '', onChange: (e) => onChange(name, e.target.value), isDisabled: disabled, minRows: 3, isReadOnly: attr.readOnly, description: description, className: "w-full", isRequired: required }));
|
|
448
|
+
}
|
|
449
|
+
function attrHead(attr) {
|
|
450
|
+
return Array.isArray(attr) ? attr[0] : attr;
|
|
451
|
+
}
|
|
452
|
+
function isAttrReadOnly(attr) {
|
|
453
|
+
const a = attrHead(attr);
|
|
454
|
+
return !!a?.readOnly;
|
|
455
|
+
}
|
|
456
|
+
// Array (non-ref) → comma-separated input
|
|
457
|
+
if (Array.isArray(attr)) {
|
|
458
|
+
return (_jsx(Input, { variant: "bordered", classNames: inputClassNames, id: id, name: name, label: label, isReadOnly: isAttrReadOnly(attr), labelPlacement: "outside-top", placeholder: "comma-separated values", value: Array.isArray(value) ? value.join(', ') : (value ?? ''), onChange: (e) => onChange(name, e.target.value
|
|
459
|
+
.split(',')
|
|
460
|
+
.map((s) => s.trim())
|
|
461
|
+
.filter(Boolean)), isDisabled: disabled, description: description ?? 'Enter values separated by commas.', className: "w-full", isRequired: required }));
|
|
462
|
+
}
|
|
463
|
+
function pickInputType(x, fieldName) {
|
|
464
|
+
if (fieldName.toLowerCase() === 'password')
|
|
465
|
+
return 'password';
|
|
466
|
+
switch (x) {
|
|
467
|
+
case 'number':
|
|
468
|
+
case 'email':
|
|
469
|
+
case 'password':
|
|
470
|
+
case 'date':
|
|
471
|
+
case 'time':
|
|
472
|
+
case 'datetime-local':
|
|
473
|
+
return x;
|
|
474
|
+
default:
|
|
475
|
+
return 'text';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const inputType = pickInputType(inputTypeFor(attr), name);
|
|
479
|
+
return (_jsx(Input, { variant: "bordered", classNames: inputClassNames, id: id, name: name, label: label, isReadOnly: attr.readOnly, labelPlacement: "outside-top", type: inputType, value: value ?? '', onChange: (e) => onChange(name, inputType === 'number'
|
|
480
|
+
? numberOrEmpty(e.target.value)
|
|
481
|
+
: e.target.value), isDisabled: disabled, description: description, className: "w-full", isRequired: required, autoComplete: name.toLowerCase() === 'password' ? 'new-password' : undefined }));
|
|
482
|
+
}
|
|
483
|
+
function isHiddenAttr(attr, isEdit) {
|
|
484
|
+
const head = Array.isArray(attr) ? (attr?.[0] ?? {}) : (attr ?? {});
|
|
485
|
+
const rule = head?.hidden;
|
|
486
|
+
if (typeof rule === 'boolean')
|
|
487
|
+
return rule;
|
|
488
|
+
if (rule && typeof rule === 'object') {
|
|
489
|
+
return isEdit ? !!rule.edit : !!rule.create;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
function isPhoneAttr(name, attr) {
|
|
494
|
+
const byName = /phone/i.test(name);
|
|
495
|
+
const byFormat = String(attr?.format || '').toLowerCase() === 'phone';
|
|
496
|
+
const byType = String(attr?.type || '').toLowerCase() === 'phone';
|
|
497
|
+
const byMask = typeof attr?.mask === 'string' && attr.mask.includes('x');
|
|
498
|
+
return byName || byFormat || byType || byMask;
|
|
499
|
+
}
|
|
500
|
+
function isPasswordAttr(name, attr) {
|
|
501
|
+
const byName = /^password$/i.test(name);
|
|
502
|
+
const byFormat = String(attr?.format || '').toLowerCase() === 'password';
|
|
503
|
+
const byWriteOnly = attr?.writeOnly === true;
|
|
504
|
+
return byName || byFormat || byWriteOnly;
|
|
505
|
+
}
|
|
506
|
+
function getRefArraySpec(attr) {
|
|
507
|
+
if (!Array.isArray(attr) || attr.length === 0)
|
|
508
|
+
return null;
|
|
509
|
+
const head = attr[0];
|
|
510
|
+
if (head &&
|
|
511
|
+
typeof head === 'object' &&
|
|
512
|
+
String(head.type).toLowerCase() === 'objectid' &&
|
|
513
|
+
typeof head.ref === 'string') {
|
|
514
|
+
return { ref: head.ref, show: head.show };
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
function getRefSingleSpec(attr) {
|
|
519
|
+
if (!attr || typeof attr !== 'object' || Array.isArray(attr))
|
|
520
|
+
return null;
|
|
521
|
+
const a = attr;
|
|
522
|
+
const t = String(a.type || '').toLowerCase();
|
|
523
|
+
if (t === 'objectid' && typeof a.ref === 'string' && a.ref.trim()) {
|
|
524
|
+
return { ref: a.ref, show: a.show };
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
function normalizeIdsArray(raw) {
|
|
529
|
+
if (!raw)
|
|
530
|
+
return [];
|
|
531
|
+
if (Array.isArray(raw)) {
|
|
532
|
+
return raw
|
|
533
|
+
.map((v) => {
|
|
534
|
+
if (typeof v === 'string')
|
|
535
|
+
return v;
|
|
536
|
+
if (v && typeof v === 'object') {
|
|
537
|
+
const anyv = v;
|
|
538
|
+
return ((typeof anyv.id === 'string' && anyv.id) ||
|
|
539
|
+
(typeof anyv._id === 'string' && anyv._id) ||
|
|
540
|
+
null);
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
})
|
|
544
|
+
.filter((x) => !!x);
|
|
545
|
+
}
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
function normalizeId(raw) {
|
|
549
|
+
if (!raw)
|
|
550
|
+
return null;
|
|
551
|
+
if (typeof raw === 'string')
|
|
552
|
+
return raw;
|
|
553
|
+
if (typeof raw === 'object') {
|
|
554
|
+
const anyv = raw;
|
|
555
|
+
const id = (typeof anyv.id === 'string' && anyv.id) ||
|
|
556
|
+
(typeof anyv._id === 'string' && anyv._id) ||
|
|
557
|
+
null;
|
|
558
|
+
return id;
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
function formatLabel(raw) {
|
|
563
|
+
const spaced = raw
|
|
564
|
+
.replace(/[_\-]+/g, ' ')
|
|
565
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
566
|
+
.trim();
|
|
567
|
+
return spaced
|
|
568
|
+
.split(/\s+/)
|
|
569
|
+
.map((w) => (w ? w[0].toUpperCase() + w.slice(1).toLowerCase() : w))
|
|
570
|
+
.join(' ');
|
|
571
|
+
}
|
|
572
|
+
function numberOrEmpty(v) {
|
|
573
|
+
if (v === '' || v === null || v === undefined)
|
|
574
|
+
return '';
|
|
575
|
+
const n = Number(v);
|
|
576
|
+
return Number.isNaN(n) ? '' : n;
|
|
577
|
+
}
|
|
578
|
+
function isAddressAttr(name, attr) {
|
|
579
|
+
const fname = String(name || '').toLowerCase();
|
|
580
|
+
const byName = /(address|location)/i.test(fname);
|
|
581
|
+
const byFormat = String(attr?.format || '').toLowerCase() === 'address';
|
|
582
|
+
const byPopulate = typeof attr?.populate === 'string' &&
|
|
583
|
+
attr.populate.length > 0;
|
|
584
|
+
const isStringType = String(attr?.type || '').toLowerCase() === 'string' ||
|
|
585
|
+
!('type' in attr);
|
|
586
|
+
return isStringType && (byName || byFormat || byPopulate);
|
|
587
|
+
}
|
|
588
|
+
function getPopulateTarget(attr) {
|
|
589
|
+
const head = Array.isArray(attr) ? (attr?.[0] ?? {}) : (attr ?? {});
|
|
590
|
+
const t = head?.populate ?? attr?.populate;
|
|
591
|
+
return typeof t === 'string' && t.trim() ? t.trim() : null;
|
|
592
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Spinner } from '@heroui/react';
|
|
4
|
+
export function SectionLoader({ label = 'Loading…' }) {
|
|
5
|
+
// Fills only the parent area; parent should have `relative`
|
|
6
|
+
return (_jsx("div", { className: "absolute inset-0 grid place-items-center", children: _jsxs("div", { className: "flex items-center gap-3 rounded-xl border bg-background/70 backdrop-blur px-4 py-3 shadow-sm", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { className: "text-sm text-foreground/80", children: label })] }) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Sidebar(): import("react/jsx-runtime").JSX.Element;
|