@airoom/nextmin-react 1.4.0 → 1.4.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/dist/components/SchemaForm.js +17 -2
- package/dist/components/SchemaSuggestionList.d.ts +7 -0
- package/dist/components/SchemaSuggestionList.js +43 -0
- package/dist/components/editor/TiptapEditor.d.ts +11 -0
- package/dist/components/editor/TiptapEditor.js +330 -0
- package/dist/components/editor/Toolbar.d.ts +7 -0
- package/dist/components/editor/Toolbar.js +99 -0
- package/dist/components/editor/components/CommandList.d.ts +7 -0
- package/dist/components/editor/components/CommandList.js +47 -0
- package/dist/components/editor/components/DistrictGridModal.d.ts +12 -0
- package/dist/components/editor/components/DistrictGridModal.js +67 -0
- package/dist/components/editor/components/ImageBubbleMenu.d.ts +6 -0
- package/dist/components/editor/components/ImageBubbleMenu.js +15 -0
- package/dist/components/editor/components/ImageComponent.d.ts +3 -0
- package/dist/components/editor/components/ImageComponent.js +45 -0
- package/dist/components/editor/components/SchemaInsertionModal.d.ts +15 -0
- package/dist/components/editor/components/SchemaInsertionModal.js +91 -0
- package/dist/components/editor/components/TableBubbleMenu.d.ts +6 -0
- package/dist/components/editor/components/TableBubbleMenu.js +8 -0
- package/dist/components/editor/extensions/Container.d.ts +2 -0
- package/dist/components/editor/extensions/Container.js +51 -0
- package/dist/components/editor/extensions/Grid.d.ts +3 -0
- package/dist/components/editor/extensions/Grid.js +89 -0
- package/dist/components/editor/extensions/Layout.d.ts +3 -0
- package/dist/components/editor/extensions/Layout.js +116 -0
- package/dist/components/editor/extensions/ResizableImage.d.ts +1 -0
- package/dist/components/editor/extensions/ResizableImage.js +52 -0
- package/dist/components/editor/extensions/SlashCommand.d.ts +15 -0
- package/dist/components/editor/extensions/SlashCommand.js +161 -0
- package/dist/components/editor/utils/upload.d.ts +1 -0
- package/dist/components/editor/utils/upload.js +49 -0
- package/dist/editor.css +460 -0
- package/dist/lib/upload.d.ts +1 -0
- package/dist/lib/upload.js +53 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/nextmin.css +1 -1
- package/dist/router/NextMinRouter.d.ts +1 -0
- package/dist/router/NextMinRouter.js +1 -0
- package/dist/views/list/useListData.js +4 -0
- package/package.json +34 -8
- package/tsconfig.json +8 -3
|
@@ -12,6 +12,10 @@ import { FileUploader } from './FileUploader';
|
|
|
12
12
|
import AddressAutocompleteGoogle from './AddressAutocomplete';
|
|
13
13
|
import { useGoogleMapsKey } from '../hooks/useGoogleMapsKey';
|
|
14
14
|
import { parseDate, parseDateTime, parseTime, } from '@internationalized/date';
|
|
15
|
+
// import RichTextEditor from './editor/RichTextEditor';
|
|
16
|
+
// import ShadcnEditor from './editor/ShadcnEditor';
|
|
17
|
+
// import LexicalEditor from './editor/LexicalEditor';
|
|
18
|
+
import TiptapEditor from './editor/TiptapEditor';
|
|
15
19
|
const inputClassNames = { inputWrapper: 'bg-transparent shadow-none' };
|
|
16
20
|
const selectClassNames = { trigger: 'bg-transparent shadow-none' };
|
|
17
21
|
const AUDIT_FIELDS = new Set(['createdAt', 'updatedAt']);
|
|
@@ -401,10 +405,15 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
|
|
|
401
405
|
? form[name]
|
|
402
406
|
: ''
|
|
403
407
|
: (form[name] ?? (Array.isArray(attr) ? [] : ''));
|
|
404
|
-
|
|
408
|
+
// If it's a rich text field, force full width
|
|
409
|
+
const isRich = attr?.rich === true;
|
|
410
|
+
const dynamicColClass = isRich ? 'col-span-2' : colClass;
|
|
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
|
+
// pass schemas for slash cmd
|
|
413
|
+
availableSchemas: items, mapsKey: mapsKey, formState: form }) }, name));
|
|
405
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" }))] })] }));
|
|
406
415
|
}
|
|
407
|
-
function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, mapsKey, }) {
|
|
416
|
+
function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, mapsKey, availableSchemas, formState, }) {
|
|
408
417
|
const id = `${uid}-${name}`;
|
|
409
418
|
const label = attr?.label ?? formatLabel(name);
|
|
410
419
|
const required = !!attr?.required;
|
|
@@ -416,6 +425,12 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
|
|
|
416
425
|
const isPasswordField = name.toLowerCase() === 'password' ||
|
|
417
426
|
attr?.format === 'password' ||
|
|
418
427
|
attr?.writeOnly === true;
|
|
428
|
+
// RICH TEXT EDITOR CHECK
|
|
429
|
+
if (attr?.rich === true) {
|
|
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 }));
|
|
433
|
+
}
|
|
419
434
|
const isPhoneField = isPhoneAttr(name, attr);
|
|
420
435
|
const rawMask = typeof attr?.mask === 'string' ? attr.mask : '';
|
|
421
436
|
const hasSlots = /[Xx9#_]/.test(rawMask);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
|
4
|
+
import { Card, Listbox, ListboxItem } from '@heroui/react';
|
|
5
|
+
export const SchemaSuggestionList = forwardRef((props, ref) => {
|
|
6
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
7
|
+
const selectItem = (index) => {
|
|
8
|
+
const item = props.items[index];
|
|
9
|
+
if (item) {
|
|
10
|
+
props.command(item);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setSelectedIndex(0);
|
|
15
|
+
}, [props.items]);
|
|
16
|
+
useImperativeHandle(ref, () => ({
|
|
17
|
+
onKeyDown: ({ event }) => {
|
|
18
|
+
if (event.key === 'ArrowUp') {
|
|
19
|
+
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (event.key === 'ArrowDown') {
|
|
23
|
+
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (event.key === 'Enter') {
|
|
27
|
+
selectItem(selectedIndex);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
if (!props.items?.length) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return (_jsx(Card, { className: "min-w-[200px] overflow-hidden p-1 shadow-medium border border-default-200", children: _jsx(Listbox, { variant: "flat", "aria-label": "Schema suggestions", onAction: (key) => {
|
|
37
|
+
const index = props.items.findIndex(i => i.title === key);
|
|
38
|
+
selectItem(index);
|
|
39
|
+
}, children: props.items.map((item, index) => (_jsx(ListboxItem, {
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
className: `${index === selectedIndex ? 'bg-default-100' : ''}`, startContent: _jsx("span", { className: "text-xl", children: "\uD83D\uDCC4" }), children: item.title }, item.title))) }) }));
|
|
42
|
+
});
|
|
43
|
+
SchemaSuggestionList.displayName = 'SchemaSuggestionList';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SchemaDef } from '../../lib/types';
|
|
2
|
+
interface TiptapEditorProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
className?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
availableSchemas?: SchemaDef[];
|
|
8
|
+
currentSpeciality?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const TiptapEditor: ({ value, onChange, className, placeholder, availableSchemas, currentSpeciality }: TiptapEditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export default TiptapEditor;
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Color } from '@tiptap/extension-color';
|
|
4
|
+
import { TextStyle } from '@tiptap/extension-text-style';
|
|
5
|
+
import Highlight from '@tiptap/extension-highlight';
|
|
6
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
7
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
8
|
+
import Link from '@tiptap/extension-link';
|
|
9
|
+
import { api } from '../../lib/api';
|
|
10
|
+
import Underline from '@tiptap/extension-underline';
|
|
11
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
12
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
13
|
+
import { Table } from '@tiptap/extension-table';
|
|
14
|
+
import TableRow from '@tiptap/extension-table-row';
|
|
15
|
+
import TableCell from '@tiptap/extension-table-cell';
|
|
16
|
+
import TableHeader from '@tiptap/extension-table-header';
|
|
17
|
+
import { ResizableImage } from './extensions/ResizableImage';
|
|
18
|
+
import { GridContainer, GridItem } from './extensions/Grid';
|
|
19
|
+
import { LayoutRow, LayoutColumn } from './extensions/Layout';
|
|
20
|
+
import { Toolbar } from './Toolbar';
|
|
21
|
+
import { cn } from '../../lib/utils';
|
|
22
|
+
// Styles for Tiptap
|
|
23
|
+
// Styles for Tiptap
|
|
24
|
+
// import '../../styles.css'; // REMOVED: CSS should be imported by consumer (see package.json exports)
|
|
25
|
+
import { SlashCommand, getSuggestionOptions } from './extensions/SlashCommand';
|
|
26
|
+
// ... imports
|
|
27
|
+
import { SchemaInsertionModal } from './components/SchemaInsertionModal';
|
|
28
|
+
import { DistrictGridModal } from './components/DistrictGridModal';
|
|
29
|
+
import { ImageBubbleMenu } from './components/ImageBubbleMenu';
|
|
30
|
+
import { TableBubbleMenu } from './components/TableBubbleMenu';
|
|
31
|
+
import { Container } from './extensions/Container';
|
|
32
|
+
// ... interface
|
|
33
|
+
export const TiptapEditor = ({ value, onChange, className, placeholder = 'Start writing...', availableSchemas = [], currentSpeciality }) => {
|
|
34
|
+
// State for schema insertion modal
|
|
35
|
+
const [selectedSchema, setSelectedSchema] = useState(null);
|
|
36
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
37
|
+
// State for district grid modal
|
|
38
|
+
const [isDistrictGridModalOpen, setIsDistrictGridModalOpen] = useState(false);
|
|
39
|
+
// Filter restricted schemas
|
|
40
|
+
const filteredSchemas = React.useMemo(() => {
|
|
41
|
+
const restricted = ['Roles', 'Settings', 'Users'];
|
|
42
|
+
return availableSchemas.filter(s => !restricted.includes(s.modelName));
|
|
43
|
+
}, [availableSchemas]);
|
|
44
|
+
const extensions = React.useMemo(() => [
|
|
45
|
+
StarterKit.configure({
|
|
46
|
+
heading: {
|
|
47
|
+
levels: [1, 2, 3]
|
|
48
|
+
}
|
|
49
|
+
}),
|
|
50
|
+
Highlight.configure({
|
|
51
|
+
multicolor: true,
|
|
52
|
+
}),
|
|
53
|
+
TextStyle,
|
|
54
|
+
Color,
|
|
55
|
+
Link.configure({
|
|
56
|
+
openOnClick: false,
|
|
57
|
+
HTMLAttributes: {
|
|
58
|
+
class: 'editor-link'
|
|
59
|
+
}
|
|
60
|
+
}),
|
|
61
|
+
Underline,
|
|
62
|
+
TextAlign.configure({
|
|
63
|
+
types: ['heading', 'paragraph', 'image'],
|
|
64
|
+
}),
|
|
65
|
+
ResizableImage,
|
|
66
|
+
GridContainer,
|
|
67
|
+
GridItem,
|
|
68
|
+
Container,
|
|
69
|
+
LayoutRow,
|
|
70
|
+
LayoutColumn,
|
|
71
|
+
Table.configure({
|
|
72
|
+
resizable: true,
|
|
73
|
+
}),
|
|
74
|
+
TableRow,
|
|
75
|
+
TableHeader,
|
|
76
|
+
TableCell,
|
|
77
|
+
Placeholder.configure({
|
|
78
|
+
placeholder,
|
|
79
|
+
}),
|
|
80
|
+
SlashCommand.configure({
|
|
81
|
+
suggestion: getSuggestionOptions(filteredSchemas, (item) => {
|
|
82
|
+
setSelectedSchema(item);
|
|
83
|
+
setIsModalOpen(true);
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
], [placeholder, filteredSchemas]);
|
|
87
|
+
const editor = useEditor({
|
|
88
|
+
extensions,
|
|
89
|
+
content: value,
|
|
90
|
+
editorProps: {
|
|
91
|
+
attributes: {
|
|
92
|
+
class: cn("prose prose-sm sm:prose-base dark:prose-invert focus:outline-none h-[500px] overflow-y-auto p-4 max-w-none [&_li_p]:m-0", className)
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
onUpdate: ({ editor }) => {
|
|
96
|
+
onChange(editor.getHTML());
|
|
97
|
+
},
|
|
98
|
+
immediatelyRender: false,
|
|
99
|
+
});
|
|
100
|
+
// ... useEffect for sync ...
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (editor && value !== editor.getHTML()) {
|
|
103
|
+
if (editor.getText() === '' && value === '')
|
|
104
|
+
return;
|
|
105
|
+
if (!editor.isFocused) {
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
editor.commands.setContent(value);
|
|
108
|
+
}, 0);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, [value, editor]);
|
|
112
|
+
const handleSchemaInsert = async ({ viewType, template, ids = [], urlPattern }) => {
|
|
113
|
+
if (!editor || !selectedSchema)
|
|
114
|
+
return;
|
|
115
|
+
try {
|
|
116
|
+
let items = [];
|
|
117
|
+
if (ids.length > 0) {
|
|
118
|
+
// Fetch specific IDs
|
|
119
|
+
// Since api.list might not support ids array directly in all implementations,
|
|
120
|
+
// and we don't have a bulk get endpoint guaranteed, assume we can parallel fetch or filter.
|
|
121
|
+
// Optimally: user search results are already in RefMultiSelect but we don't have access to them here easily.
|
|
122
|
+
// We'll simplisticly fetch them in parallel.
|
|
123
|
+
// In production, an API like /model?ids=a,b,c is better.
|
|
124
|
+
// Let's assume we can fetch them one by one for now (up to ~10-20 is fine).
|
|
125
|
+
const promises = ids.map(id => api.get(selectedSchema.modelName, id).then((r) => r.data || r).catch(() => null));
|
|
126
|
+
const results = await Promise.all(promises);
|
|
127
|
+
items = results.filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Fallback to latest 6
|
|
131
|
+
const res = await api.list(selectedSchema.modelName, 0, 6);
|
|
132
|
+
const payload = res?.data ?? res;
|
|
133
|
+
items =
|
|
134
|
+
payload?.items ??
|
|
135
|
+
payload?.docs ??
|
|
136
|
+
payload?.results ??
|
|
137
|
+
payload?.list ??
|
|
138
|
+
(Array.isArray(payload) ? payload : []);
|
|
139
|
+
items = items.slice(0, 6);
|
|
140
|
+
}
|
|
141
|
+
const displayItems = items;
|
|
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
|
+
};
|
|
196
|
+
// Helper to interpolate string with item data
|
|
197
|
+
const processTemplate = (tmpl, item) => {
|
|
198
|
+
let output = tmpl || selectedSchema.modelName;
|
|
199
|
+
output = output.replace(/{([\w\.]+)}/g, (match, key) => {
|
|
200
|
+
const val = getGenericNestedValue(item, key);
|
|
201
|
+
return val !== '' ? val : match;
|
|
202
|
+
});
|
|
203
|
+
return output;
|
|
204
|
+
};
|
|
205
|
+
// Helper for URL
|
|
206
|
+
const processUrl = (pattern, item) => {
|
|
207
|
+
const suffix = processTemplate(pattern || '{slug}_{id}', item);
|
|
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.
|
|
220
|
+
const cleanBase = baseFrontendUrl.replace(/\/$/, '');
|
|
221
|
+
const cleanSuffix = suffix.replace(/^\//, '');
|
|
222
|
+
return `${cleanBase}/${cleanSuffix}`;
|
|
223
|
+
};
|
|
224
|
+
let html = ''; // Add new line first
|
|
225
|
+
// Helper to interpolate string with item data
|
|
226
|
+
// const processTemplate = (tmpl: string, item: any) => {
|
|
227
|
+
// let output = tmpl || selectedSchema.modelName;
|
|
228
|
+
// output = output.replace(/{(\w+)}/g, (match, key) => {
|
|
229
|
+
// return item[key] !== undefined && item[key] !== null ? String(item[key]) : match;
|
|
230
|
+
// });
|
|
231
|
+
// return output;
|
|
232
|
+
// };
|
|
233
|
+
const gridClass = "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 my-4";
|
|
234
|
+
const listClass = "flex flex-col gap-2 my-4";
|
|
235
|
+
// Link to styling ...
|
|
236
|
+
// For Grid: Use our custom GridContainer/GridItem nodes via data-type
|
|
237
|
+
// For List: Use standard divs (if supported) or just paragraphs?
|
|
238
|
+
// Actually, we can use the same Card style for list but in a single column?
|
|
239
|
+
// Tiptap might strip 'flex-col' div if we don't have a node for it.
|
|
240
|
+
// Let's use GridContainer with a modifier or just a standard behavior?
|
|
241
|
+
// The GridContainer is hardcoded to 3 cols.
|
|
242
|
+
// Responsive grid covers the list view mainly (mobile).
|
|
243
|
+
// But if user wants explicit List, maybe we use a different structure.
|
|
244
|
+
// For now, let's focus on GRID as requested.
|
|
245
|
+
// If viewType is 'list', we might just insert paragraphs or a different styled container.
|
|
246
|
+
// Actually, if we want list view, we can style the container?
|
|
247
|
+
// But GridContainer class is hardcoded in the Node extension.
|
|
248
|
+
// We can pass attributes?
|
|
249
|
+
// "data-cols"="1"?
|
|
250
|
+
// But I didn't verify dynamic classes in extension.
|
|
251
|
+
// Let's stick to Grid for 'grid' view type.
|
|
252
|
+
// For 'list', maybe we can just use paragraphs for now, or the same Grid with 1 column?
|
|
253
|
+
// I'll assume 'grid' is the priority.
|
|
254
|
+
if (viewType === 'grid') {
|
|
255
|
+
html = `<div data-type="grid-container">`;
|
|
256
|
+
if (displayItems.length === 0) {
|
|
257
|
+
html += `<div data-type="grid-item" href="#">No items found</div>`;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
displayItems.forEach(item => {
|
|
261
|
+
const text = processTemplate(template, item);
|
|
262
|
+
const url = processUrl(urlPattern || '', item);
|
|
263
|
+
html += `<div data-type="grid-item" href="${url}">${text}</div>`;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
html += `</div>`; // Exit block
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// List View - use standard markup which might be safer or just paragraphs
|
|
270
|
+
html = `<p><strong>${selectedSchema.modelName} List:</strong></p><ul class="list-disc pl-5">`;
|
|
271
|
+
displayItems.forEach(item => {
|
|
272
|
+
const text = processTemplate(template, item);
|
|
273
|
+
const url = processUrl(urlPattern || '', item);
|
|
274
|
+
html += `<li><a href="${url}" target="_blank">${text}</a></li>`;
|
|
275
|
+
});
|
|
276
|
+
html += `</ul>`;
|
|
277
|
+
}
|
|
278
|
+
// Insert spacing first securely
|
|
279
|
+
editor.chain().focus()
|
|
280
|
+
.insertContent('<p> </p>') // Use non-breaking space to ensure block creation
|
|
281
|
+
.insertContent(html)
|
|
282
|
+
.run();
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
console.error("Failed to fetch schema data", err);
|
|
286
|
+
// Fallback error message in editor?
|
|
287
|
+
editor.chain().focus().insertContent(`<p class="text-red-500">Error loading data for ${selectedSchema.modelName}</p>`).run();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const handleDistrictGridInsert = async ({ districts, baseType, specialitySlug }) => {
|
|
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
|
+
const displayName = district.name || district.bnName || districtSlug;
|
|
314
|
+
html += `<div data-type="grid-item" href="${url}"><p>${displayName}</p></div>`;
|
|
315
|
+
});
|
|
316
|
+
html += `</div>`;
|
|
317
|
+
// Insert spacing first, then grid
|
|
318
|
+
editor.chain().focus()
|
|
319
|
+
.insertContent('<p> </p>')
|
|
320
|
+
.insertContent(html)
|
|
321
|
+
.run();
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
console.error('Failed to fetch district data', err);
|
|
325
|
+
editor.chain().focus().insertContent(`<p class="text-red-500">Error loading district data</p>`).run();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
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 })] }));
|
|
329
|
+
};
|
|
330
|
+
export default TiptapEditor;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Editor } from '@tiptap/react';
|
|
2
|
+
interface ToolbarProps {
|
|
3
|
+
editor: Editor | null;
|
|
4
|
+
onDistrictGridClick?: () => void;
|
|
5
|
+
}
|
|
6
|
+
export declare const Toolbar: ({ editor, onDistrictGridClick }: ToolbarProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
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';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import { uploadFile } from '../../lib/upload';
|
|
6
|
+
export const Toolbar = ({ editor, onDistrictGridClick }) => {
|
|
7
|
+
const fileInputRef = useRef(null);
|
|
8
|
+
if (!editor) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const addImage = async (e) => {
|
|
12
|
+
const file = e.target.files?.[0];
|
|
13
|
+
if (file) {
|
|
14
|
+
try {
|
|
15
|
+
// Upload logic here - reusing the one we discussed or a simple URL creator for now
|
|
16
|
+
// For production, use the real uploadFile utility
|
|
17
|
+
// const url = URL.createObjectURL(file);
|
|
18
|
+
const url = await uploadFile(file); // Needs verify this exists/works
|
|
19
|
+
if (url) {
|
|
20
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error("Upload failed", error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Reset inputs
|
|
28
|
+
if (fileInputRef.current)
|
|
29
|
+
fileInputRef.current.value = '';
|
|
30
|
+
};
|
|
31
|
+
const handleImageClick = () => {
|
|
32
|
+
fileInputRef.current?.click();
|
|
33
|
+
};
|
|
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(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("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("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
|
+
editor
|
|
37
|
+
.chain()
|
|
38
|
+
.focus()
|
|
39
|
+
.insertContent('<p> </p>')
|
|
40
|
+
.insertContent('<div data-type="layout-row" data-cols="2"><div data-type="layout-column"><p></p></div><div data-type="layout-column"><p></p></div></div>')
|
|
41
|
+
.run();
|
|
42
|
+
}, children: _jsx(LayoutTemplate, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => {
|
|
43
|
+
editor
|
|
44
|
+
.chain()
|
|
45
|
+
.focus()
|
|
46
|
+
.insertContent('<p> </p>')
|
|
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
|
+
.run();
|
|
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
|
+
editor
|
|
51
|
+
.chain()
|
|
52
|
+
.focus()
|
|
53
|
+
.insertContent({
|
|
54
|
+
type: 'container',
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: 'paragraph',
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
})
|
|
61
|
+
.run();
|
|
62
|
+
}, children: _jsxs("div", { className: "flex flex-col items-center justify-center", children: [_jsx(Square, { size: 18 }), _jsx("span", { className: "text-[10px] leading-none", children: "Full" })] }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: _jsx(TableIcon, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("div", { className: "relative group", children: [_jsx(ToolbarButton, { onClick: () => { }, isActive: editor.isActive('textStyle'), children: _jsx("div", { className: "flex items-center", children: _jsx(Type, { size: 18 }) }) }), _jsx("input", { type: "color", className: "absolute inset-0 opacity-0 w-full h-full cursor-pointer", onChange: (e) => editor.chain().focus().setColor(e.target.value).run(), value: editor.getAttributes('textStyle').color || '#000000' })] }), _jsxs("div", { className: "relative group", children: [_jsx(ToolbarButton, { onClick: () => { }, isActive: editor.isActive('highlight'), children: _jsx("div", { className: "flex items-center", children: _jsx(Palette, { size: 18 }) }) }), _jsx("input", { type: "color", className: "absolute inset-0 opacity-0 w-full h-full cursor-pointer", onChange: (e) => {
|
|
63
|
+
const color = e.target.value;
|
|
64
|
+
editor.chain().focus().setHighlight({ color }).run();
|
|
65
|
+
}, value: editor.getAttributes('highlight').color || '#ffff00' })] }), _jsxs("div", { className: "relative group", children: [_jsx(ToolbarButton, { onClick: () => { }, isActive: !!editor.getAttributes('gridContainer').backgroundColor || !!editor.getAttributes('layoutRow').backgroundColor || !!editor.getAttributes('layoutColumn').backgroundColor || !!editor.getAttributes('container').backgroundColor, children: _jsx(PaintBucket, { size: 18 }) }), _jsx("input", { type: "color", className: "absolute inset-0 opacity-0 w-full h-full cursor-pointer", onChange: (e) => {
|
|
66
|
+
const val = e.target.value;
|
|
67
|
+
// Priority: Grid Container (Apply to ALL) -> Container -> Column -> Row
|
|
68
|
+
if (editor.isActive('gridItem')) {
|
|
69
|
+
// Set on CONTAINER so it applies to all items via variable
|
|
70
|
+
editor.chain().focus().updateAttributes('gridContainer', { backgroundColor: val }).run();
|
|
71
|
+
}
|
|
72
|
+
else if (editor.isActive('container')) {
|
|
73
|
+
editor.chain().focus().updateAttributes('container', { backgroundColor: val }).run();
|
|
74
|
+
}
|
|
75
|
+
else if (editor.isActive('layoutColumn')) {
|
|
76
|
+
editor.chain().focus().updateAttributes('layoutColumn', { backgroundColor: val }).run();
|
|
77
|
+
}
|
|
78
|
+
else if (editor.isActive('layoutRow')) {
|
|
79
|
+
editor.chain().focus().updateAttributes('layoutRow', { backgroundColor: val }).run();
|
|
80
|
+
}
|
|
81
|
+
} })] })] }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsxs("div", { className: "flex items-center gap-1 relative group", children: [_jsx("span", { className: "text-[10px] text-gray-500 absolute left-1 pointer-events-none", children: "R" }), _jsx("input", { type: "number", placeholder: "0", className: "w-16 h-8 pl-4 pr-1 text-xs bg-transparent border border-gray-200 dark:border-gray-700 rounded", onChange: (e) => {
|
|
82
|
+
const val = e.target.value;
|
|
83
|
+
const radius = val ? `${val}px` : null;
|
|
84
|
+
const cmd = editor.chain().focus();
|
|
85
|
+
if (editor.isActive('gridItem') || editor.isActive('gridContainer')) {
|
|
86
|
+
cmd.updateAttributes('gridContainer', { borderRadius: radius });
|
|
87
|
+
}
|
|
88
|
+
else if (editor.isActive('container')) {
|
|
89
|
+
cmd.updateAttributes('container', { borderRadius: radius });
|
|
90
|
+
}
|
|
91
|
+
else if (editor.isActive('layoutColumn')) {
|
|
92
|
+
cmd.updateAttributes('layoutColumn', { borderRadius: radius });
|
|
93
|
+
}
|
|
94
|
+
else if (editor.isActive('layoutRow')) {
|
|
95
|
+
cmd.updateAttributes('layoutRow', { borderRadius: radius });
|
|
96
|
+
}
|
|
97
|
+
cmd.run();
|
|
98
|
+
} })] }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: handleImageClick, children: _jsx(ImageIcon, { size: 18 }) }), _jsx("input", { type: "file", ref: fileInputRef, onChange: addImage, className: "hidden", accept: "image/*" }), _jsx("div", { className: "flex-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().undo().run(), disabled: !editor.can().undo(), children: _jsx(Undo, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().redo().run(), disabled: !editor.can().redo(), children: _jsx(Redo, { size: 18 }) })] }));
|
|
99
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SchemaDef } from '../../../lib/types';
|
|
3
|
+
export interface CommandListProps {
|
|
4
|
+
items: SchemaDef[];
|
|
5
|
+
command: (item: any) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare const CommandList: React.ForwardRefExoticComponent<CommandListProps & React.RefAttributes<unknown>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
|
3
|
+
import { cn } from '../../../lib/utils';
|
|
4
|
+
import { LayoutGrid } from 'lucide-react';
|
|
5
|
+
export const CommandList = forwardRef((props, ref) => {
|
|
6
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
7
|
+
const selectItem = (index) => {
|
|
8
|
+
const item = props.items[index];
|
|
9
|
+
if (item) {
|
|
10
|
+
props.command(item);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const upHandler = () => {
|
|
14
|
+
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
|
15
|
+
};
|
|
16
|
+
const downHandler = () => {
|
|
17
|
+
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
|
18
|
+
};
|
|
19
|
+
const enterHandler = () => {
|
|
20
|
+
selectItem(selectedIndex);
|
|
21
|
+
};
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setSelectedIndex(0);
|
|
24
|
+
}, [props.items]);
|
|
25
|
+
useImperativeHandle(ref, () => ({
|
|
26
|
+
onKeyDown: ({ event }) => {
|
|
27
|
+
if (event.key === 'ArrowUp') {
|
|
28
|
+
upHandler();
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (event.key === 'ArrowDown') {
|
|
32
|
+
downHandler();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (event.key === 'Enter') {
|
|
36
|
+
enterHandler();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
if (!props.items.length) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return (_jsx("div", { className: "z-50 min-w-[250px] max-h-[300px] overflow-y-auto rounded-md border border-gray-200 bg-white shadow-md dark:border-gray-800 dark:bg-zinc-950 p-1", children: props.items.map((item, index) => (_jsxs("button", { className: cn("flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors", index === selectedIndex ? "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-50" : "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"), onClick: () => selectItem(index), children: [_jsx(LayoutGrid, { className: "h-4 w-4" }), _jsx("span", { className: "flex-1 text-left", children: item.modelName })] }, index))) }));
|
|
46
|
+
});
|
|
47
|
+
CommandList.displayName = 'CommandList';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface DistrictGridModalProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
onClose: () => void;
|
|
4
|
+
onInsert: (config: {
|
|
5
|
+
districts: string[];
|
|
6
|
+
baseType: 'doctors' | 'hospitals';
|
|
7
|
+
specialitySlug?: string;
|
|
8
|
+
}) => void;
|
|
9
|
+
currentSpeciality?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const DistrictGridModal: ({ isOpen, onClose, onInsert, currentSpeciality, }: DistrictGridModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export {};
|