feat(workspace): links to single bullets
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m8s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m8s
- copy a link to a bullet from its action bubble or pick a bullet in the link dialog; [[Note#^id|Note › excerpt]] opens the note, jumps to and highlights the bullet, or says it is gone - wiki links render inline with their label and are clickable; the duplicate chip below the text is gone test(e2e): run each suite with its own container, image tag and port, so parallel runs from another working copy no longer remove our server Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d408e7d307
commit
1a49e6b304
12 files changed
+284
-45
No files matched your search
@@ -8,9 +8,12 @@ import {
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
Plus,
|
||||
ArrowLeft,
|
||||
ListTree,
|
||||
} from 'lucide-react';
|
||||
import { api } from '../../lib/api';
|
||||
import { Document } from '../../lib/types';
|
||||
import { Document, DocumentBlock } from '../../lib/types';
|
||||
import { buildBlockLink } from '../../lib/wikiLink';
|
||||
|
||||
export interface LinkModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -100,6 +103,37 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
);
|
||||
}, [docs, search]);
|
||||
|
||||
// Picking a single bullet of a note (link to that bullet)
|
||||
const [pickDoc, setPickDoc] = useState<Document | null>(null);
|
||||
const [pickBlocks, setPickBlocks] = useState<DocumentBlock[] | null>(null);
|
||||
const [blockSearch, setBlockSearch] = useState('');
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setPickDoc(null);
|
||||
setPickBlocks(null);
|
||||
setBlockSearch('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
const openBlockPicker = (doc: Document) => {
|
||||
setPickDoc(doc);
|
||||
setBlockSearch('');
|
||||
if (doc.blocks && doc.blocks.length > 0) {
|
||||
setPickBlocks(doc.blocks);
|
||||
return;
|
||||
}
|
||||
setPickBlocks(null);
|
||||
api
|
||||
.getDocument(doc.id)
|
||||
.then((full) => setPickBlocks(full.blocks || []))
|
||||
.catch(() => setPickBlocks([]));
|
||||
};
|
||||
const pickableBlocks = useMemo(() => {
|
||||
const q = blockSearch.trim().toLowerCase();
|
||||
return (pickBlocks || []).filter(
|
||||
(b) => b.content.trim() !== '' && b.type !== 'divider' && (!q || b.content.toLowerCase().includes(q))
|
||||
);
|
||||
}, [pickBlocks, blockSearch]);
|
||||
|
||||
const handleSelectNote = (doc: { title: string }) => {
|
||||
const noteTitle = doc.title.trim() || 'Notiz';
|
||||
onInsertLink(`[[${noteTitle}]]`);
|
||||
@@ -244,8 +278,59 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bullets of one note: link to a single bullet */}
|
||||
{tab === 'note' && pickDoc && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPickDoc(null)}
|
||||
className="btn-icon h-11 w-11 sm:h-7 sm:w-7 shrink-0"
|
||||
title="Zurück zur Notizliste"
|
||||
aria-label="Zurück zur Notizliste"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<span className="text-xs font-semibold text-main truncate">
|
||||
Punkt in „{pickDoc.title || 'Unbenannte Notiz'}“ wählen
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={blockSearch}
|
||||
onChange={(e) => setBlockSearch(e.target.value)}
|
||||
placeholder="Punkte filtern…"
|
||||
className="input-field w-full h-11 sm:h-8 px-2.5 text-xs"
|
||||
aria-label="Punkte filtern"
|
||||
/>
|
||||
<div className="space-y-0.5 max-h-64 overflow-y-auto pr-0.5">
|
||||
{pickBlocks === null ? (
|
||||
<div className="p-4 text-xs text-muted flex items-center gap-2 justify-center">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-accent" />
|
||||
<span>Punkte werden geladen…</span>
|
||||
</div>
|
||||
) : pickableBlocks.length === 0 ? (
|
||||
<div className="p-4 text-xs text-muted text-center">Keine passenden Punkte.</div>
|
||||
) : (
|
||||
pickableBlocks.map((b) => (
|
||||
<button
|
||||
key={b.id}
|
||||
type="button"
|
||||
onClick={() => onInsertLink(buildBlockLink(pickDoc.title || 'Notiz', b.id, b.content))}
|
||||
className="w-full text-left min-h-[44px] sm:min-h-0 py-1.5 pr-2 rounded-btn text-xs text-main hover:bg-panel-elevated hover:text-accent flex items-start gap-1.5 cursor-pointer"
|
||||
style={{ paddingLeft: `${0.5 + Math.min(b.level || 0, 5) * 0.875}rem` }}
|
||||
>
|
||||
<span className="text-faint shrink-0">•</span>
|
||||
<span className="line-clamp-2 break-words">{b.content}</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Note List Section (Matching Active Shares list in UnifiedShareModal) */}
|
||||
{tab === 'note' && (
|
||||
{tab === 'note' && !pickDoc && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs font-semibold text-muted uppercase tracking-wider px-0.5">
|
||||
<span>Verfügbare Notizen</span>
|
||||
@@ -289,13 +374,27 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-action text-xs h-7 px-2.5 opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-1 text-accent cursor-pointer shrink-0"
|
||||
>
|
||||
<span>Einfügen</span>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openBlockPicker(d as Document);
|
||||
}}
|
||||
className="btn-icon h-11 w-11 sm:h-7 sm:w-7 text-muted hover:text-accent"
|
||||
title="Einen einzelnen Punkt dieser Notiz verlinken"
|
||||
aria-label="Punkt dieser Notiz wählen"
|
||||
>
|
||||
<ListTree className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-action text-xs h-7 px-2.5 opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-1 text-accent cursor-pointer"
|
||||
>
|
||||
<span>Einfügen</span>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user