@airoom/nextmin-react 1.4.6 → 2.0.2
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/README.md +29 -3
- package/dist/auth/SignInForm.js +4 -2
- package/dist/components/AdminApp.js +15 -38
- package/dist/components/ArchitectureDemo.d.ts +1 -0
- package/dist/components/ArchitectureDemo.js +45 -0
- package/dist/components/PhoneInput.d.ts +3 -0
- package/dist/components/PhoneInput.js +23 -19
- package/dist/components/RefSelect.d.ts +16 -0
- package/dist/components/RefSelect.js +225 -0
- package/dist/components/SchemaForm.js +125 -50
- package/dist/components/Sidebar.js +6 -13
- package/dist/components/TableFilters.js +2 -0
- package/dist/components/editor/TiptapEditor.js +4 -2
- package/dist/components/editor/Toolbar.js +21 -2
- package/dist/components/editor/components/DistrictGridModal.js +2 -3
- package/dist/components/editor/components/SchemaInsertionModal.js +2 -2
- package/dist/components/viewer/DynamicViewer.js +70 -9
- package/dist/hooks/useRealtime.d.ts +8 -0
- package/dist/hooks/useRealtime.js +30 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/AuthClient.d.ts +15 -0
- package/dist/lib/AuthClient.js +63 -0
- package/dist/lib/QueryBuilder.d.ts +29 -0
- package/dist/lib/QueryBuilder.js +74 -0
- package/dist/lib/RealtimeClient.d.ts +16 -0
- package/dist/lib/RealtimeClient.js +56 -0
- package/dist/lib/api.d.ts +15 -3
- package/dist/lib/api.js +71 -58
- package/dist/lib/auth.js +7 -2
- package/dist/lib/types.d.ts +16 -0
- package/dist/nextmin.css +1 -1
- package/dist/providers/NextMinProvider.d.ts +8 -1
- package/dist/providers/NextMinProvider.js +40 -8
- package/dist/router/NextMinRouter.d.ts +1 -1
- package/dist/router/NextMinRouter.js +1 -1
- package/dist/state/schemasSlice.js +4 -27
- package/dist/views/DashboardPage.js +56 -42
- package/dist/views/ListPage.js +34 -4
- package/dist/views/SettingsEdit.js +25 -2
- package/dist/views/list/DataTableHero.js +103 -46
- package/dist/views/list/ListHeader.d.ts +3 -1
- package/dist/views/list/ListHeader.js +2 -2
- package/dist/views/list/jsonSummary.d.ts +3 -3
- package/dist/views/list/jsonSummary.js +47 -20
- package/dist/views/list/useListData.js +5 -1
- package/package.json +9 -4
- package/dist/components/RefMultiSelect.d.ts +0 -22
- package/dist/components/RefMultiSelect.js +0 -113
- package/dist/components/RefSingleSelect.d.ts +0 -17
- package/dist/components/RefSingleSelect.js +0 -110
- package/dist/lib/schemaService.d.ts +0 -2
- package/dist/lib/schemaService.js +0 -39
- package/dist/state/schemaLive.d.ts +0 -2
- package/dist/state/schemaLive.js +0 -19
- /package/dist/{editor.css → components/editor/editor.css} +0 -0
|
@@ -4,8 +4,7 @@ import { useEffect, useMemo, useState, useId } from 'react';
|
|
|
4
4
|
import { Form, Input, Textarea, Checkbox, RadioGroup, Radio, Select, SelectItem, Button, DatePicker, DateRangePicker, TimeInput, } from '@heroui/react';
|
|
5
5
|
import { useSelector } from 'react-redux';
|
|
6
6
|
import { inputTypeFor } from '../lib/schemaUtils';
|
|
7
|
-
import {
|
|
8
|
-
import { RefSingleSelect } from './RefSingleSelect';
|
|
7
|
+
import { RefSelect } from './RefSelect';
|
|
9
8
|
import { PasswordInput } from './PasswordInput';
|
|
10
9
|
import { PhoneInput } from './PhoneInput';
|
|
11
10
|
import { FileUploader } from './FileUploader';
|
|
@@ -278,7 +277,8 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
278
277
|
return raw.map(toName).filter((s) => !!s).map(s => s.toLowerCase());
|
|
279
278
|
}
|
|
280
279
|
const single = toName(raw);
|
|
281
|
-
|
|
280
|
+
const result = single ? [single.toLowerCase()] : [];
|
|
281
|
+
return result;
|
|
282
282
|
}, [effectiveUser]);
|
|
283
283
|
const schema = useMemo(() => schemaOverride ??
|
|
284
284
|
items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model, schemaOverride]);
|
|
@@ -333,20 +333,72 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
333
333
|
.map(toName)
|
|
334
334
|
.filter((s) => !!s)
|
|
335
335
|
.map((s) => s.toLowerCase());
|
|
336
|
-
|
|
336
|
+
const result = configured.length > 0 ? configured : ['admin', 'superadmin'];
|
|
337
|
+
return result;
|
|
337
338
|
}, [schema]);
|
|
338
339
|
const canBypassPrivacy = useMemo(() => {
|
|
339
340
|
const userRolesLC = sessionRole.map((r) => r.toLowerCase());
|
|
340
|
-
|
|
341
|
+
const result = userRolesLC.some((r) => bypassRoles.includes(r));
|
|
342
|
+
return result;
|
|
341
343
|
}, [sessionRole, bypassRoles]);
|
|
342
|
-
const fields = useMemo(() =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.filter(([name]) => name !== '
|
|
346
|
-
.filter(([, attr]) =>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
344
|
+
const fields = useMemo(() => {
|
|
345
|
+
const allFields = Object.entries(schema.attributes);
|
|
346
|
+
const afterAudit = allFields.filter(([name]) => !AUDIT_FIELDS.has(name));
|
|
347
|
+
const afterBaseId = afterAudit.filter(([name]) => name !== 'exId');
|
|
348
|
+
const afterPrivate = afterBaseId.filter(([name, attr]) => {
|
|
349
|
+
const head = Array.isArray(attr) ? attr[0] : attr;
|
|
350
|
+
// Always show password fields in create mode for users with bypass privacy
|
|
351
|
+
if (!isEdit && isPasswordAttr(name, attr) && canBypassPrivacy) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
// Otherwise apply normal private field filtering
|
|
355
|
+
const keep = canBypassPrivacy ? true : !head?.private;
|
|
356
|
+
return keep;
|
|
357
|
+
});
|
|
358
|
+
const afterHidden = afterPrivate.filter(([, attr]) => !isHiddenAttr(attr, isEdit));
|
|
359
|
+
const isUserExt = schema.extends?.toLowerCase() === 'users';
|
|
360
|
+
const afterInherited = afterHidden.filter(([name, attr]) => {
|
|
361
|
+
const head = Array.isArray(attr) ? attr[0] : attr;
|
|
362
|
+
// Specifically for User extensions, hide inherited fields unless they are explicitly not marked inherited
|
|
363
|
+
if (isUserExt && name !== 'baseId') {
|
|
364
|
+
return !head?.inherited;
|
|
365
|
+
}
|
|
366
|
+
return true;
|
|
367
|
+
});
|
|
368
|
+
const afterPassword = afterInherited.filter(([name, attr]) => {
|
|
369
|
+
const isPass = isPasswordAttr(name, attr);
|
|
370
|
+
const keep = !(isEdit && isPass && !canBypassPrivacy);
|
|
371
|
+
return keep;
|
|
372
|
+
});
|
|
373
|
+
let finalFields = afterPassword.map(([name, attr]) => ({ name, attr }));
|
|
374
|
+
// If it's a User extension, move baseId to the top and inject the role filter
|
|
375
|
+
if (isUserExt) {
|
|
376
|
+
const baseIdIndex = finalFields.findIndex(f => f.name === 'baseId');
|
|
377
|
+
if (baseIdIndex !== -1) {
|
|
378
|
+
const [baseIdField] = finalFields.splice(baseIdIndex, 1);
|
|
379
|
+
// Inject the filter into baseId attribute (clone to avoid "not extensible" error)
|
|
380
|
+
const baseIdFieldClone = {
|
|
381
|
+
...baseIdField,
|
|
382
|
+
attr: { ...baseIdField.attr }
|
|
383
|
+
};
|
|
384
|
+
const baseIdAttr = baseIdFieldClone.attr;
|
|
385
|
+
if (baseIdAttr) {
|
|
386
|
+
baseIdAttr.where = {
|
|
387
|
+
role: {
|
|
388
|
+
$nestedSearch: {
|
|
389
|
+
ref: 'Roles',
|
|
390
|
+
show: 'name',
|
|
391
|
+
value: ['admin', 'superadmin', 'manager'],
|
|
392
|
+
operator: '$nin'
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
finalFields.unshift(baseIdFieldClone);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return finalFields;
|
|
401
|
+
}, [schema, canBypassPrivacy, isEdit]);
|
|
350
402
|
const gridClass = 'grid gap-4 grid-cols-2';
|
|
351
403
|
const handleChange = (name, value) => {
|
|
352
404
|
setForm((f) => {
|
|
@@ -390,7 +442,7 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
390
442
|
e.preventDefault();
|
|
391
443
|
setError(undefined);
|
|
392
444
|
try {
|
|
393
|
-
const { createdAt, updatedAt,
|
|
445
|
+
const { createdAt, updatedAt, __childId, ...rest } = form;
|
|
394
446
|
const payload = { ...rest };
|
|
395
447
|
if (schema) {
|
|
396
448
|
for (const [fname, fattr] of Object.entries(schema.attributes)) {
|
|
@@ -432,14 +484,17 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
432
484
|
const canRemove = spec.minItems == null || items.length > spec.minItems;
|
|
433
485
|
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: [_jsxs("div", { className: "col-span-2 font-medium text-default-600", children: [(attr?.label ||
|
|
434
486
|
spec.label ||
|
|
435
|
-
formatLabel(name)).replace(/s$/, ''), ' ', idx + 1] }), Object.entries(itemSchema).map(([k, a]) =>
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
487
|
+
formatLabel(name)).replace(/s$/, ''), ' ', idx + 1] }), Object.entries(itemSchema).map(([k, a]) => {
|
|
488
|
+
const isRich = a?.rich === true;
|
|
489
|
+
return (_jsx("div", { className: isRich ? 'col-span-2' : 'col-span-1', children: _jsx(SchemaField, { uid: `${formUid}-${name}-${idx}`, name: k, attr: a, value: it?.[k], onChange: (fieldName, fieldValue) => {
|
|
490
|
+
const next = items.slice();
|
|
491
|
+
next[idx] = {
|
|
492
|
+
...(next[idx] || {}),
|
|
493
|
+
[fieldName]: fieldValue,
|
|
494
|
+
};
|
|
495
|
+
handleChange(name, next);
|
|
496
|
+
}, disabled: busy, inputClassNames: inputClassNames, selectClassNames: selectClassNames, mapsKey: mapsKey, availableSchemas: items, formState: it }) }, `${name}-${idx}-${k}`));
|
|
497
|
+
}), _jsx("div", { className: "col-span-2 flex justify-between", children: _jsx(Button, { size: "sm", variant: "flat", color: "danger", isDisabled: !canRemove, onPress: () => {
|
|
443
498
|
const next = items.slice();
|
|
444
499
|
next.splice(idx, 1);
|
|
445
500
|
handleChange(name, next);
|
|
@@ -453,23 +508,13 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
453
508
|
!Array.isArray(form[name])
|
|
454
509
|
? form[name]
|
|
455
510
|
: parseJsonGroupValue(form[name], 'single');
|
|
456
|
-
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]) =>
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
// --- 1) Array of references → multi select (1 column)
|
|
465
|
-
const refArray = getRefArraySpec(attr);
|
|
466
|
-
if (refArray) {
|
|
467
|
-
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));
|
|
468
|
-
}
|
|
469
|
-
// --- 2) Single reference → single select (1 column)
|
|
470
|
-
const refSingle = getRefSingleSpec(attr);
|
|
471
|
-
if (refSingle) {
|
|
472
|
-
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));
|
|
511
|
+
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]) => {
|
|
512
|
+
const isRich = a?.rich === true;
|
|
513
|
+
return (_jsx("div", { className: isRich ? 'col-span-2' : 'col-span-1', children: _jsx(SchemaField, { uid: `${formUid}-${name}`, name: k, attr: a, value: obj?.[k], onChange: (fieldName, fieldValue) => {
|
|
514
|
+
const next = { ...(obj || {}), [fieldName]: fieldValue };
|
|
515
|
+
handleChange(name, next);
|
|
516
|
+
}, disabled: busy, inputClassNames: inputClassNames, selectClassNames: selectClassNames, mapsKey: mapsKey, availableSchemas: items, formState: obj }) }, `${name}-${k}`));
|
|
517
|
+
}) })] }, name));
|
|
473
518
|
}
|
|
474
519
|
// let password reflect local state, but never prefill
|
|
475
520
|
const baseValue = name.toLowerCase() === 'password'
|
|
@@ -505,8 +550,19 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
|
|
|
505
550
|
const specialitySlug = formState?.speciality?.slug || formState?.slug || '';
|
|
506
551
|
return (_jsx(TiptapEditor, { value: value, onChange: (html) => onChange(name, html), placeholder: label, availableSchemas: availableSchemas, currentSpeciality: specialitySlug }));
|
|
507
552
|
}
|
|
553
|
+
// --- 1) Array of references → multi select
|
|
554
|
+
const refArray = getRefArraySpec(attr);
|
|
555
|
+
if (refArray) {
|
|
556
|
+
return (_jsx(RefSelect, { name: name, label: attr[0]?.label ?? label, refModel: refArray.ref, showKey: refArray.show ?? 'name', description: attr?.description, value: normalizeIdsArray(value), onChange: (ids) => onChange(name, ids), multiple: true, disabled: disabled, required: attr[0]?.required, pageSize: attr[0]?.pageSize, where: attr[0]?.where || attr?.where }));
|
|
557
|
+
}
|
|
558
|
+
// --- 2) Single reference → single select
|
|
559
|
+
const refSingle = getRefSingleSpec(attr);
|
|
560
|
+
if (refSingle) {
|
|
561
|
+
return (_jsx(RefSelect, { name: name, label: label, refModel: refSingle.ref, showKey: refSingle.show ?? 'name', description: attr?.description, value: normalizeId(value), onChange: (id) => onChange(name, id), multiple: false, disabled: disabled, required: required, pageSize: attr?.pageSize, where: attr?.where }));
|
|
562
|
+
}
|
|
563
|
+
const head = Array.isArray(attr) ? (attr?.[0] ?? {}) : (attr ?? {});
|
|
508
564
|
const isPhoneField = isPhoneAttr(name, attr);
|
|
509
|
-
const rawMask = typeof
|
|
565
|
+
const rawMask = typeof head?.mask === 'string' ? head.mask : '';
|
|
510
566
|
const hasSlots = /[Xx9#_]/.test(rawMask);
|
|
511
567
|
const phoneMask = hasSlots ? rawMask : 'xxx-xxxx-xxxx';
|
|
512
568
|
const isFileField = String(attr?.format || '').toLowerCase() === 'file' ||
|
|
@@ -539,7 +595,7 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
|
|
|
539
595
|
const rawDigits = typeof value === 'string' || typeof value === 'number'
|
|
540
596
|
? String(value).replace(/\D/g, '')
|
|
541
597
|
: '';
|
|
542
|
-
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 }));
|
|
598
|
+
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, minLength: head?.minLength, maxLength: head?.maxLength, regex: head?.pattern }));
|
|
543
599
|
}
|
|
544
600
|
const populateField = getPopulateTarget(attr);
|
|
545
601
|
// Address → Autocomplete
|
|
@@ -611,7 +667,19 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
|
|
|
611
667
|
if (!currentUser)
|
|
612
668
|
return false;
|
|
613
669
|
const o = normalize(opt);
|
|
614
|
-
|
|
670
|
+
// Handle roleName field (preferred for SQL databases)
|
|
671
|
+
const roleName = currentUser?.roleName;
|
|
672
|
+
if (typeof roleName === 'string') {
|
|
673
|
+
const normalized = normalize(roleName);
|
|
674
|
+
if (normalized === 'superadmin')
|
|
675
|
+
return true;
|
|
676
|
+
if (o === 'system')
|
|
677
|
+
return false;
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
// Fallback: handle role/roles arrays
|
|
681
|
+
const roleField = currentUser.role ?? currentUser.roles;
|
|
682
|
+
const roles = Array.isArray(roleField) ? roleField : [];
|
|
615
683
|
const roleNames = new Set(roles.map((r) => normalize(r.name)));
|
|
616
684
|
if (roleNames.has('superadmin'))
|
|
617
685
|
return true;
|
|
@@ -675,7 +743,7 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
|
|
|
675
743
|
const inputType = pickInputType(inputTypeFor(attr), name);
|
|
676
744
|
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'
|
|
677
745
|
? numberOrEmpty(e.target.value)
|
|
678
|
-
: e.target.value), isDisabled: disabled, description: description, className: "w-full", isRequired: required, autoComplete: name.toLowerCase() === 'password' ? 'new-password' : undefined }));
|
|
746
|
+
: e.target.value), isDisabled: disabled, description: description, className: "w-full", isRequired: required, minLength: head?.minLength, maxLength: head?.maxLength, pattern: head?.pattern, autoComplete: name.toLowerCase() === 'password' ? 'new-password' : undefined }));
|
|
679
747
|
}
|
|
680
748
|
function isHiddenAttr(attr, isEdit) {
|
|
681
749
|
const head = Array.isArray(attr) ? (attr?.[0] ?? {}) : (attr ?? {});
|
|
@@ -688,10 +756,15 @@ function isHiddenAttr(attr, isEdit) {
|
|
|
688
756
|
return false;
|
|
689
757
|
}
|
|
690
758
|
function isPhoneAttr(name, attr) {
|
|
759
|
+
const head = Array.isArray(attr) ? (attr?.[0] ?? {}) : (attr ?? {});
|
|
760
|
+
const fmt = String(head.format || attr?.format || '').toLowerCase();
|
|
761
|
+
// Explicit "text" format bypasses automatic phone detection
|
|
762
|
+
if (fmt === 'text')
|
|
763
|
+
return false;
|
|
691
764
|
const byName = /phone/i.test(name);
|
|
692
|
-
const byFormat =
|
|
693
|
-
const byType = String(attr?.type || '').toLowerCase() === 'phone';
|
|
694
|
-
const byMask = typeof
|
|
765
|
+
const byFormat = fmt === 'phone';
|
|
766
|
+
const byType = String(head.type || attr?.type || '').toLowerCase() === 'phone';
|
|
767
|
+
const byMask = typeof head.mask === 'string' && head.mask.includes('x');
|
|
695
768
|
return byName || byFormat || byType || byMask;
|
|
696
769
|
}
|
|
697
770
|
function isPasswordAttr(name, attr) {
|
|
@@ -726,7 +799,8 @@ function normalizeIdsArray(raw) {
|
|
|
726
799
|
if (!raw)
|
|
727
800
|
return [];
|
|
728
801
|
if (Array.isArray(raw)) {
|
|
729
|
-
return raw
|
|
802
|
+
return raw
|
|
803
|
+
.map((v) => {
|
|
730
804
|
if (typeof v === 'string')
|
|
731
805
|
return v;
|
|
732
806
|
if (v && typeof v === 'object') {
|
|
@@ -737,7 +811,8 @@ function normalizeIdsArray(raw) {
|
|
|
737
811
|
null);
|
|
738
812
|
}
|
|
739
813
|
return null;
|
|
740
|
-
})
|
|
814
|
+
})
|
|
815
|
+
.filter((v) => !!v);
|
|
741
816
|
}
|
|
742
817
|
return [];
|
|
743
818
|
}
|
|
@@ -774,7 +849,7 @@ function numberOrEmpty(v) {
|
|
|
774
849
|
}
|
|
775
850
|
function isAddressAttr(name, attr) {
|
|
776
851
|
const fname = String(name || '').toLowerCase();
|
|
777
|
-
const byName = /
|
|
852
|
+
const byName = /address/i.test(fname);
|
|
778
853
|
const byFormat = String(attr?.format || '').toLowerCase() === 'address';
|
|
779
854
|
const byPopulate = typeof attr?.populate === 'string' &&
|
|
780
855
|
attr.populate.length > 0;
|
|
@@ -802,7 +877,7 @@ async function findUniqueSlug(model, baseSlug, currentId) {
|
|
|
802
877
|
const checkSlug = attempt === 0 ? baseSlug : `${baseSlug}-${attempt}`;
|
|
803
878
|
try {
|
|
804
879
|
// Check if slug exists in the model
|
|
805
|
-
const res = await api.list(model, 0, 1, { slug: checkSlug });
|
|
880
|
+
const res = await api.list(model, { page: 0, limit: 1, where: { slug: checkSlug } });
|
|
806
881
|
const existing = res.data?.[0];
|
|
807
882
|
if (!existing ||
|
|
808
883
|
(currentId && String(existing.id || existing._id) === String(currentId))) {
|
|
@@ -5,6 +5,8 @@ import { useDispatch, useSelector } from 'react-redux';
|
|
|
5
5
|
import { useRouter } from 'next/navigation';
|
|
6
6
|
import { clearSession } from '../state/sessionSlice';
|
|
7
7
|
import { Button } from '@heroui/react';
|
|
8
|
+
import { AuthClient } from '../lib/AuthClient';
|
|
9
|
+
import { RealtimeClient } from '../lib/RealtimeClient';
|
|
8
10
|
import Link from 'next/link';
|
|
9
11
|
export function Sidebar() {
|
|
10
12
|
const router = useRouter();
|
|
@@ -13,11 +15,8 @@ export function Sidebar() {
|
|
|
13
15
|
const systemStatus = useSelector((s) => s.nextMin.status);
|
|
14
16
|
const { items, status, error } = useSelector((s) => s.schemas);
|
|
15
17
|
const logout = React.useCallback(() => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
localStorage.removeItem('nextmin.user');
|
|
19
|
-
}
|
|
20
|
-
catch { }
|
|
18
|
+
AuthClient.clearSession();
|
|
19
|
+
RealtimeClient.getInstance().disconnect();
|
|
21
20
|
dispatch(clearSession());
|
|
22
21
|
router.replace('/admin/auth/sign-in');
|
|
23
22
|
}, [dispatch, router]);
|
|
@@ -59,14 +58,8 @@ export function Sidebar() {
|
|
|
59
58
|
// Read user info from localStorage (client only)
|
|
60
59
|
const [user, setUser] = React.useState(null);
|
|
61
60
|
React.useEffect(() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (raw)
|
|
65
|
-
setUser(JSON.parse(raw));
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
setUser(null);
|
|
69
|
-
}
|
|
61
|
+
const u = AuthClient.getUser();
|
|
62
|
+
setUser(u);
|
|
70
63
|
}, []);
|
|
71
64
|
// —— Brand (logo/name) ——
|
|
72
65
|
const siteName = React.useMemo(() => (system?.siteName && system.siteName.trim()) || 'NextMin', [system]);
|
|
@@ -51,6 +51,8 @@ export function TableFilters({ model, value, onChange, busy, columns, visibleCol
|
|
|
51
51
|
if (keys === 'all')
|
|
52
52
|
return;
|
|
53
53
|
onVisibleColumnsChange(keys);
|
|
54
|
+
}, classNames: {
|
|
55
|
+
list: 'max-h-[300px] overflow-y-auto',
|
|
54
56
|
}, children: columns.map((col) => (_jsx(DropdownItem, { className: "capitalize", children: formatLabel(col) }, col))) })] }) })] }));
|
|
55
57
|
}
|
|
56
58
|
/** tiny inline chevron (no icon pkg) */
|
|
@@ -4,6 +4,7 @@ import { Color } from '@tiptap/extension-color';
|
|
|
4
4
|
import { TextStyle } from '@tiptap/extension-text-style';
|
|
5
5
|
import Highlight from '@tiptap/extension-highlight';
|
|
6
6
|
import { useEditor, EditorContent } from '@tiptap/react';
|
|
7
|
+
import { FontFamily } from '@tiptap/extension-font-family';
|
|
7
8
|
import StarterKit from '@tiptap/starter-kit';
|
|
8
9
|
import Link from '@tiptap/extension-link';
|
|
9
10
|
import { api } from '../../lib/api';
|
|
@@ -44,13 +45,14 @@ export const TiptapEditor = ({ value, onChange, className, placeholder = 'Start
|
|
|
44
45
|
const extensions = React.useMemo(() => [
|
|
45
46
|
StarterKit.configure({
|
|
46
47
|
heading: {
|
|
47
|
-
levels: [1, 2, 3]
|
|
48
|
+
levels: [1, 2, 3, 4, 5, 6]
|
|
48
49
|
}
|
|
49
50
|
}),
|
|
50
51
|
Highlight.configure({
|
|
51
52
|
multicolor: true,
|
|
52
53
|
}),
|
|
53
54
|
TextStyle,
|
|
55
|
+
FontFamily,
|
|
54
56
|
Color,
|
|
55
57
|
Link.configure({
|
|
56
58
|
openOnClick: false,
|
|
@@ -128,7 +130,7 @@ export const TiptapEditor = ({ value, onChange, className, placeholder = 'Start
|
|
|
128
130
|
}
|
|
129
131
|
else {
|
|
130
132
|
// Fallback to latest 6
|
|
131
|
-
const res = await api.list(selectedSchema.modelName, 0, 6);
|
|
133
|
+
const res = await api.list(selectedSchema.modelName, { page: 0, limit: 6 });
|
|
132
134
|
const payload = res?.data ?? res;
|
|
133
135
|
items =
|
|
134
136
|
payload?.items ??
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useRef } from 'react';
|
|
3
|
-
import { Bold, Italic, Underline as UnderlineIcon, List, ListOrdered, Quote, Code, Undo, Redo, Image as ImageIcon, LayoutTemplate, LayoutPanelLeft, AlignLeft, AlignCenter, AlignRight, Square, Table as TableIcon, Palette, PaintBucket, Type, MapPin } from 'lucide-react';
|
|
3
|
+
import { Bold, Italic, Underline as UnderlineIcon, List, ListOrdered, Quote, Code, Undo, Redo, Image as ImageIcon, LayoutTemplate, LayoutPanelLeft, AlignLeft, AlignCenter, AlignRight, Square, Table as TableIcon, Palette, PaintBucket, Type, MapPin, AlignJustify, Link as LinkIcon } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
5
|
import { uploadFile } from '../../lib/upload';
|
|
6
6
|
export const Toolbar = ({ editor, onDistrictGridClick }) => {
|
|
@@ -32,7 +32,26 @@ export const Toolbar = ({ editor, onDistrictGridClick }) => {
|
|
|
32
32
|
fileInputRef.current?.click();
|
|
33
33
|
};
|
|
34
34
|
const ToolbarButton = ({ onClick, isActive = false, children, disabled = false }) => (_jsx("button", { onClick: onClick, disabled: disabled, className: cn("p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors", isActive && "bg-gray-200 dark:bg-gray-700 text-blue-500", disabled && "opacity-50 cursor-not-allowed"), type: "button", children: children }));
|
|
35
|
-
return (_jsxs("div", { className: "border-b border-gray-200 dark:border-gray-800 p-2 flex flex-wrap gap-1 sticky top-0 bg-white dark:bg-zinc-950 z-10 items-center", children: [_jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleBold().run(), isActive: editor.isActive('bold'), children: _jsx(Bold, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleItalic().run(), isActive: editor.isActive('italic'), children: _jsx(Italic, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleUnderline().run(), isActive: editor.isActive('underline'), children: _jsx(UnderlineIcon, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(
|
|
35
|
+
return (_jsxs("div", { className: "border-b border-gray-200 dark:border-gray-800 p-2 flex flex-wrap gap-1 sticky top-0 bg-white dark:bg-zinc-950 z-10 items-center", children: [_jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleBold().run(), isActive: editor.isActive('bold'), children: _jsx(Bold, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleItalic().run(), isActive: editor.isActive('italic'), children: _jsx(Italic, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleUnderline().run(), isActive: editor.isActive('underline'), children: _jsx(UnderlineIcon, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx("div", { className: "flex items-center px-2 py-1 bg-gray-50 dark:bg-zinc-900 rounded-md border border-gray-200 dark:border-gray-800", children: _jsxs("select", { className: "bg-transparent text-xs outline-none cursor-pointer pr-1", onChange: (e) => {
|
|
36
|
+
const font = e.target.value;
|
|
37
|
+
if (font === 'default') {
|
|
38
|
+
editor.chain().focus().unsetFontFamily().run();
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
editor.chain().focus().setFontFamily(font).run();
|
|
42
|
+
}
|
|
43
|
+
}, value: editor.getAttributes('textStyle').fontFamily || 'default', children: [_jsx("option", { value: "default", children: "Default Font" }), _jsxs("optgroup", { label: "Bangla Fonts", children: [_jsx("option", { value: "'Hind Siliguri', sans-serif", children: "Hind Siliguri" }), _jsx("option", { value: "'Kalpurush', sans-serif", children: "Kalpurush" }), _jsx("option", { value: "'Siyam Rupali', sans-serif", children: "Siyam Rupali" }), _jsx("option", { value: "'SolaimanLipi', sans-serif", children: "SolaimanLipi" }), _jsx("option", { value: "'Akaash', sans-serif", children: "Akaash" }), _jsx("option", { value: "'Mukti Narrow', sans-serif", children: "Mukti Narrow" })] }), _jsxs("optgroup", { label: "Standard", children: [_jsx("option", { value: "Inter, sans-serif", children: "Inter" }), _jsx("option", { value: "Roboto, sans-serif", children: "Roboto" }), _jsx("option", { value: "serif", children: "Serif" }), _jsx("option", { value: "monospace", children: "Monospace" })] })] }) }), _jsx(ToolbarButton, { onClick: () => {
|
|
44
|
+
const previousUrl = editor.getAttributes('link').href;
|
|
45
|
+
const url = window.prompt('URL', previousUrl);
|
|
46
|
+
if (url === null) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (url === '') {
|
|
50
|
+
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
|
54
|
+
}, isActive: editor.isActive('link'), children: _jsx(LinkIcon, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().setTextAlign('left').run(), isActive: editor.isActive({ textAlign: 'left' }), children: _jsx(AlignLeft, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().setTextAlign('center').run(), isActive: editor.isActive({ textAlign: 'center' }), children: _jsx(AlignCenter, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().setTextAlign('right').run(), isActive: editor.isActive({ textAlign: 'right' }), children: _jsx(AlignRight, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().setTextAlign('justify').run(), isActive: editor.isActive({ textAlign: 'justify' }), children: _jsx(AlignJustify, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), isActive: editor.isActive('heading', { level: 1 }), children: _jsx("span", { className: "font-bold text-sm", children: "H1" }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), isActive: editor.isActive('heading', { level: 2 }), children: _jsx("span", { className: "font-bold text-sm", children: "H2" }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), isActive: editor.isActive('heading', { level: 3 }), children: _jsx("span", { className: "font-bold text-sm", children: "H3" }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 4 }).run(), isActive: editor.isActive('heading', { level: 4 }), children: _jsx("span", { className: "font-bold text-sm", children: "H4" }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 5 }).run(), isActive: editor.isActive('heading', { level: 5 }), children: _jsx("span", { className: "font-bold text-sm", children: "H5" }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 6 }).run(), isActive: editor.isActive('heading', { level: 6 }), children: _jsx("span", { className: "font-bold text-sm", children: "H6" }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleBulletList().run(), isActive: editor.isActive('bulletList'), children: _jsx(List, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleOrderedList().run(), isActive: editor.isActive('orderedList'), children: _jsx(ListOrdered, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleBlockquote().run(), isActive: editor.isActive('blockquote'), children: _jsx(Quote, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleCodeBlock().run(), isActive: editor.isActive('codeBlock'), children: _jsx(Code, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => {
|
|
36
55
|
editor
|
|
37
56
|
.chain()
|
|
38
57
|
.focus()
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, } from '@heroui/react';
|
|
5
|
-
import {
|
|
6
|
-
import { RefSingleSelect } from '../../RefSingleSelect';
|
|
5
|
+
import { RefSelect } from '../../RefSelect';
|
|
7
6
|
import { cn } from '../../../lib/utils';
|
|
8
7
|
import { api } from '../../../lib/api';
|
|
9
8
|
export const DistrictGridModal = ({ isOpen, onClose, onInsert, currentSpeciality, }) => {
|
|
@@ -68,5 +67,5 @@ export const DistrictGridModal = ({ isOpen, onClose, onInsert, currentSpeciality
|
|
|
68
67
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
69
68
|
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900"), children: "Doctors" }), _jsx("button", { onClick: () => setBaseType('hospitals'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", baseType === 'hospitals'
|
|
70
69
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
71
|
-
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900"), children: "Hospitals" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(
|
|
70
|
+
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900"), children: "Hospitals" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(RefSelect, { name: "districts", label: "Search & Select Districts", refModel: "Districts", value: selectedDistricts, onChange: (ids) => setSelectedDistricts(ids), multiple: true, pageSize: 1000 }), _jsx("p", { className: "text-xs text-gray-400", children: "Select one or more districts to create grid items." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(RefSelect, { name: "speciality", label: "Search & Select Speciality", refModel: "Specialities", showKey: "name", value: selectedSpecialityId, onChange: (id) => setSelectedSpecialityId(id || ''), multiple: false, pageSize: 10000 }), _jsx("p", { className: "text-xs text-gray-400", children: "Leave empty to create URLs without speciality." }), specialitySlug && (_jsx("div", { className: "mt-1 px-2 py-1 bg-primary-50 dark:bg-primary-900/20 rounded border border-primary-200 dark:border-primary-800", children: _jsxs("p", { className: "text-xs text-primary-700 dark:text-primary-400", children: ["Slug: ", _jsx("code", { className: "font-mono", children: specialitySlug })] }) }))] }), selectedDistricts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "URL Preview" }), _jsxs("div", { className: "px-3 py-2 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 max-h-32 overflow-y-auto", children: [_jsx("p", { className: "text-xs text-gray-500 mb-2", children: "Example URL pattern (actual district slugs will be used):" }), _jsx("code", { className: "text-xs text-gray-700 dark:text-gray-300", children: getPreviewUrl('[district-slug]') })] })] }))] }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "flat", onPress: onClose, children: "Cancel" }), _jsx(Button, { color: "primary", onPress: handleInsert, isDisabled: selectedDistricts.length === 0, children: "Insert Grid" })] })] }) }) }));
|
|
72
71
|
};
|
|
@@ -5,7 +5,7 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button,
|
|
|
5
5
|
// RadioGroup, // Removed
|
|
6
6
|
// Radio, // Removed
|
|
7
7
|
Input, } from '@heroui/react';
|
|
8
|
-
import {
|
|
8
|
+
import { RefSelect } from '../../RefSelect';
|
|
9
9
|
import { cn } from '../../../lib/utils'; // Assuming cn utility is available
|
|
10
10
|
// Helper to get nested keys with recursion limit
|
|
11
11
|
const getAllKeys = (attributes, allSchemas, prefix = '', depth = 0) => {
|
|
@@ -83,7 +83,7 @@ export const SchemaInsertionModal = ({ isOpen, onClose, onInsert, schema, availa
|
|
|
83
83
|
base: "bg-white dark:bg-zinc-950 border border-gray-200 dark:border-gray-800",
|
|
84
84
|
header: "border-b border-gray-200 dark:border-gray-800",
|
|
85
85
|
footer: "border-t border-gray-200 dark:border-gray-800",
|
|
86
|
-
}, children: _jsx(ModalContent, { children: _jsxs(_Fragment, { children: [_jsxs(ModalHeader, { children: ["Insert ", schema.modelName] }), _jsxs(ModalBody, { className: "py-6 flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(
|
|
86
|
+
}, children: _jsx(ModalContent, { children: _jsxs(_Fragment, { children: [_jsxs(ModalHeader, { children: ["Insert ", schema.modelName] }), _jsxs(ModalBody, { className: "py-6 flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(RefSelect, { name: "items", label: "Search & Select", refModel: schema.modelName, value: selectedIds, onChange: (ids) => setSelectedIds(ids), multiple: true, pageSize: 10000 }), _jsx("p", { className: "text-xs text-gray-400", children: "Leave empty to fetch latest items automatically." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "View Type" }), _jsxs("div", { className: "flex gap-4", children: [_jsx("button", { onClick: () => setViewType('grid'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", viewType === 'grid'
|
|
87
87
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
88
88
|
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50"), children: "Grid (Pills)" }), _jsx("button", { onClick: () => setViewType('list'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", viewType === 'list'
|
|
89
89
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|