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:
1 parent
75aea4a7d4
commit
39a04082fc
15 files changed
+293
-11
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mindsift-frontend",
|
||||
"version": "0.51.0",
|
||||
"version": "0.52.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mindsift-frontend",
|
||||
"version": "0.51.0",
|
||||
"version": "0.52.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "5.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mindsift-frontend",
|
||||
"private": true,
|
||||
"version": "0.51.0",
|
||||
"version": "0.52.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -207,7 +207,7 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
|
||||
</div>
|
||||
<div className="p-1.5 bg-panel rounded border border-border flex items-center gap-1.5 text-main col-span-2">
|
||||
<span className="text-accent font-bold">📤</span>
|
||||
<span>/extract oder /notiz (Punkt mit allen Unterpunkten in eine eigene Notiz auslagern, an seiner Stelle bleibt ein [[Link]])</span>
|
||||
<span>/extract oder /notiz (Punkt mit allen Unterpunkten in eine eigene Notiz auslagern, an seiner Stelle bleibt ein [[Link]]; der Knopf „Notiz zurückholen“ am Link macht das wieder rückgängig)</span>
|
||||
</div>
|
||||
<div className="p-1.5 bg-panel rounded border border-border flex items-center gap-1.5 text-main col-span-2">
|
||||
<span className="text-accent font-bold">🎙️</span>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
Mic,
|
||||
AudioLines,
|
||||
FileOutput,
|
||||
FileInput,
|
||||
Link2,
|
||||
Type,
|
||||
} from 'lucide-react';
|
||||
@@ -96,6 +97,7 @@ export interface OutlinerBlockProps {
|
||||
onZoomBlock?: (blockId: string) => void;
|
||||
/** Turns this bullet and its children into a note of its own */
|
||||
onExtractBlock?: (blockId: string) => void;
|
||||
onInlineBlock?: (blockId: string) => void;
|
||||
onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void;
|
||||
onCreateBlockBelow: (afterBlockId: string, newContent?: string) => void;
|
||||
onDeleteBlock: (blockId: string, deleteSubtree?: boolean) => void;
|
||||
@@ -151,6 +153,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
levelOffset = 0,
|
||||
onZoomBlock,
|
||||
onExtractBlock,
|
||||
onInlineBlock,
|
||||
onUpdateBlock,
|
||||
onCreateBlockBelow,
|
||||
onDeleteBlock,
|
||||
@@ -1724,6 +1727,19 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* The way back: a bullet that only links to a note gets that note's bullets again */}
|
||||
{canEdit && onInlineBlock && isNoteLinkOnly(localContent) && !mediaPicker && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onInlineBlock(block.id)}
|
||||
className="p-1 sm:p-0.5 min-w-[28px] min-h-[28px] sm:min-w-0 sm:min-h-0 flex items-center justify-center text-faint hover:text-accent rounded-btn transition-colors cursor-pointer"
|
||||
title="Notiz zurückholen (ihre Punkte kommen wieder hierher, die Notiz in den Papierkorb)"
|
||||
aria-label="Notiz zurückholen"
|
||||
>
|
||||
<FileInput className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Zoom / Focus button */}
|
||||
{onZoomBlock && (
|
||||
<button
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface OutlinerTreeProps {
|
||||
levelOffset?: number;
|
||||
onZoomBlock?: (blockId: string) => void;
|
||||
onExtractBlock?: (blockId: string) => void;
|
||||
onInlineBlock?: (blockId: string) => void;
|
||||
onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void;
|
||||
onUpdateBlocks?: (updated: DocumentBlock[]) => void;
|
||||
onCreateBlock: (newBlock: DocumentBlock, afterBlockId?: string) => void;
|
||||
@@ -54,6 +55,7 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
|
||||
levelOffset = 0,
|
||||
onZoomBlock,
|
||||
onExtractBlock,
|
||||
onInlineBlock,
|
||||
onUpdateBlock,
|
||||
onUpdateBlocks,
|
||||
onCreateBlock,
|
||||
@@ -933,6 +935,7 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
|
||||
levelOffset={levelOffset}
|
||||
onZoomBlock={onZoomBlock}
|
||||
onExtractBlock={onExtractBlock}
|
||||
onInlineBlock={onInlineBlock}
|
||||
onUpdateBlock={onUpdateBlock}
|
||||
onCreateBlockBelow={handleCreateBlockBelow}
|
||||
onPasteMultiLines={handlePasteMultiLines}
|
||||
|
||||
@@ -799,6 +799,11 @@ export const api = {
|
||||
});
|
||||
},
|
||||
|
||||
/** The way back from extractBranch: the linked note's blocks for the bullet; the note goes to the trash. */
|
||||
async inlineNote(docId: string, blockId: string): Promise<{ sourceId: string; content: string; blocks: DocumentBlock[] }> {
|
||||
return request(`/api/v1/documents/${docId}/inline`, { method: 'POST', body: JSON.stringify({ blockId }) });
|
||||
},
|
||||
|
||||
/** Puts a note created by extractBranch into the trash again. */
|
||||
async undoExtract(newDocId: string): Promise<void> {
|
||||
await request(`/api/v1/documents/${newDocId}/extract/undo`, { method: 'POST' });
|
||||
|
||||
Reference in new issue
Block a user