feat(workspace): bring an extracted note back into the bullet

A bullet that is only a link to a note (as left by /extract) gets a
"Notiz zurückholen" button. POST /documents/{id}/inline returns the
linked note's blocks with fresh ids and trashes the note; the editor
puts the title back as text and inserts the blocks as an own edit, so
Strg+Z restores the link and the note. Only own notes that no other
note links to. Release 0.52.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 20:13:53 +01:00
1 parent 75aea4a7d4
commit 39a04082fc
15 files changed
+293 -11

No files matched your search

@@ -545,7 +545,21 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
// The last extraction, so undoing it (toast or Strg+Z) also puts the new note into the trash
const lastExtractRef = useRef<{ newDocId: string; blockId: string; link: string } | null>(null);
// The last note brought back, so undoing it also takes the note out of the trash again
const lastInlineRef = useRef<{ sourceId: string; blockId: string; content: string } | null>(null);
const finishUndoExtract = useCallback(() => {
const inlined = lastInlineRef.current;
if (inlined) {
const block = currentBlocksRef.current.find((b) => b.id === inlined.blockId);
if (!block || block.content !== inlined.content) {
lastInlineRef.current = null;
api
.restoreDocument(inlined.sourceId)
.then(() => api.getDocument(inlined.sourceId))
.then((restored) => onDocCreated?.(restored))
.catch((err: any) => showNotice(err?.message || 'Notiz konnte nicht wiederhergestellt werden'));
}
}
const info = lastExtractRef.current;
if (!info) return;
const block = currentBlocksRef.current.find((b) => b.id === info.blockId);
@@ -555,7 +569,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
.undoExtract(info.newDocId)
.then(() => onDocRemoved?.(info.newDocId))
.catch((err: any) => showNotice(err?.message || 'Ausgelagerte Notiz konnte nicht entfernt werden'));
}, [onDocRemoved, showNotice]);
}, [onDocCreated, onDocRemoved, showNotice]);
useEffect(() => () => {
if (noticeTimerRef.current !== null) window.clearTimeout(noticeTimerRef.current);
}, []);
@@ -614,6 +628,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
while (end < prev.length && (prev[end].level || 0) > level) end++;
applyLocalBlocks([...prev.slice(0, i), { ...prev[i], content: link }, ...prev.slice(end)]);
}
lastInlineRef.current = null;
lastExtractRef.current = { newDocId: created.id, blockId, link };
onDocCreated?.(created);
showNotice(`In eigene Notiz „${created.title}“ umgewandelt`, [
@@ -627,6 +642,35 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
[docId, onOpenDocBlock, onDocCreated, showNotice, handleUndo, applyLocalBlocks]
);
// The way back: a bullet that only links to a note gets that note's bullets below it again, the note
// goes to the trash. Inserted here as an own edit like extracting, so Strg+Z brings the link back.
const handleInlineBlock = useCallback(
async (blockId: string) => {
const engine = engineRef.current;
if (!engine) return;
try {
await engine.syncNow();
const res = await api.inlineNote(docId, blockId);
(document.activeElement as HTMLElement | null)?.blur?.();
const prev = currentBlocksRef.current;
const i = prev.findIndex((b) => b.id === blockId);
if (i >= 0) {
const level = prev[i].level || 0;
const inserted = res.blocks.map((b) => ({ ...b, level: level + 1 + (b.level || 0) }));
// Above any bullets added below the link meanwhile
applyLocalBlocks([...prev.slice(0, i), { ...prev[i], content: res.content }, ...inserted, ...prev.slice(i + 1)]);
}
lastExtractRef.current = null;
lastInlineRef.current = { sourceId: res.sourceId, blockId, content: res.content };
onDocRemoved?.(res.sourceId);
showNotice('Notiz zurückgeholt, sie liegt jetzt im Papierkorb', [{ label: 'Rückgängig', run: () => handleUndo() }]);
} catch (err: any) {
showNotice(err?.message || 'Zurückholen fehlgeschlagen');
}
},
[docId, onDocRemoved, showNotice, handleUndo, applyLocalBlocks]
);
// Keep parent sidebar metadata in sync when blocks/tags change
useEffect(() => {
if (!docRef.current) return;
@@ -1363,6 +1407,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
levelOffset={levelOffset}
onZoomBlock={setZoomedBlockId}
onExtractBlock={canEdit ? handleExtractBlock : undefined}
onInlineBlock={canEdit ? handleInlineBlock : undefined}
onUpdateBlock={handleUpdateBlock}
onUpdateBlocks={handleUpdateBlocks}
onCreateBlock={handleCreateBlock}
@@ -1513,6 +1558,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
levelOffset={0}
onZoomBlock={setZoomedBlockId}
onExtractBlock={canEdit ? handleExtractBlock : undefined}
onInlineBlock={canEdit ? handleInlineBlock : undefined}
onUpdateBlock={handleUpdateBlock}
onUpdateBlocks={handleUpdateBlocks}
onCreateBlock={handleCreateBlock}