Dev #7
@@ -812,6 +812,87 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
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<string>();
|
||||
|
||||
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<WorkspaceViewProps> = ({
|
||||
{/* Matched Document Blocks */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-[10px] font-bold uppercase tracking-wider text-faint">
|
||||
Verknüpfte Outliner-Blöcke & Unterpunkte ({tagDetails?.totalMatches || 0})
|
||||
Verknüpfte Outliner-Blöcke & Unterpunkte ({activeTagTotalMatches})
|
||||
</h3>
|
||||
{tagDetails?.documents.map((docGroup) => (
|
||||
<div
|
||||
key={docGroup.docId}
|
||||
className="p-3 rounded-card border border-border bg-canvas space-y-2"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleJumpToDocBlock(docGroup.docId)}
|
||||
className="flex items-center gap-2 text-xs font-semibold text-accent hover:underline cursor-pointer"
|
||||
>
|
||||
<span>{docGroup.icon || '📝'}</span>
|
||||
<span>{docGroup.title}</span>
|
||||
{docGroup.folderPath && (
|
||||
<span className="badge-neutral text-[10px] font-normal">
|
||||
📁 {docGroup.folderPath}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<div className="space-y-2 pl-3 border-l-2 border-border/70">
|
||||
{docGroup.matchedBlocks.map((mb) => (
|
||||
<div key={mb.rootBlock.id} className="space-y-1">
|
||||
{/* Root Bullet Point containing the hashtag */}
|
||||
<div
|
||||
onClick={() => 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 ? (
|
||||
<div className="p-8 text-center text-muted text-xs space-y-1 border border-border rounded-card bg-canvas">
|
||||
<p className="font-semibold text-main">Keine Blöcke mit #{selectedTag} gefunden.</p>
|
||||
</div>
|
||||
) : (
|
||||
activeTagGroups.map((docGroup) => {
|
||||
const docObj = docs.find((d) => d.id === docGroup.docId);
|
||||
return (
|
||||
<div
|
||||
key={docGroup.docId}
|
||||
className="p-3 rounded-card border border-border bg-canvas space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleJumpToDocBlock(docGroup.docId)}
|
||||
className="flex items-center gap-2 text-xs font-semibold text-accent hover:underline cursor-pointer"
|
||||
>
|
||||
<span className="text-accent font-bold select-none">•</span>
|
||||
<span className="flex-1 break-words">{mb.rootBlock.content}</span>
|
||||
</div>
|
||||
<span>{docGroup.icon || '📝'}</span>
|
||||
<span>{docGroup.title}</span>
|
||||
{docGroup.folderPath && (
|
||||
<span className="badge-neutral text-[10px] font-normal">
|
||||
📁 {docGroup.folderPath}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleJumpToDocBlock(docGroup.docId)}
|
||||
className="text-[10px] text-accent hover:underline px-1.5 py-0.5 rounded-btn hover:bg-canvas cursor-pointer flex items-center gap-1"
|
||||
title="Notiz öffnen"
|
||||
>
|
||||
<span>Notiz öffnen</span>
|
||||
<ExternalLink className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* All Children indented */}
|
||||
{mb.children && mb.children.length > 0 && (
|
||||
<div className="space-y-0.5">
|
||||
{mb.children.map((ch) => {
|
||||
const relLevel = Math.max(
|
||||
1,
|
||||
(ch.level || 0) - (mb.rootBlock.level || 0)
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={ch.id}
|
||||
onClick={() => 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' ? (
|
||||
<span
|
||||
className={`w-3.5 h-3.5 mt-0.5 checkbox-square flex items-center justify-center shrink-0 border ${
|
||||
ch.checked
|
||||
? 'bg-status-ok border-status-ok text-on-accent'
|
||||
: 'border-strong text-transparent'
|
||||
}`}
|
||||
>
|
||||
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-faint group-hover:text-accent font-bold select-none">
|
||||
◦
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`flex-1 break-words ${
|
||||
ch.checked ? 'line-through text-faint' : ''
|
||||
<div className="space-y-2 pl-3 border-l-2 border-border/70">
|
||||
{docGroup.matchedBlocks.map((mb) => (
|
||||
<div key={mb.rootBlock.id} className="space-y-1">
|
||||
{/* Root Bullet Point containing the hashtag */}
|
||||
<div
|
||||
onClick={() => 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"
|
||||
>
|
||||
<div className="flex items-start gap-2 flex-1 min-w-0">
|
||||
{mb.rootBlock.type === 'todo' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (docObj) {
|
||||
handleToggleRollupTodo(
|
||||
docObj,
|
||||
mb.rootBlock.id,
|
||||
Boolean(mb.rootBlock.checked)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className={`w-3.5 h-3.5 mt-0.5 checkbox-square flex items-center justify-center shrink-0 border cursor-pointer ${
|
||||
mb.rootBlock.checked
|
||||
? 'bg-status-ok border-status-ok text-on-accent'
|
||||
: 'border-strong hover:border-accent text-transparent'
|
||||
}`}
|
||||
>
|
||||
{ch.content}
|
||||
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-accent font-bold select-none text-sm leading-none mt-0.5">
|
||||
•
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
<span
|
||||
className={`flex-1 break-words ${
|
||||
mb.rootBlock.checked ? 'line-through text-faint' : ''
|
||||
}`}
|
||||
>
|
||||
{mb.rootBlock.content}
|
||||
</span>
|
||||
</div>
|
||||
<ExternalLink className="w-3 h-3 text-faint group-hover:text-accent opacity-0 group-hover:opacity-100 transition-opacity shrink-0 mt-0.5" />
|
||||
</div>
|
||||
|
||||
{/* All Children indented */}
|
||||
{mb.children && mb.children.length > 0 && (
|
||||
<div className="space-y-0.5">
|
||||
{mb.children.map((ch) => {
|
||||
const relLevel = Math.max(
|
||||
1,
|
||||
(ch.level || 0) - (mb.rootBlock.level || 0)
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={ch.id}
|
||||
onClick={() => 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"
|
||||
>
|
||||
<div className="flex items-start gap-2 flex-1 min-w-0">
|
||||
{ch.type === 'todo' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (docObj) {
|
||||
handleToggleRollupTodo(
|
||||
docObj,
|
||||
ch.id,
|
||||
Boolean(ch.checked)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className={`w-3.5 h-3.5 mt-0.5 checkbox-square flex items-center justify-center shrink-0 border cursor-pointer ${
|
||||
ch.checked
|
||||
? 'bg-status-ok border-status-ok text-on-accent'
|
||||
: 'border-strong hover:border-accent text-transparent'
|
||||
}`}
|
||||
>
|
||||
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-faint group-hover:text-accent font-bold select-none text-xs leading-none mt-1">
|
||||
◦
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`flex-1 break-words ${
|
||||
ch.checked ? 'line-through text-faint' : ''
|
||||
}`}
|
||||
>
|
||||
{ch.content}
|
||||
</span>
|
||||
</div>
|
||||
<ExternalLink className="w-3 h-3 text-faint group-hover:text-accent opacity-0 group-hover:opacity-100 transition-opacity shrink-0 mt-0.5" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user