Dev #7
No files matched your search
@@ -0,0 +1,271 @@
|
|||||||
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { Link as LinkIcon, FileText, Globe, X, Search, ExternalLink, Loader2 } from 'lucide-react';
|
||||||
|
import { api } from '../../lib/api';
|
||||||
|
import { Document } from '../../lib/types';
|
||||||
|
|
||||||
|
export interface LinkModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onInsertLink: (markdown: string) => void;
|
||||||
|
availableDocs?: { id: string; title: string; icon?: string; folderPath?: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkModal: React.FC<LinkModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onInsertLink,
|
||||||
|
availableDocs,
|
||||||
|
}) => {
|
||||||
|
const [tab, setTab] = useState<'note' | 'web'>('note');
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [docs, setDocs] = useState<Document[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const [webTitle, setWebTitle] = useState('');
|
||||||
|
const [webUrl, setWebUrl] = useState('');
|
||||||
|
|
||||||
|
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const webTitleInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
// Load documents when modal opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
if (availableDocs && availableDocs.length > 0) {
|
||||||
|
setDocs(availableDocs as Document[]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let isCancelled = false;
|
||||||
|
setLoading(true);
|
||||||
|
api
|
||||||
|
.getDocuments()
|
||||||
|
.then((fetched) => {
|
||||||
|
if (!isCancelled) {
|
||||||
|
setDocs(fetched);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!isCancelled) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
|
}, [isOpen, availableDocs]);
|
||||||
|
|
||||||
|
// Focus input on mount or tab change
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (tab === 'note') {
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
webTitleInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isOpen, tab]);
|
||||||
|
|
||||||
|
// Close on Escape
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
const filteredDocs = useMemo(() => {
|
||||||
|
if (!search.trim()) return docs;
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
return docs.filter(
|
||||||
|
(d) =>
|
||||||
|
d.title.toLowerCase().includes(q) ||
|
||||||
|
(d.folderPath && d.folderPath.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}, [docs, search]);
|
||||||
|
|
||||||
|
const handleSelectNote = (doc: { title: string }) => {
|
||||||
|
const noteTitle = doc.title.trim() || 'Notiz';
|
||||||
|
onInsertLink(`[[${noteTitle}]]`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWebLinkSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let url = webUrl.trim();
|
||||||
|
if (!url) return;
|
||||||
|
if (!/^https?:\/\//i.test(url) && !url.startsWith('#') && !url.startsWith('/')) {
|
||||||
|
url = `https://${url}`;
|
||||||
|
}
|
||||||
|
const label = webTitle.trim() || url;
|
||||||
|
onInsertLink(`[${label}](${url})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4 backdrop-blur-xs animate-in fade-in duration-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="panel-surface shadow-panel border border-border rounded-card max-w-md w-full overflow-hidden flex flex-col max-h-[85vh]">
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-panel">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LinkIcon className="w-4 h-4 text-accent" />
|
||||||
|
<h3 className="text-sm font-bold text-main">Link einfügen</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-1 rounded-btn text-muted hover:text-main hover:bg-panel-elevated transition-colors"
|
||||||
|
title="Schließen (Esc)"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Switcher */}
|
||||||
|
<div className="flex border-b border-border bg-panel-elevated/40 p-1 gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTab('note')}
|
||||||
|
className={`flex-1 py-1.5 px-3 rounded-btn text-xs font-semibold flex items-center justify-center gap-1.5 transition-colors ${
|
||||||
|
tab === 'note'
|
||||||
|
? 'bg-accent text-on-accent shadow-sm'
|
||||||
|
: 'text-muted hover:text-main hover:bg-panel-elevated'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
<span>Notiz verknüpfen</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTab('web')}
|
||||||
|
className={`flex-1 py-1.5 px-3 rounded-btn text-xs font-semibold flex items-center justify-center gap-1.5 transition-colors ${
|
||||||
|
tab === 'web'
|
||||||
|
? 'bg-accent text-on-accent shadow-sm'
|
||||||
|
: 'text-muted hover:text-main hover:bg-panel-elevated'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Globe className="w-3.5 h-3.5" />
|
||||||
|
<span>Web-Link</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div className="p-4 flex-1 overflow-y-auto">
|
||||||
|
{tab === 'note' ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-muted" />
|
||||||
|
<input
|
||||||
|
ref={searchInputRef}
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Notiz suchen…"
|
||||||
|
className="w-full bg-input border border-border focus:border-accent text-main text-xs pl-8 pr-3 py-1.5 rounded-btn focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="py-8 flex items-center justify-center text-xs text-muted gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-accent" />
|
||||||
|
<span>Notizen werden geladen…</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-60 overflow-y-auto space-y-1 pr-1">
|
||||||
|
{filteredDocs.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectNote(d)}
|
||||||
|
className="w-full text-left px-3 py-2 rounded-btn hover:bg-panel-elevated flex items-center justify-between text-xs transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="text-base shrink-0">{d.icon || '📝'}</span>
|
||||||
|
<span className="font-medium text-main truncate group-hover:text-accent">
|
||||||
|
{d.title || 'Unbenannte Notiz'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{d.folderPath && (
|
||||||
|
<span className="text-[10px] text-faint ml-2 shrink-0 truncate max-w-[100px]">
|
||||||
|
📁 {d.folderPath}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredDocs.length === 0 && (
|
||||||
|
<div className="py-8 text-center text-xs text-muted">
|
||||||
|
{search
|
||||||
|
? `Keine Notiz für "${search}" gefunden.`
|
||||||
|
: 'Keine Notizen im Workspace vorhanden.'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleWebLinkSubmit} className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-semibold text-muted mb-1">
|
||||||
|
Link-Text / Titel
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={webTitleInputRef}
|
||||||
|
type="text"
|
||||||
|
value={webTitle}
|
||||||
|
onChange={(e) => setWebTitle(e.target.value)}
|
||||||
|
placeholder="z. B. MindSift Dokumentation"
|
||||||
|
className="w-full bg-input border border-border focus:border-accent text-main text-xs px-3 py-1.5 rounded-btn focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-semibold text-muted mb-1">
|
||||||
|
Web-Adresse (URL) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={webUrl}
|
||||||
|
onChange={(e) => setWebUrl(e.target.value)}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
className="w-full bg-input border border-border focus:border-accent text-main text-xs px-3 py-1.5 rounded-btn focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="btn-action px-3 py-1.5 text-xs text-muted hover:text-main"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!webUrl.trim()}
|
||||||
|
className="btn-primary inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-on-accent rounded-btn disabled:opacity-50 transition-opacity"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
<span>Link einfügen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,12 +15,15 @@ import {
|
|||||||
Hash,
|
Hash,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
|
Link as LinkIcon,
|
||||||
|
FileText,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { DocumentBlock, BlockType } from '../../lib/types';
|
import { DocumentBlock, BlockType } from '../../lib/types';
|
||||||
import { RemoteCursor } from '../../lib/useDocumentSync';
|
import { RemoteCursor } from '../../lib/useDocumentSync';
|
||||||
|
import { LinkModal } from './LinkModal';
|
||||||
|
|
||||||
export interface SlashCommand {
|
export interface SlashCommand {
|
||||||
type: BlockType;
|
type: BlockType | 'link';
|
||||||
label: string;
|
label: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@@ -28,6 +31,7 @@ export interface SlashCommand {
|
|||||||
|
|
||||||
const SLASH_COMMANDS: SlashCommand[] = [
|
const SLASH_COMMANDS: SlashCommand[] = [
|
||||||
{ type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: <ListTodo className="w-3.5 h-3.5" /> },
|
{ type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: <ListTodo className="w-3.5 h-3.5" /> },
|
||||||
|
{ type: 'link', label: '/link', desc: 'Notiz verknüpfen oder Web-Link einfügen', icon: <LinkIcon className="w-3.5 h-3.5" /> },
|
||||||
{ type: 'h1', label: '/h1', desc: 'Große Überschrift', icon: <Heading1 className="w-3.5 h-3.5" /> },
|
{ type: 'h1', label: '/h1', desc: 'Große Überschrift', icon: <Heading1 className="w-3.5 h-3.5" /> },
|
||||||
{ type: 'h2', label: '/h2', desc: 'Abschnitts-Überschrift', icon: <Heading2 className="w-3.5 h-3.5" /> },
|
{ type: 'h2', label: '/h2', desc: 'Abschnitts-Überschrift', icon: <Heading2 className="w-3.5 h-3.5" /> },
|
||||||
{ type: 'h3', label: '/h3', desc: 'Unterabschnitt', icon: <Heading3 className="w-3.5 h-3.5" /> },
|
{ type: 'h3', label: '/h3', desc: 'Unterabschnitt', icon: <Heading3 className="w-3.5 h-3.5" /> },
|
||||||
@@ -89,6 +93,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
const [localContent, setLocalContent] = useState<string>(block.content || '');
|
const [localContent, setLocalContent] = useState<string>(block.content || '');
|
||||||
const [slashSearch, setSlashSearch] = useState<string | null>(null);
|
const [slashSearch, setSlashSearch] = useState<string | null>(null);
|
||||||
const [slashIndex, setSlashIndex] = useState(0);
|
const [slashIndex, setSlashIndex] = useState(0);
|
||||||
|
const [showLinkModal, setShowLinkModal] = useState<boolean>(false);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const debounceTimerRef = useRef<number | null>(null);
|
const debounceTimerRef = useRef<number | null>(null);
|
||||||
@@ -158,6 +163,11 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
const applySlashCommand = (cmd: SlashCommand) => {
|
const applySlashCommand = (cmd: SlashCommand) => {
|
||||||
setSlashSearch(null);
|
setSlashSearch(null);
|
||||||
const clean = localContent.replace(/\/([a-z0-9]*)$/i, '').trim();
|
const clean = localContent.replace(/\/([a-z0-9]*)$/i, '').trim();
|
||||||
|
if (cmd.type === 'link') {
|
||||||
|
setLocalContent(clean);
|
||||||
|
setShowLinkModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLocalContent(clean);
|
setLocalContent(clean);
|
||||||
flushDebounce(clean, {
|
flushDebounce(clean, {
|
||||||
type: cmd.type,
|
type: cmd.type,
|
||||||
@@ -165,6 +175,16 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInsertLink = (linkMarkdown: string) => {
|
||||||
|
setShowLinkModal(false);
|
||||||
|
const cursor = textareaRef.current?.selectionStart ?? localContent.length;
|
||||||
|
const before = localContent.slice(0, cursor);
|
||||||
|
const after = localContent.slice(cursor);
|
||||||
|
const newContent = `${before}${before && !before.endsWith(' ') ? ' ' : ''}${linkMarkdown}${after && !after.startsWith(' ') ? ' ' : ''}${after}`.trim();
|
||||||
|
setLocalContent(newContent);
|
||||||
|
flushDebounce(newContent);
|
||||||
|
};
|
||||||
|
|
||||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
if (!canEdit) return;
|
if (!canEdit) return;
|
||||||
let val = e.target.value;
|
let val = e.target.value;
|
||||||
@@ -339,14 +359,20 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
const level = Math.max(0, block.level || 0);
|
const level = Math.max(0, block.level || 0);
|
||||||
const visualLevel = Math.max(0, level - (levelOffset || 0));
|
const visualLevel = Math.max(0, level - (levelOffset || 0));
|
||||||
|
|
||||||
// Parse rich tokens (WikiLinks [[Page]] and #Tags)
|
// Parse rich tokens (WikiLinks [[Page]], Markdown Links [Title](url), raw URLs, and #Tags)
|
||||||
const tokens = useMemo(() => {
|
const tokens = useMemo(() => {
|
||||||
const parts = (localContent || '').split(/(\[\[.*?\]\]|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
const parts = (localContent || '').split(
|
||||||
|
/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/.*?\)|\bhttps?:\/\/[^\s]+|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g
|
||||||
|
);
|
||||||
return parts.filter(Boolean);
|
return parts.filter(Boolean);
|
||||||
}, [localContent]);
|
}, [localContent]);
|
||||||
|
|
||||||
const hasSpecialTokens = tokens.some(
|
const hasSpecialTokens = tokens.some(
|
||||||
(t) => t.startsWith('#') || (t.startsWith('[[') && t.endsWith(']]'))
|
(t) =>
|
||||||
|
t.startsWith('#') ||
|
||||||
|
(t.startsWith('[[') && t.endsWith(']]')) ||
|
||||||
|
/^\[(.*?)\]\((https?:\/\/.*?)\)$/.test(t) ||
|
||||||
|
/^https?:\/\//.test(t)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -619,12 +645,46 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onWikiLinkClick?.(page)}
|
onClick={() => onWikiLinkClick?.(page)}
|
||||||
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-medium text-status-info bg-status-info/10 hover:bg-status-info/20 rounded-btn transition-colors"
|
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-medium text-status-info bg-status-info/10 hover:bg-status-info/20 rounded-btn transition-colors"
|
||||||
|
title={`Notiz öffnen: ${page}`}
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-2.5 h-2.5 opacity-80" />
|
<FileText className="w-2.5 h-2.5 opacity-80" />
|
||||||
<span>{page}</span>
|
<span>{page}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const mdMatch = part.match(/^\[(.*?)\]\((https?:\/\/.*?)\)$/);
|
||||||
|
if (mdMatch) {
|
||||||
|
const label = mdMatch[1];
|
||||||
|
const url = mdMatch[2];
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={idx}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-medium text-status-ok bg-status-ok/10 hover:bg-status-ok/20 rounded-btn transition-colors"
|
||||||
|
title={url}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-2.5 h-2.5 opacity-80" />
|
||||||
|
<span>{label || url}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (/^https?:\/\//.test(part)) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={idx}
|
||||||
|
href={part}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-medium text-status-ok bg-status-ok/10 hover:bg-status-ok/20 rounded-btn transition-colors"
|
||||||
|
title={part}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-2.5 h-2.5 opacity-80" />
|
||||||
|
<span className="truncate max-w-[160px]">{part}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -689,6 +749,15 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
<X className="w-3.5 h-3.5" />
|
<X className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Link Insertion Modal */}
|
||||||
|
{showLinkModal && (
|
||||||
|
<LinkModal
|
||||||
|
isOpen={showLinkModal}
|
||||||
|
onClose={() => setShowLinkModal(false)}
|
||||||
|
onInsertLink={handleInsertLink}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user