feat(workspace): real-time live child block resolution and interactive checkboxes in hashtag overview
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 18s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 18s
This commit is contained in:
1 parent
149cfdc244
commit
7a8d759bf2
1 file changed
+223
-73
@@ -812,6 +812,87 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
return count;
|
return count;
|
||||||
}, [docs]);
|
}, [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(
|
const selectedDocObj = useMemo(
|
||||||
() => docs.find((d) => d.id === selectedDocId) || null,
|
() => docs.find((d) => d.id === selectedDocId) || null,
|
||||||
[docs, selectedDocId]
|
[docs, selectedDocId]
|
||||||
@@ -1143,87 +1224,156 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{/* Matched Document Blocks */}
|
{/* Matched Document Blocks */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h3 className="text-[10px] font-bold uppercase tracking-wider text-faint">
|
<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>
|
</h3>
|
||||||
{tagDetails?.documents.map((docGroup) => (
|
{activeTagGroups.length === 0 ? (
|
||||||
<div
|
<div className="p-8 text-center text-muted text-xs space-y-1 border border-border rounded-card bg-canvas">
|
||||||
key={docGroup.docId}
|
<p className="font-semibold text-main">Keine Blöcke mit #{selectedTag} gefunden.</p>
|
||||||
className="p-3 rounded-card border border-border bg-canvas space-y-2"
|
</div>
|
||||||
>
|
) : (
|
||||||
<button
|
activeTagGroups.map((docGroup) => {
|
||||||
type="button"
|
const docObj = docs.find((d) => d.id === docGroup.docId);
|
||||||
onClick={() => handleJumpToDocBlock(docGroup.docId)}
|
return (
|
||||||
className="flex items-center gap-2 text-xs font-semibold text-accent hover:underline cursor-pointer"
|
<div
|
||||||
>
|
key={docGroup.docId}
|
||||||
<span>{docGroup.icon || '📝'}</span>
|
className="p-3 rounded-card border border-border bg-canvas space-y-2"
|
||||||
<span>{docGroup.title}</span>
|
>
|
||||||
{docGroup.folderPath && (
|
<div className="flex items-center justify-between">
|
||||||
<span className="badge-neutral text-[10px] font-normal">
|
<button
|
||||||
📁 {docGroup.folderPath}
|
type="button"
|
||||||
</span>
|
onClick={() => handleJumpToDocBlock(docGroup.docId)}
|
||||||
)}
|
className="flex items-center gap-2 text-xs font-semibold text-accent hover:underline cursor-pointer"
|
||||||
</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"
|
|
||||||
>
|
>
|
||||||
<span className="text-accent font-bold select-none">•</span>
|
<span>{docGroup.icon || '📝'}</span>
|
||||||
<span className="flex-1 break-words">{mb.rootBlock.content}</span>
|
<span>{docGroup.title}</span>
|
||||||
</div>
|
{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 */}
|
<div className="space-y-2 pl-3 border-l-2 border-border/70">
|
||||||
{mb.children && mb.children.length > 0 && (
|
{docGroup.matchedBlocks.map((mb) => (
|
||||||
<div className="space-y-0.5">
|
<div key={mb.rootBlock.id} className="space-y-1">
|
||||||
{mb.children.map((ch) => {
|
{/* Root Bullet Point containing the hashtag */}
|
||||||
const relLevel = Math.max(
|
<div
|
||||||
1,
|
onClick={() => handleJumpToDocBlock(docGroup.docId, mb.rootBlock.id)}
|
||||||
(ch.level || 0) - (mb.rootBlock.level || 0)
|
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"
|
||||||
return (
|
>
|
||||||
<div
|
<div className="flex items-start gap-2 flex-1 min-w-0">
|
||||||
key={ch.id}
|
{mb.rootBlock.type === 'todo' ? (
|
||||||
onClick={() => handleJumpToDocBlock(docGroup.docId, ch.id)}
|
<button
|
||||||
style={{ paddingLeft: `${relLevel * 16}px` }}
|
type="button"
|
||||||
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"
|
onClick={(e) => {
|
||||||
title="Klick: Zu dieser Zeile springen"
|
e.stopPropagation();
|
||||||
>
|
if (docObj) {
|
||||||
{ch.type === 'todo' ? (
|
handleToggleRollupTodo(
|
||||||
<span
|
docObj,
|
||||||
className={`w-3.5 h-3.5 mt-0.5 checkbox-square flex items-center justify-center shrink-0 border ${
|
mb.rootBlock.id,
|
||||||
ch.checked
|
Boolean(mb.rootBlock.checked)
|
||||||
? 'bg-status-ok border-status-ok text-on-accent'
|
);
|
||||||
: 'border-strong text-transparent'
|
}
|
||||||
}`}
|
}}
|
||||||
>
|
className={`w-3.5 h-3.5 mt-0.5 checkbox-square flex items-center justify-center shrink-0 border cursor-pointer ${
|
||||||
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
mb.rootBlock.checked
|
||||||
</span>
|
? 'bg-status-ok border-status-ok text-on-accent'
|
||||||
) : (
|
: 'border-strong hover:border-accent text-transparent'
|
||||||
<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' : ''
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user