@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.
@@ -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
- return (_jsx(TiptapEditor, { value: value, onChange: (html) => onChange(name, html), placeholder: label, availableSchemas: availableSchemas }));
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
- return item[key] !== undefined && item[key] !== null ? String(item[key]) : match;
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
- // Ensure base ends with / if needed? Usually env var doesn't have it.
152
- // Or Pattern starts with /?
153
- // User said "add another input that I can configure the url parts too AFTER the base url part"
154
- // Assuming base url is domain.com/
155
- // Join them carefully.
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
- 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 }), _jsx(EditorContent, { editor: editor }), _jsx(ImageBubbleMenu, { editor: editor }), _jsx(TableBubbleMenu, { editor: editor }), _jsx(SchemaInsertionModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), onInsert: handleSchemaInsert, schema: selectedSchema })] }));
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>&nbsp;</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>&nbsp;</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
- export const SchemaInsertionModal = ({ isOpen, onClose, onInsert, schema, }) => {
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
  };