feat(workspace): recursive folder tree, link modal redesign, todo grouping & line jumping, hashtag child listing
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 23s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 23s
This commit is contained in:
1 parent
6c1898d489
commit
4850f9dfad
6 files changed
+802
-223
No files matched your search
@@ -26,6 +26,8 @@ export interface DocumentEditorProps {
|
|||||||
currentUser?: User | null;
|
currentUser?: User | null;
|
||||||
initialDoc?: Document | null;
|
initialDoc?: Document | null;
|
||||||
availableFolders: string[];
|
availableFolders: string[];
|
||||||
|
autoFocusTitle?: boolean;
|
||||||
|
initialFocusedBlockId?: string | null;
|
||||||
onDocMetadataChange: (updated: Document) => void;
|
onDocMetadataChange: (updated: Document) => void;
|
||||||
onDeleteDoc: (docId: string) => void;
|
onDeleteDoc: (docId: string) => void;
|
||||||
onOpenShareModal: (doc: Document) => void;
|
onOpenShareModal: (doc: Document) => void;
|
||||||
@@ -143,6 +145,8 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
currentUser,
|
currentUser,
|
||||||
initialDoc,
|
initialDoc,
|
||||||
availableFolders,
|
availableFolders,
|
||||||
|
autoFocusTitle,
|
||||||
|
initialFocusedBlockId,
|
||||||
onDocMetadataChange,
|
onDocMetadataChange,
|
||||||
onDeleteDoc,
|
onDeleteDoc,
|
||||||
onOpenShareModal,
|
onOpenShareModal,
|
||||||
@@ -161,6 +165,18 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
const [showFolderMenu, setShowFolderMenu] = useState<boolean>(false);
|
const [showFolderMenu, setShowFolderMenu] = useState<boolean>(false);
|
||||||
const [zoomedBlockId, setZoomedBlockId] = useState<string | null>(null);
|
const [zoomedBlockId, setZoomedBlockId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocusTitle || (doc?.title === 'Neue Notiz' && !doc?.markdownCache)) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (titleInputRef.current) {
|
||||||
|
titleInputRef.current.focus();
|
||||||
|
titleInputRef.current.select();
|
||||||
|
}
|
||||||
|
}, 60);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [autoFocusTitle]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (zoomedBlockId && !blocks.some((b) => b.id === zoomedBlockId)) {
|
if (zoomedBlockId && !blocks.some((b) => b.id === zoomedBlockId)) {
|
||||||
setZoomedBlockId(null);
|
setZoomedBlockId(null);
|
||||||
@@ -932,6 +948,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
onMoveBlock={handleMoveBlock}
|
onMoveBlock={handleMoveBlock}
|
||||||
onTagClick={onSelectTag}
|
onTagClick={onSelectTag}
|
||||||
onWikiLinkClick={onWikiLinkClick}
|
onWikiLinkClick={onWikiLinkClick}
|
||||||
|
initialFocusedBlockId={initialFocusedBlockId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -941,12 +958,12 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
========================================================================= */
|
========================================================================= */
|
||||||
<>
|
<>
|
||||||
{/* Document Icon + Title Header */}
|
{/* Document Icon + Title Header */}
|
||||||
<div className="flex items-center gap-3 mb-3 relative">
|
<div className="flex items-center gap-3 mb-4 relative">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => canEdit && setShowEmojiPicker((prev) => !prev)}
|
onClick={() => canEdit && setShowEmojiPicker((prev) => !prev)}
|
||||||
className="w-10 h-10 rounded-btn bg-canvas border border-border hover:border-strong flex items-center justify-center text-xl transition-colors"
|
className="text-2xl sm:text-3xl hover:opacity-80 transition-opacity p-0.5 rounded cursor-pointer select-none"
|
||||||
title="Icon ändern"
|
title="Icon ändern"
|
||||||
>
|
>
|
||||||
{doc?.icon || '📝'}
|
{doc?.icon || '📝'}
|
||||||
@@ -958,7 +975,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
key={emoji}
|
key={emoji}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelectIcon(emoji)}
|
onClick={() => handleSelectIcon(emoji)}
|
||||||
className="p-1.5 rounded-btn hover:bg-panel-elevated text-base flex items-center justify-center"
|
className="p-1.5 rounded-btn hover:bg-panel-elevated text-base flex items-center justify-center cursor-pointer"
|
||||||
>
|
>
|
||||||
{emoji}
|
{emoji}
|
||||||
</button>
|
</button>
|
||||||
@@ -979,23 +996,6 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Extracted #Tags Summary Bar */}
|
|
||||||
{extractedTags.length > 0 && (
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5 mb-4 pb-2 border-b border-border">
|
|
||||||
<Hash className="w-3.5 h-3.5 text-faint mr-0.5" />
|
|
||||||
{extractedTags.map((tag) => (
|
|
||||||
<button
|
|
||||||
key={tag}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onSelectTag(tag)}
|
|
||||||
className="badge-accent text-[11px] font-medium px-2 py-0.5 rounded-btn hover:opacity-85 transition-opacity"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Logseq Outliner Tree */}
|
{/* Logseq Outliner Tree */}
|
||||||
<OutlinerTree
|
<OutlinerTree
|
||||||
docId={docId}
|
docId={docId}
|
||||||
@@ -1011,6 +1011,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
onMoveBlock={handleMoveBlock}
|
onMoveBlock={handleMoveBlock}
|
||||||
onTagClick={onSelectTag}
|
onTagClick={onSelectTag}
|
||||||
onWikiLinkClick={onWikiLinkClick}
|
onWikiLinkClick={onWikiLinkClick}
|
||||||
|
initialFocusedBlockId={initialFocusedBlockId}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -111,22 +111,29 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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"
|
className="fixed inset-0 !m-0 modal-overlay z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) onClose();
|
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]">
|
<div className="panel-surface w-full max-w-lg p-5 shadow-panel rounded-card border border-border space-y-4 animate-in fade-in zoom-in-95 duration-150">
|
||||||
{/* Modal Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-panel">
|
<div className="flex items-center justify-between pb-3 border-b border-border">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
<LinkIcon className="w-4 h-4 text-accent" />
|
<div className="w-8 h-8 rounded-btn bg-accent/15 text-accent flex items-center justify-center shrink-0">
|
||||||
<h3 className="text-sm font-bold text-main">Link einfügen</h3>
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="text-sm font-bold text-main truncate">Link einfügen</h3>
|
||||||
|
<p className="text-[11px] text-muted truncate">
|
||||||
|
Notiz intern verknüpfen oder externen Web-Link einbetten
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-1 rounded-btn text-muted hover:text-main hover:bg-panel-elevated transition-colors"
|
className="p-1.5 text-muted hover:text-main rounded-btn hover:bg-panel transition-colors cursor-pointer"
|
||||||
title="Schließen (Esc)"
|
title="Schließen (Esc)"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
@@ -134,14 +141,14 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Switcher */}
|
{/* Tab Switcher */}
|
||||||
<div className="flex border-b border-border bg-panel-elevated/40 p-1 gap-1">
|
<div className="flex rounded-btn bg-panel p-0.5 border border-border">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTab('note')}
|
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 ${
|
className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs font-semibold rounded-btn transition-colors cursor-pointer ${
|
||||||
tab === 'note'
|
tab === 'note'
|
||||||
? 'bg-accent text-on-accent shadow-sm'
|
? 'bg-panel-elevated text-main shadow-xs'
|
||||||
: 'text-muted hover:text-main hover:bg-panel-elevated'
|
: 'text-muted hover:text-main'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FileText className="w-3.5 h-3.5" />
|
<FileText className="w-3.5 h-3.5" />
|
||||||
@@ -150,10 +157,10 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTab('web')}
|
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 ${
|
className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs font-semibold rounded-btn transition-colors cursor-pointer ${
|
||||||
tab === 'web'
|
tab === 'web'
|
||||||
? 'bg-accent text-on-accent shadow-sm'
|
? 'bg-panel-elevated text-main shadow-xs'
|
||||||
: 'text-muted hover:text-main hover:bg-panel-elevated'
|
: 'text-muted hover:text-main'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Globe className="w-3.5 h-3.5" />
|
<Globe className="w-3.5 h-3.5" />
|
||||||
@@ -162,7 +169,7 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div className="p-4 flex-1 overflow-y-auto">
|
<div>
|
||||||
{tab === 'note' ? (
|
{tab === 'note' ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -172,8 +179,8 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder="Notiz suchen…"
|
placeholder="Notiz nach Titel oder Ordner filtern…"
|
||||||
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"
|
className="input-field w-full text-xs pl-8 pr-3 py-2 text-main"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -183,22 +190,22 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
<span>Notizen werden geladen…</span>
|
<span>Notizen werden geladen…</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-60 overflow-y-auto space-y-1 pr-1">
|
<div className="max-h-64 overflow-y-auto space-y-1 pr-1">
|
||||||
{filteredDocs.map((d) => (
|
{filteredDocs.map((d) => (
|
||||||
<button
|
<button
|
||||||
key={d.id}
|
key={d.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelectNote(d)}
|
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"
|
className="w-full text-left px-3 py-2 rounded-btn hover:bg-panel border border-transparent hover:border-border flex items-center justify-between text-xs transition-colors group cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<span className="text-base shrink-0">{d.icon || '📝'}</span>
|
<span className="text-base shrink-0">{d.icon || '📝'}</span>
|
||||||
<span className="font-medium text-main truncate group-hover:text-accent">
|
<span className="font-semibold text-main truncate group-hover:text-accent">
|
||||||
{d.title || 'Unbenannte Notiz'}
|
{d.title || 'Unbenannte Notiz'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{d.folderPath && (
|
{d.folderPath && (
|
||||||
<span className="text-[10px] text-faint ml-2 shrink-0 truncate max-w-[100px]">
|
<span className="badge-neutral text-[10px] ml-2 shrink-0 truncate max-w-[120px]">
|
||||||
📁 {d.folderPath}
|
📁 {d.folderPath}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -206,7 +213,7 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{filteredDocs.length === 0 && (
|
{filteredDocs.length === 0 && (
|
||||||
<div className="py-8 text-center text-xs text-muted">
|
<div className="py-8 text-center text-xs text-muted border border-dashed border-border rounded-card">
|
||||||
{search
|
{search
|
||||||
? `Keine Notiz für "${search}" gefunden.`
|
? `Keine Notiz für "${search}" gefunden.`
|
||||||
: 'Keine Notizen im Workspace vorhanden.'}
|
: 'Keine Notizen im Workspace vorhanden.'}
|
||||||
@@ -218,8 +225,8 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleWebLinkSubmit} className="space-y-3">
|
<form onSubmit={handleWebLinkSubmit} className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[11px] font-semibold text-muted mb-1">
|
<label className="text-[10px] font-bold uppercase tracking-wider text-faint mb-1 block">
|
||||||
Link-Text / Titel
|
Link-Text / Label
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
ref={webTitleInputRef}
|
ref={webTitleInputRef}
|
||||||
@@ -227,12 +234,12 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
value={webTitle}
|
value={webTitle}
|
||||||
onChange={(e) => setWebTitle(e.target.value)}
|
onChange={(e) => setWebTitle(e.target.value)}
|
||||||
placeholder="z. B. MindSift Dokumentation"
|
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"
|
className="input-field w-full p-2 text-xs text-main"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[11px] font-semibold text-muted mb-1">
|
<label className="text-[10px] font-bold uppercase tracking-wider text-faint mb-1 block">
|
||||||
Web-Adresse (URL) *
|
Web-Adresse (URL) *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -241,22 +248,22 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
value={webUrl}
|
value={webUrl}
|
||||||
onChange={(e) => setWebUrl(e.target.value)}
|
onChange={(e) => setWebUrl(e.target.value)}
|
||||||
placeholder="https://example.com"
|
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"
|
className="input-field w-full p-2 text-xs text-main"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2 flex justify-end gap-2">
|
<div className="pt-2 flex justify-end gap-2 border-t border-border">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="btn-action px-3 py-1.5 text-xs text-muted hover:text-main"
|
className="btn-action px-3 py-1.5 text-xs text-main cursor-pointer"
|
||||||
>
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!webUrl.trim()}
|
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"
|
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 cursor-pointer"
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-3.5 h-3.5" />
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
<span>Link einfügen</span>
|
<span>Link einfügen</span>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export interface OutlinerBlockProps {
|
|||||||
onDragOverBlock?: (e: React.DragEvent, index: number) => void;
|
onDragOverBlock?: (e: React.DragEvent, index: number) => void;
|
||||||
onDropBlock?: (index: number) => void;
|
onDropBlock?: (index: number) => void;
|
||||||
isDragTarget?: boolean;
|
isDragTarget?: boolean;
|
||||||
|
isTargeted?: boolean;
|
||||||
registerTextareaRef: (blockId: string, el: HTMLTextAreaElement | null) => void;
|
registerTextareaRef: (blockId: string, el: HTMLTextAreaElement | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
onDragOverBlock,
|
onDragOverBlock,
|
||||||
onDropBlock,
|
onDropBlock,
|
||||||
isDragTarget,
|
isDragTarget,
|
||||||
|
isTargeted,
|
||||||
registerTextareaRef,
|
registerTextareaRef,
|
||||||
}) => {
|
}) => {
|
||||||
const [localContent, setLocalContent] = useState<string>(block.content || '');
|
const [localContent, setLocalContent] = useState<string>(block.content || '');
|
||||||
@@ -439,9 +441,11 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group relative flex items-start gap-1 py-1 rounded-card transition-colors ${
|
className={`group relative flex items-start gap-1 py-1 rounded-card transition-all duration-300 ${
|
||||||
isDragTarget
|
isDragTarget
|
||||||
? 'border-t-2 border-accent bg-accent/10'
|
? 'border-t-2 border-accent bg-accent/10'
|
||||||
|
: isTargeted
|
||||||
|
? 'bg-accent/15 ring-2 ring-accent'
|
||||||
: isFocused
|
: isFocused
|
||||||
? 'bg-panel-elevated'
|
? 'bg-panel-elevated'
|
||||||
: 'hover:bg-panel'
|
: 'hover:bg-panel'
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface OutlinerTreeProps {
|
|||||||
onMoveBlock: (blockId: string, newIndex: number, level?: number) => void;
|
onMoveBlock: (blockId: string, newIndex: number, level?: number) => void;
|
||||||
onTagClick?: (tag: string) => void;
|
onTagClick?: (tag: string) => void;
|
||||||
onWikiLinkClick?: (pageTitle: string) => void;
|
onWikiLinkClick?: (pageTitle: string) => void;
|
||||||
|
initialFocusedBlockId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
||||||
@@ -34,14 +35,38 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
onMoveBlock,
|
onMoveBlock,
|
||||||
onTagClick,
|
onTagClick,
|
||||||
onWikiLinkClick,
|
onWikiLinkClick,
|
||||||
|
initialFocusedBlockId,
|
||||||
}) => {
|
}) => {
|
||||||
const [focusedBlockId, setFocusedBlockId] = useState<string | null>(null);
|
const [focusedBlockId, setFocusedBlockId] = useState<string | null>(null);
|
||||||
|
const [highlightedBlockId, setHighlightedBlockId] = useState<string | null>(
|
||||||
|
initialFocusedBlockId || null
|
||||||
|
);
|
||||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const textareaMapRef = useRef<Map<string, HTMLTextAreaElement>>(new Map());
|
const textareaMapRef = useRef<Map<string, HTMLTextAreaElement>>(new Map());
|
||||||
const pendingFocusRef = useRef<{ blockId: string; cursor: 'start' | 'end' } | null>(null);
|
const pendingFocusRef = useRef<{ blockId: string; cursor: 'start' | 'end' } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialFocusedBlockId) {
|
||||||
|
setHighlightedBlockId(initialFocusedBlockId);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
focusBlockInput(initialFocusedBlockId, 'end');
|
||||||
|
const el = textareaMapRef.current.get(initialFocusedBlockId);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
}, 80);
|
||||||
|
const fadeTimer = setTimeout(() => {
|
||||||
|
setHighlightedBlockId(null);
|
||||||
|
}, 2500);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
clearTimeout(fadeTimer);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [initialFocusedBlockId]);
|
||||||
|
|
||||||
const registerTextareaRef = useCallback((blockId: string, el: HTMLTextAreaElement | null) => {
|
const registerTextareaRef = useCallback((blockId: string, el: HTMLTextAreaElement | null) => {
|
||||||
if (el) {
|
if (el) {
|
||||||
textareaMapRef.current.set(blockId, el);
|
textareaMapRef.current.set(blockId, el);
|
||||||
@@ -304,6 +329,7 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
onDragOverBlock={handleDragOverBlock}
|
onDragOverBlock={handleDragOverBlock}
|
||||||
onDropBlock={handleDropBlock}
|
onDropBlock={handleDropBlock}
|
||||||
isDragTarget={dragOverIndex === index && draggedIndex !== index}
|
isDragTarget={dragOverIndex === index && draggedIndex !== index}
|
||||||
|
isTargeted={highlightedBlockId === block.id}
|
||||||
registerTextareaRef={registerTextareaRef}
|
registerTextareaRef={registerTextareaRef}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -2404,11 +2404,18 @@ func (s *Store) GetWorkspaceTags(userID string) ([]domain.TagSummary, error) {
|
|||||||
JOIN team_members tm ON tm.team_id = dts2.team_id
|
JOIN team_members tm ON tm.team_id = dts2.team_id
|
||||||
WHERE tm.user_id = ?
|
WHERE tm.user_id = ?
|
||||||
) dts ON d.id = dts.doc_id
|
) dts ON d.id = dts.doc_id
|
||||||
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL)
|
LEFT JOIN workspace_shared_folders wsf ON d.shared_folder_id = wsf.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT wfs2.folder_id
|
||||||
|
FROM workspace_folder_shares wfs2
|
||||||
|
WHERE (wfs2.target_type = 'user' AND wfs2.target_id = ?)
|
||||||
|
OR (wfs2.target_type = 'team' AND wfs2.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||||
|
) wfs ON d.shared_folder_id = wfs.folder_id
|
||||||
|
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL OR (wsf.id IS NOT NULL AND (wsf.owner_id = ? OR wfs.folder_id IS NOT NULL)))
|
||||||
AND d.is_archived = 0
|
AND d.is_archived = 0
|
||||||
GROUP BY t.tag
|
GROUP BY t.tag
|
||||||
ORDER BY count DESC, t.tag ASC`,
|
ORDER BY count DESC, t.tag ASC`,
|
||||||
userID, userID, userID, userID,
|
userID, userID, userID, userID, userID, userID, userID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2460,7 +2467,7 @@ func (s *Store) GetTagDetails(userID, tag string) (*domain.TagDetails, error) {
|
|||||||
rows, err := s.db.Query(
|
rows, err := s.db.Query(
|
||||||
`SELECT t.block_id, d.id, d.title, d.icon, d.folder_path, d.blocks_json, d.updated_at, d.user_id,
|
`SELECT t.block_id, d.id, d.title, d.icon, d.folder_path, d.blocks_json, d.updated_at, d.user_id,
|
||||||
COALESCE(u.display_name, '') as owner_name,
|
COALESCE(u.display_name, '') as owner_name,
|
||||||
COALESCE(s.permission, dts.permission, '') as shared_permission
|
COALESCE(s.permission, dts.permission, wfs.permission, '') as shared_permission
|
||||||
FROM block_tags t
|
FROM block_tags t
|
||||||
JOIN documents d ON t.document_id = d.id
|
JOIN documents d ON t.document_id = d.id
|
||||||
LEFT JOIN users u ON d.user_id = u.id
|
LEFT JOIN users u ON d.user_id = u.id
|
||||||
@@ -2471,11 +2478,18 @@ func (s *Store) GetTagDetails(userID, tag string) (*domain.TagDetails, error) {
|
|||||||
JOIN team_members tm ON tm.team_id = dts2.team_id
|
JOIN team_members tm ON tm.team_id = dts2.team_id
|
||||||
WHERE tm.user_id = ?
|
WHERE tm.user_id = ?
|
||||||
) dts ON d.id = dts.doc_id
|
) dts ON d.id = dts.doc_id
|
||||||
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL)
|
LEFT JOIN workspace_shared_folders wsf ON d.shared_folder_id = wsf.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT wfs2.folder_id, wfs2.permission
|
||||||
|
FROM workspace_folder_shares wfs2
|
||||||
|
WHERE (wfs2.target_type = 'user' AND wfs2.target_id = ?)
|
||||||
|
OR (wfs2.target_type = 'team' AND wfs2.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||||
|
) wfs ON d.shared_folder_id = wfs.folder_id
|
||||||
|
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL OR (wsf.id IS NOT NULL AND (wsf.owner_id = ? OR wfs.folder_id IS NOT NULL)))
|
||||||
AND t.tag = ?
|
AND t.tag = ?
|
||||||
AND d.is_archived = 0
|
AND d.is_archived = 0
|
||||||
ORDER BY d.updated_at DESC`,
|
ORDER BY d.updated_at DESC`,
|
||||||
userID, userID, userID, userID, cleanTag,
|
userID, userID, userID, userID, userID, userID, userID, cleanTag,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
Reference in new issue
Block a user