@airoom/nextmin-react 1.4.1 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/SchemaForm.js +5 -3
- package/dist/components/editor/TiptapEditor.d.ts +2 -1
- package/dist/components/editor/TiptapEditor.js +115 -10
- package/dist/components/editor/Toolbar.d.ts +2 -1
- package/dist/components/editor/Toolbar.js +3 -3
- package/dist/components/editor/components/DistrictGridModal.d.ts +13 -0
- package/dist/components/editor/components/DistrictGridModal.js +72 -0
- package/dist/components/editor/components/ImageBubbleMenu.js +1 -7
- package/dist/components/editor/components/SchemaInsertionModal.d.ts +2 -1
- package/dist/components/editor/components/SchemaInsertionModal.js +58 -5
- package/dist/nextmin.css +1 -1
- package/package.json +1 -1
|
@@ -410,10 +410,10 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
410
410
|
const dynamicColClass = isRich ? 'col-span-2' : colClass;
|
|
411
411
|
return (_jsx("div", { className: dynamicColClass, children: _jsx(SchemaField, { uid: formUid, name: name, attr: attr, inputClassNames: inputClassNames, selectClassNames: selectClassNames, value: baseValue, onChange: handleChange, disabled: busy,
|
|
412
412
|
// pass schemas for slash cmd
|
|
413
|
-
availableSchemas: items, mapsKey: mapsKey }) }, name));
|
|
413
|
+
availableSchemas: items, mapsKey: mapsKey, formState: form }) }, name));
|
|
414
414
|
}), 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" }))] })] }));
|
|
415
415
|
}
|
|
416
|
-
function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, mapsKey, availableSchemas, }) {
|
|
416
|
+
function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, mapsKey, availableSchemas, formState, }) {
|
|
417
417
|
const id = `${uid}-${name}`;
|
|
418
418
|
const label = attr?.label ?? formatLabel(name);
|
|
419
419
|
const required = !!attr?.required;
|
|
@@ -427,7 +427,9 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
|
|
|
427
427
|
attr?.writeOnly === true;
|
|
428
428
|
// RICH TEXT EDITOR CHECK
|
|
429
429
|
if (attr?.rich === true) {
|
|
430
|
-
|
|
430
|
+
// Extract speciality slug from form state
|
|
431
|
+
const specialitySlug = formState?.speciality?.slug || formState?.slug || '';
|
|
432
|
+
return (_jsx(TiptapEditor, { value: value, onChange: (html) => onChange(name, html), placeholder: label, availableSchemas: availableSchemas, currentSpeciality: specialitySlug }));
|
|
431
433
|
}
|
|
432
434
|
const isPhoneField = isPhoneAttr(name, attr);
|
|
433
435
|
const rawMask = typeof attr?.mask === 'string' ? attr.mask : '';
|
|
@@ -5,6 +5,7 @@ interface TiptapEditorProps {
|
|
|
5
5
|
className?: string;
|
|
6
6
|
placeholder?: string;
|
|
7
7
|
availableSchemas?: SchemaDef[];
|
|
8
|
+
currentSpeciality?: string;
|
|
8
9
|
}
|
|
9
|
-
export declare const TiptapEditor: ({ value, onChange, className, placeholder, availableSchemas }: TiptapEditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare const TiptapEditor: ({ value, onChange, className, placeholder, availableSchemas, currentSpeciality }: TiptapEditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
11
|
export default TiptapEditor;
|
|
@@ -25,14 +25,17 @@ import { cn } from '../../lib/utils';
|
|
|
25
25
|
import { SlashCommand, getSuggestionOptions } from './extensions/SlashCommand';
|
|
26
26
|
// ... imports
|
|
27
27
|
import { SchemaInsertionModal } from './components/SchemaInsertionModal';
|
|
28
|
+
import { DistrictGridModal } from './components/DistrictGridModal';
|
|
28
29
|
import { ImageBubbleMenu } from './components/ImageBubbleMenu';
|
|
29
30
|
import { TableBubbleMenu } from './components/TableBubbleMenu';
|
|
30
31
|
import { Container } from './extensions/Container';
|
|
31
32
|
// ... interface
|
|
32
|
-
export const TiptapEditor = ({ value, onChange, className, placeholder = 'Start writing...', availableSchemas = [] }) => {
|
|
33
|
-
// State for modal
|
|
33
|
+
export const TiptapEditor = ({ value, onChange, className, placeholder = 'Start writing...', availableSchemas = [], currentSpeciality }) => {
|
|
34
|
+
// State for schema insertion modal
|
|
34
35
|
const [selectedSchema, setSelectedSchema] = useState(null);
|
|
35
36
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
37
|
+
// State for district grid modal
|
|
38
|
+
const [isDistrictGridModalOpen, setIsDistrictGridModalOpen] = useState(false);
|
|
36
39
|
// Filter restricted schemas
|
|
37
40
|
const filteredSchemas = React.useMemo(() => {
|
|
38
41
|
const restricted = ['Roles', 'Settings', 'Users'];
|
|
@@ -137,22 +140,83 @@ export const TiptapEditor = ({ value, onChange, className, placeholder = 'Start
|
|
|
137
140
|
}
|
|
138
141
|
const displayItems = items;
|
|
139
142
|
const baseFrontendUrl = process.env.NEXT_PUBLIC_FRONTEND_URL || '';
|
|
143
|
+
// Helper to safe access nested properties with Array support (Map & Lookup)
|
|
144
|
+
const getGenericNestedValue = (obj, path) => {
|
|
145
|
+
if (!obj)
|
|
146
|
+
return '';
|
|
147
|
+
const parts = path.split('.');
|
|
148
|
+
let current = obj;
|
|
149
|
+
for (let i = 0; i < parts.length; i++) {
|
|
150
|
+
const part = parts[i];
|
|
151
|
+
if (current === undefined || current === null)
|
|
152
|
+
return '';
|
|
153
|
+
if (Array.isArray(current)) {
|
|
154
|
+
// Case 1: Is 'part' a property of the items? (Map)
|
|
155
|
+
// Heuristic: check first item
|
|
156
|
+
if (current.length > 0 && current[0] && typeof current[0] === 'object' && part in current[0]) {
|
|
157
|
+
// Map
|
|
158
|
+
current = current.map((item) => item[part]);
|
|
159
|
+
// If this was the last part, we are potentially done (will be joined at end if it's array of primitives)
|
|
160
|
+
// If there are more parts, we have an array of values.
|
|
161
|
+
// Deep mapping? e.g. tags.author.name -> tags map(author) -> map(name).
|
|
162
|
+
// For simplicity, if we mapped and there are more parts, we recursively map?
|
|
163
|
+
// This gets complex. Let's handle simple map for now.
|
|
164
|
+
// If there are more parts, we'd need to map current array again.
|
|
165
|
+
// Let's defer mapping logic:
|
|
166
|
+
// Actually 'current' becomes array. Next loop iteration will hit Array check again.
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Case 2: Is 'part' a Lookup Key? (ID/Slug)
|
|
170
|
+
const found = current.find((item) => item.id === part || item._id === part || item.slug === part || item.code === part);
|
|
171
|
+
if (found) {
|
|
172
|
+
current = found;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// No match found, maybe it's just not there.
|
|
176
|
+
return '';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (typeof current === 'object') {
|
|
181
|
+
// Standard object access
|
|
182
|
+
current = current[part];
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Primitive cannot have property
|
|
186
|
+
return '';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Final Value processing
|
|
190
|
+
if (Array.isArray(current)) {
|
|
191
|
+
// Filter nulls/undefined and join
|
|
192
|
+
return current.filter((c) => c !== undefined && c !== null).join(', ');
|
|
193
|
+
}
|
|
194
|
+
return current !== undefined && current !== null ? String(current) : '';
|
|
195
|
+
};
|
|
140
196
|
// Helper to interpolate string with item data
|
|
141
197
|
const processTemplate = (tmpl, item) => {
|
|
142
198
|
let output = tmpl || selectedSchema.modelName;
|
|
143
|
-
output = output.replace(/{(\w+)}/g, (match, key) => {
|
|
144
|
-
|
|
199
|
+
output = output.replace(/{([\w\.]+)}/g, (match, key) => {
|
|
200
|
+
const val = getGenericNestedValue(item, key);
|
|
201
|
+
return val !== '' ? val : match;
|
|
145
202
|
});
|
|
146
203
|
return output;
|
|
147
204
|
};
|
|
148
205
|
// Helper for URL
|
|
149
206
|
const processUrl = (pattern, item) => {
|
|
150
207
|
const suffix = processTemplate(pattern || '{slug}_{id}', item);
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
208
|
+
// Clean output (remove unresolved braces if any?)
|
|
209
|
+
// Actually processTemplate leaves match if returns empty, but we changed logic to return match if val is empty in previous version?
|
|
210
|
+
// New logic: returns '' if empty. So `{foo}` becomes ``.
|
|
211
|
+
// Wait, replace expects string.
|
|
212
|
+
// If val is '', we likely want to specific behavior.
|
|
213
|
+
// If the key doesn't exist at all, usually simple templates leave it or show empty.
|
|
214
|
+
// User's previous behavior: `return val !== undefined ... ? String(val) : match;`
|
|
215
|
+
// Let's stick to: if resolved value is empty string, we replace with empty string? Or keep placeholder?
|
|
216
|
+
// Actually my implementation returned `match` if `val===''`? No, `val !== '' ? val : match`.
|
|
217
|
+
// If `getGenericNestedValue` returns '', then we return match.
|
|
218
|
+
// If we want to clear it, we should return ''.
|
|
219
|
+
// Use case: `{districts.dhaka.name}`. If not found, showing `{...}` hints error.
|
|
156
220
|
const cleanBase = baseFrontendUrl.replace(/\/$/, '');
|
|
157
221
|
const cleanSuffix = suffix.replace(/^\//, '');
|
|
158
222
|
return `${cleanBase}/${cleanSuffix}`;
|
|
@@ -223,6 +287,47 @@ export const TiptapEditor = ({ value, onChange, className, placeholder = 'Start
|
|
|
223
287
|
editor.chain().focus().insertContent(`<p class="text-red-500">Error loading data for ${selectedSchema.modelName}</p>`).run();
|
|
224
288
|
}
|
|
225
289
|
};
|
|
226
|
-
|
|
290
|
+
const handleDistrictGridInsert = async ({ districts, baseType, specialitySlug, specialityName }) => {
|
|
291
|
+
if (!editor || districts.length === 0)
|
|
292
|
+
return;
|
|
293
|
+
try {
|
|
294
|
+
// Fetch district data for selected IDs
|
|
295
|
+
const promises = districts.map(id => api.get('Districts', id).then((r) => r.data || r).catch(() => null));
|
|
296
|
+
const results = await Promise.all(promises);
|
|
297
|
+
const districtItems = results.filter(Boolean);
|
|
298
|
+
if (districtItems.length === 0) {
|
|
299
|
+
console.error('No districts found');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const baseFrontendUrl = process.env.NEXT_PUBLIC_FRONTEND_URL || '';
|
|
303
|
+
const cleanBase = baseFrontendUrl.replace(/\/$/, '');
|
|
304
|
+
// Build grid HTML
|
|
305
|
+
let html = `<div data-type="grid-container">`;
|
|
306
|
+
districtItems.forEach((district) => {
|
|
307
|
+
const districtSlug = district.slug || district.name?.toLowerCase().replace(/\s+/g, '-');
|
|
308
|
+
const urlParts = [cleanBase, baseType, districtSlug];
|
|
309
|
+
if (specialitySlug) {
|
|
310
|
+
urlParts.push(specialitySlug);
|
|
311
|
+
}
|
|
312
|
+
const url = urlParts.join('/');
|
|
313
|
+
// Format: "Speciality Name in District Name" or just "District Name" if no speciality
|
|
314
|
+
const displayText = specialityName
|
|
315
|
+
? `${specialityName} in ${district.name}`
|
|
316
|
+
: district.name;
|
|
317
|
+
html += `<div data-type="grid-item" href="${url}"><p>${displayText}</p></div>`;
|
|
318
|
+
});
|
|
319
|
+
html += `</div>`;
|
|
320
|
+
// Insert spacing first, then grid
|
|
321
|
+
editor.chain().focus()
|
|
322
|
+
.insertContent('<p> </p>')
|
|
323
|
+
.insertContent(html)
|
|
324
|
+
.run();
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
console.error('Failed to fetch district data', err);
|
|
328
|
+
editor.chain().focus().insertContent(`<p class="text-red-500">Error loading district data</p>`).run();
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
return (_jsxs("div", { className: "border border-gray-200 dark:border-gray-800 rounded-lg overflow-hidden bg-white dark:bg-zinc-950 shadow-sm transition-all hover:border-gray-300 dark:hover:border-gray-700", children: [_jsx(Toolbar, { editor: editor, onDistrictGridClick: () => setIsDistrictGridModalOpen(true) }), _jsx(EditorContent, { editor: editor }), _jsx(ImageBubbleMenu, { editor: editor }), _jsx(TableBubbleMenu, { editor: editor }), _jsx(SchemaInsertionModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), onInsert: handleSchemaInsert, schema: selectedSchema, availableSchemas: filteredSchemas }), _jsx(DistrictGridModal, { isOpen: isDistrictGridModalOpen, onClose: () => setIsDistrictGridModalOpen(false), onInsert: handleDistrictGridInsert, currentSpeciality: currentSpeciality })] }));
|
|
227
332
|
};
|
|
228
333
|
export default TiptapEditor;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Editor } from '@tiptap/react';
|
|
2
2
|
interface ToolbarProps {
|
|
3
3
|
editor: Editor | null;
|
|
4
|
+
onDistrictGridClick?: () => void;
|
|
4
5
|
}
|
|
5
|
-
export declare const Toolbar: ({ editor }: ToolbarProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
6
|
+
export declare const Toolbar: ({ editor, onDistrictGridClick }: ToolbarProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
6
7
|
export {};
|
|
@@ -1,9 +1,9 @@
|
|
|
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 } 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 } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
5
|
import { uploadFile } from '../../lib/upload';
|
|
6
|
-
export const Toolbar = ({ editor }) => {
|
|
6
|
+
export const Toolbar = ({ editor, onDistrictGridClick }) => {
|
|
7
7
|
const fileInputRef = useRef(null);
|
|
8
8
|
if (!editor) {
|
|
9
9
|
return null;
|
|
@@ -46,7 +46,7 @@ export const Toolbar = ({ editor }) => {
|
|
|
46
46
|
.insertContent('<p> </p>')
|
|
47
47
|
.insertContent('<div data-type="layout-row" data-cols="3"><div data-type="layout-column"><p></p></div><div data-type="layout-column"><p></p></div><div data-type="layout-column"><p></p></div></div>')
|
|
48
48
|
.run();
|
|
49
|
-
}, children: _jsx(LayoutPanelLeft, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => {
|
|
49
|
+
}, children: _jsx(LayoutPanelLeft, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), onDistrictGridClick && (_jsx(ToolbarButton, { onClick: onDistrictGridClick, children: _jsx(MapPin, { size: 18 }) })), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => {
|
|
50
50
|
editor
|
|
51
51
|
.chain()
|
|
52
52
|
.focus()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface DistrictGridModalProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
onClose: () => void;
|
|
4
|
+
onInsert: (config: {
|
|
5
|
+
districts: string[];
|
|
6
|
+
baseType: 'doctors' | 'hospitals';
|
|
7
|
+
specialitySlug?: string;
|
|
8
|
+
specialityName?: string;
|
|
9
|
+
}) => void;
|
|
10
|
+
currentSpeciality?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const DistrictGridModal: ({ isOpen, onClose, onInsert, currentSpeciality, }: DistrictGridModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, } from '@heroui/react';
|
|
5
|
+
import { RefMultiSelect } from '../../RefMultiSelect';
|
|
6
|
+
import { RefSingleSelect } from '../../RefSingleSelect';
|
|
7
|
+
import { cn } from '../../../lib/utils';
|
|
8
|
+
import { api } from '../../../lib/api';
|
|
9
|
+
export const DistrictGridModal = ({ isOpen, onClose, onInsert, currentSpeciality, }) => {
|
|
10
|
+
const [baseType, setBaseType] = useState('doctors');
|
|
11
|
+
const [selectedDistricts, setSelectedDistricts] = useState([]);
|
|
12
|
+
const [selectedSpecialityId, setSelectedSpecialityId] = useState('');
|
|
13
|
+
const [specialitySlug, setSpecialitySlug] = useState('');
|
|
14
|
+
const [specialityName, setSpecialityName] = useState('');
|
|
15
|
+
// Fetch speciality data when ID changes
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!selectedSpecialityId) {
|
|
18
|
+
setSpecialitySlug('');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const fetchSpeciality = async () => {
|
|
22
|
+
try {
|
|
23
|
+
const res = await api.get('Specialities', selectedSpecialityId);
|
|
24
|
+
const speciality = res.data || res;
|
|
25
|
+
setSpecialitySlug(speciality?.slug || '');
|
|
26
|
+
setSpecialityName(speciality?.name || '');
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error('Failed to fetch speciality:', err);
|
|
30
|
+
setSpecialitySlug('');
|
|
31
|
+
setSpecialityName('');
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
fetchSpeciality();
|
|
35
|
+
}, [selectedSpecialityId]);
|
|
36
|
+
const handleInsert = () => {
|
|
37
|
+
onInsert({
|
|
38
|
+
districts: selectedDistricts,
|
|
39
|
+
baseType,
|
|
40
|
+
specialitySlug: specialitySlug,
|
|
41
|
+
specialityName: specialityName
|
|
42
|
+
});
|
|
43
|
+
onClose();
|
|
44
|
+
// Reset state after a short delay
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
setBaseType('doctors');
|
|
47
|
+
setSelectedDistricts([]);
|
|
48
|
+
setSelectedSpecialityId('');
|
|
49
|
+
setSpecialitySlug('');
|
|
50
|
+
setSpecialityName('');
|
|
51
|
+
}, 200);
|
|
52
|
+
};
|
|
53
|
+
const baseFrontendUrl = process.env.NEXT_PUBLIC_FRONTEND_URL || '';
|
|
54
|
+
const cleanBase = baseFrontendUrl.replace(/\/$/, '');
|
|
55
|
+
// Generate preview URL
|
|
56
|
+
const getPreviewUrl = (districtSlug) => {
|
|
57
|
+
const parts = [cleanBase, baseType, districtSlug];
|
|
58
|
+
if (specialitySlug) {
|
|
59
|
+
parts.push(specialitySlug);
|
|
60
|
+
}
|
|
61
|
+
return parts.join('/');
|
|
62
|
+
};
|
|
63
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, size: "2xl", classNames: {
|
|
64
|
+
base: "bg-white dark:bg-zinc-950 border border-gray-200 dark:border-gray-800",
|
|
65
|
+
header: "border-b border-gray-200 dark:border-gray-800",
|
|
66
|
+
footer: "border-t border-gray-200 dark:border-gray-800",
|
|
67
|
+
}, children: _jsx(ModalContent, { children: _jsxs(_Fragment, { children: [_jsx(ModalHeader, { children: "Insert District Grid" }), _jsxs(ModalBody, { className: "py-6 flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Base Type" }), _jsxs("div", { className: "flex gap-4", children: [_jsx("button", { onClick: () => setBaseType('doctors'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", baseType === 'doctors'
|
|
68
|
+
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
69
|
+
: "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
|
+
? "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("label", { className: "text-sm font-medium", children: "Select Districts" }), _jsx(RefMultiSelect, { name: "districts", label: "Search & Select Districts", refModel: "Districts", value: selectedDistricts, onChange: (ids) => setSelectedDistricts(ids), pageSize: 64 }), _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("label", { className: "text-sm font-medium", children: "Select Speciality (Optional)" }), _jsx(RefSingleSelect, { name: "speciality", label: "Search & Select Speciality", refModel: "Specialities", showKey: "name", value: selectedSpecialityId, onChange: (id) => setSelectedSpecialityId(id || ''), pageSize: 50 }), _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
|
+
};
|
|
@@ -5,11 +5,5 @@ import { cn } from '../../../lib/utils';
|
|
|
5
5
|
export const ImageBubbleMenu = ({ editor }) => {
|
|
6
6
|
if (!editor)
|
|
7
7
|
return null;
|
|
8
|
-
return (_jsxs(BubbleMenu, { editor: editor, pluginKey: "imageBubbleMenu", shouldShow: ({ editor }) => editor.isActive('image'),
|
|
9
|
-
// @ts-ignore
|
|
10
|
-
tippyOptions: {
|
|
11
|
-
duration: 100,
|
|
12
|
-
zIndex: 99, // Ensure it sits above other elements
|
|
13
|
-
maxWidth: 'none',
|
|
14
|
-
}, className: "flex items-center gap-1 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700 shadow-lg p-1 rounded-lg z-50", children: [_jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { width: '100%', height: null }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.getAttributes('image').width === '100%' && "bg-gray-200 dark:bg-gray-600"), title: "Full Width", children: _jsx(StretchHorizontal, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { width: null, height: null }).run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", title: "Original Size", children: _jsx(Minimize, { size: 16 }) }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 dark:bg-gray-600 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('left').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'left' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Left", children: _jsx(AlignLeft, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('center').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'center' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Center", children: _jsx(AlignCenter, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('right').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'right' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Right", children: _jsx(AlignRight, { size: 16 }) }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 dark:bg-gray-600 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'contain' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'contain' && "bg-gray-200 dark:bg-gray-600"), title: "Contain", children: "Fit" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'cover' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'cover' && "bg-gray-200 dark:bg-gray-600"), title: "Cover", children: "Cover" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'fill' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'fill' && "bg-gray-200 dark:bg-gray-600"), title: "Fill", children: "Fill" })] }));
|
|
8
|
+
return (_jsxs(BubbleMenu, { editor: editor, pluginKey: "imageBubbleMenu", shouldShow: ({ editor }) => editor.isActive('image'), className: "flex items-center gap-1 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700 shadow-lg p-1 rounded-lg z-50", children: [_jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { width: '100%', height: null }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.getAttributes('image').width === '100%' && "bg-gray-200 dark:bg-gray-600"), title: "Full Width", children: _jsx(StretchHorizontal, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { width: null, height: null }).run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", title: "Original Size", children: _jsx(Minimize, { size: 16 }) }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 dark:bg-gray-600 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('left').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'left' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Left", children: _jsx(AlignLeft, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('center').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'center' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Center", children: _jsx(AlignCenter, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('right').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'right' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Right", children: _jsx(AlignRight, { size: 16 }) }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 dark:bg-gray-600 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'contain' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'contain' && "bg-gray-200 dark:bg-gray-600"), title: "Contain", children: "Fit" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'cover' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'cover' && "bg-gray-200 dark:bg-gray-600"), title: "Cover", children: "Cover" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'fill' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'fill' && "bg-gray-200 dark:bg-gray-600"), title: "Fill", children: "Fill" })] }));
|
|
15
9
|
};
|
|
@@ -9,6 +9,7 @@ interface SchemaInsertionModalProps {
|
|
|
9
9
|
urlPattern: string;
|
|
10
10
|
}) => void;
|
|
11
11
|
schema: SchemaDef | null;
|
|
12
|
+
availableSchemas: SchemaDef[];
|
|
12
13
|
}
|
|
13
|
-
export declare const SchemaInsertionModal: ({ isOpen, onClose, onInsert, schema, }: SchemaInsertionModalProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
14
|
+
export declare const SchemaInsertionModal: ({ isOpen, onClose, onInsert, schema, availableSchemas, }: SchemaInsertionModalProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
14
15
|
export {};
|
|
@@ -1,22 +1,64 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
4
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button,
|
|
5
5
|
// RadioGroup, // Removed
|
|
6
6
|
// Radio, // Removed
|
|
7
7
|
Input, } from '@heroui/react';
|
|
8
8
|
import { RefMultiSelect } from '../../RefMultiSelect';
|
|
9
9
|
import { cn } from '../../../lib/utils'; // Assuming cn utility is available
|
|
10
|
-
|
|
10
|
+
// Helper to get nested keys with recursion limit
|
|
11
|
+
const getAllKeys = (attributes, allSchemas, prefix = '', depth = 0) => {
|
|
12
|
+
if (depth > 2)
|
|
13
|
+
return []; // Prevent infinite recursion
|
|
14
|
+
let keys = [];
|
|
15
|
+
Object.keys(attributes).forEach(key => {
|
|
16
|
+
const attr = attributes[key];
|
|
17
|
+
const currentKey = prefix ? `${prefix}.${key}` : key;
|
|
18
|
+
keys.push(currentKey);
|
|
19
|
+
// Check for reference
|
|
20
|
+
let refModelName;
|
|
21
|
+
if (attr.ref) {
|
|
22
|
+
refModelName = attr.ref;
|
|
23
|
+
}
|
|
24
|
+
else if (Array.isArray(attr) && attr[0]?.ref) {
|
|
25
|
+
refModelName = attr[0].ref;
|
|
26
|
+
}
|
|
27
|
+
if (refModelName) {
|
|
28
|
+
const refSchema = allSchemas.find(s => s.modelName === refModelName);
|
|
29
|
+
if (refSchema) {
|
|
30
|
+
const subKeys = getAllKeys(refSchema.attributes, allSchemas, currentKey, depth + 1);
|
|
31
|
+
keys = [...keys, ...subKeys];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (typeof attr === 'object' && !attr.type && !Array.isArray(attr)) {
|
|
35
|
+
// Plain nested object (not an attribute definition itself, but a grouping)
|
|
36
|
+
// Check if it has type property? SchemaDef attributes are Record<string, Attribute>.
|
|
37
|
+
// Attribute can be AttributeBase or AttributeBase[].
|
|
38
|
+
// If it doesn't have 'type', it might be a nested object structure if supported.
|
|
39
|
+
// But based on types.ts, attributes is Record<string, Attribute>.
|
|
40
|
+
// So we strictly look for ref in AttributeBase.
|
|
41
|
+
// If keys are just grouping (e.g. meta: { title: ... }), then we recurse.
|
|
42
|
+
// But Mongoose schema defs usually have type.
|
|
43
|
+
// Let's assume structure is flat or standard Mongoose.
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Add common virtuals at appropriate levels if needed, or just top level
|
|
47
|
+
if (prefix === '') {
|
|
48
|
+
keys.push('id');
|
|
49
|
+
keys.push('createdAt');
|
|
50
|
+
keys.push('updatedAt');
|
|
51
|
+
}
|
|
52
|
+
return keys;
|
|
53
|
+
};
|
|
54
|
+
export const SchemaInsertionModal = ({ isOpen, onClose, onInsert, schema, availableSchemas, }) => {
|
|
11
55
|
const [viewType, setViewType] = useState('grid');
|
|
12
56
|
const [template, setTemplate] = useState('');
|
|
13
57
|
const [urlPattern, setUrlPattern] = useState('{slug}_{id}');
|
|
14
58
|
const [selectedIds, setSelectedIds] = useState([]);
|
|
15
|
-
// const [limit, setLimit] = useState('6'); // Removed limit
|
|
16
59
|
const handleInsert = () => {
|
|
17
60
|
onInsert({ viewType, template, ids: selectedIds, urlPattern });
|
|
18
61
|
onClose();
|
|
19
|
-
// Reset state
|
|
20
62
|
setTimeout(() => {
|
|
21
63
|
setViewType('grid');
|
|
22
64
|
setTemplate('');
|
|
@@ -24,6 +66,17 @@ export const SchemaInsertionModal = ({ isOpen, onClose, onInsert, schema, }) =>
|
|
|
24
66
|
setSelectedIds([]);
|
|
25
67
|
}, 200);
|
|
26
68
|
};
|
|
69
|
+
const availableKeys = React.useMemo(() => {
|
|
70
|
+
if (!schema)
|
|
71
|
+
return [];
|
|
72
|
+
return getAllKeys(schema.attributes, availableSchemas);
|
|
73
|
+
}, [schema, availableSchemas]);
|
|
74
|
+
const addToTemplate = (key) => {
|
|
75
|
+
setTemplate(prev => `${prev} {${key}}`);
|
|
76
|
+
};
|
|
77
|
+
const addToUrlPattern = (key) => {
|
|
78
|
+
setUrlPattern(prev => `${prev}{${key}}`);
|
|
79
|
+
};
|
|
27
80
|
if (!schema)
|
|
28
81
|
return null;
|
|
29
82
|
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, size: "2xl", classNames: {
|
|
@@ -34,5 +87,5 @@ export const SchemaInsertionModal = ({ isOpen, onClose, onInsert, schema, }) =>
|
|
|
34
87
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
35
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'
|
|
36
89
|
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
37
|
-
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50"), children: "List (Rows)" })] })] }), _jsx(Input, { label: "Template String", placeholder: "e.g. {name} - {designation}", value: template, onChange: (e) => setTemplate(e.target.value), description: "Use {fieldName} to insert dynamic data", variant: "bordered", labelPlacement: "outside" }), _jsx(Input, { label: "URL Suffix", placeholder: "{slug}_{id}", value: urlPattern, onChange: (e) => setUrlPattern(e.target.value), description: `Appended to ${process.env.NEXT_PUBLIC_FRONTEND_URL || 'BASE_URL'}/...`, variant: "bordered", labelPlacement: "outside" })] }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "flat", onPress: onClose, children: "Cancel" }), _jsx(Button, { color: "primary", onPress: handleInsert, children: "Insert" })] })] }) }) }));
|
|
90
|
+
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50"), children: "List (Rows)" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Input, { label: "Template String", placeholder: "e.g. {name} - {designation}", value: template, onChange: (e) => setTemplate(e.target.value), description: "Use {fieldName} to insert dynamic data", variant: "bordered", labelPlacement: "outside" }), _jsx("div", { className: "flex flex-wrap gap-2 mt-1", children: availableKeys.map(key => (_jsx("button", { onClick: () => addToTemplate(key), className: "px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors", children: key }, key))) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Input, { label: "URL Suffix", placeholder: "{slug}_{id}", value: urlPattern, onChange: (e) => setUrlPattern(e.target.value), description: `Appended to ${process.env.NEXT_PUBLIC_FRONTEND_URL || 'BASE_URL'}/...`, variant: "bordered", labelPlacement: "outside" }), _jsx("div", { className: "flex flex-wrap gap-2 mt-1", children: availableKeys.map(key => (_jsx("button", { onClick: () => addToUrlPattern(key), className: "px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors", children: key }, key))) })] })] }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "flat", onPress: onClose, children: "Cancel" }), _jsx(Button, { color: "primary", onPress: handleInsert, children: "Insert" })] })] }) }) }));
|
|
38
91
|
};
|