diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index db89a42..f0b1d79 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -812,6 +812,87 @@ export const WorkspaceView: React.FC = ({ return count; }, [docs]); + // Real-time matched blocks and all nested children for the selected hashtag + const activeTagGroups = useMemo(() => { + if (!selectedTag) return []; + const cleanTag = selectedTag.toLowerCase(); + + const map = new Map< + string, + { + docId: string; + title: string; + icon: string; + folderPath: string; + matchedBlocks: { rootBlock: DocumentBlock; children: DocumentBlock[] }[]; + } + >(); + + for (const d of docs) { + const blks = d.blocks || []; + if (blks.length === 0) continue; + + const matched: { rootBlock: DocumentBlock; children: DocumentBlock[] }[] = []; + const processedChildIds = new Set(); + + for (let i = 0; i < blks.length; i++) { + const b = blks[i]; + if (processedChildIds.has(b.id)) continue; + + const content = b.content || ''; + const regex = new RegExp(`(?:^|[\\s(])#(${cleanTag})(?=[\\s.,!?:;)#\\]}]|$)`, 'i'); + if (regex.test(content)) { + const children: DocumentBlock[] = []; + const rootLevel = b.level || 0; + for (let j = i + 1; j < blks.length; j++) { + const nextB = blks[j]; + if ((nextB.level || 0) > rootLevel) { + children.push(nextB); + processedChildIds.add(nextB.id); + } else { + break; + } + } + matched.push({ rootBlock: b, children }); + } + } + + if (matched.length > 0) { + map.set(d.id, { + docId: d.id, + title: d.title || 'Unbenannte Notiz', + icon: d.icon || '📝', + folderPath: d.folderPath || '', + matchedBlocks: matched, + }); + } + } + + if (tagDetails?.documents) { + for (const dg of tagDetails.documents) { + if (!map.has(dg.docId)) { + map.set(dg.docId, { + docId: dg.docId, + title: dg.title || 'Unbenannte Notiz', + icon: dg.icon || '📝', + folderPath: dg.folderPath || '', + matchedBlocks: dg.matchedBlocks || [], + }); + } + } + } + + return Array.from(map.values()); + }, [docs, selectedTag, tagDetails]); + + const activeTagTotalMatches = useMemo(() => { + let count = 0; + for (const g of activeTagGroups) { + count += g.matchedBlocks.length; + } + return count; + }, [activeTagGroups]); + const selectedDocObj = useMemo( () => docs.find((d) => d.id === selectedDocId) || null, [docs, selectedDocId] @@ -1143,87 +1224,156 @@ export const WorkspaceView: React.FC = ({ {/* Matched Document Blocks */}

- Verknüpfte Outliner-Blöcke & Unterpunkte ({tagDetails?.totalMatches || 0}) + Verknüpfte Outliner-Blöcke & Unterpunkte ({activeTagTotalMatches})

- {tagDetails?.documents.map((docGroup) => ( -
- -
- {docGroup.matchedBlocks.map((mb) => ( -
- {/* Root Bullet Point containing the hashtag */} -
handleJumpToDocBlock(docGroup.docId, mb.rootBlock.id)} - className="flex items-start gap-1.5 text-xs text-main font-medium group cursor-pointer hover:bg-panel-elevated p-1 rounded-btn transition-colors" - title="Klick: Zu dieser Zeile springen" + {activeTagGroups.length === 0 ? ( +
+

Keine Blöcke mit #{selectedTag} gefunden.

+
+ ) : ( + activeTagGroups.map((docGroup) => { + const docObj = docs.find((d) => d.id === docGroup.docId); + return ( +
+
+
+ {docGroup.icon || '📝'} + {docGroup.title} + {docGroup.folderPath && ( + + 📁 {docGroup.folderPath} + + )} + + +
- {/* All Children indented */} - {mb.children && mb.children.length > 0 && ( -
- {mb.children.map((ch) => { - const relLevel = Math.max( - 1, - (ch.level || 0) - (mb.rootBlock.level || 0) - ); - return ( -
handleJumpToDocBlock(docGroup.docId, ch.id)} - style={{ paddingLeft: `${relLevel * 16}px` }} - className="flex items-start gap-1.5 text-xs text-muted hover:text-main group cursor-pointer hover:bg-panel-elevated p-1 rounded-btn transition-colors" - title="Klick: Zu dieser Zeile springen" - > - {ch.type === 'todo' ? ( - - - - ) : ( - - ◦ - - )} - + {docGroup.matchedBlocks.map((mb) => ( +
+ {/* Root Bullet Point containing the hashtag */} +
handleJumpToDocBlock(docGroup.docId, mb.rootBlock.id)} + className="flex items-start justify-between gap-1.5 text-xs text-main font-medium group cursor-pointer hover:bg-panel-elevated p-1.5 rounded-btn transition-colors" + title="Klick: Zu dieser Zeile springen und markieren" + > +
+ {mb.rootBlock.type === 'todo' ? ( + + ) : ( + + • -
- ); - })} + )} + + {mb.rootBlock.content} + +
+ +
+ + {/* All Children indented */} + {mb.children && mb.children.length > 0 && ( +
+ {mb.children.map((ch) => { + const relLevel = Math.max( + 1, + (ch.level || 0) - (mb.rootBlock.level || 0) + ); + return ( +
handleJumpToDocBlock(docGroup.docId, ch.id)} + style={{ paddingLeft: `${relLevel * 16}px` }} + className="flex items-start justify-between gap-1.5 text-xs text-muted hover:text-main group cursor-pointer hover:bg-panel-elevated p-1 rounded-btn transition-colors" + title="Klick: Zu dieser Zeile springen und markieren" + > +
+ {ch.type === 'todo' ? ( + + ) : ( + + ◦ + + )} + + {ch.content} + +
+ +
+ ); + })} +
+ )}
- )} + ))}
- ))} -
-
- ))} +
+ ); + }) + )}