From aea05e868a04e31e6bb9ff4e2bd47c98f3ee65e7 Mon Sep 17 00:00:00 2001 From: Sandro Date: Tue, 27 Oct 2026 16:06:31 +0100 Subject: [PATCH] feat(outliner): indent and outdent entire subtree with all children on Tab and Shift+Tab --- .../src/features/workspace/DocumentEditor.tsx | 21 +++++++++ .../src/features/workspace/OutlinerTree.tsx | 47 ++++++++++++++----- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/frontend/src/features/workspace/DocumentEditor.tsx b/frontend/src/features/workspace/DocumentEditor.tsx index a3c76ff..8ebb4bc 100644 --- a/frontend/src/features/workspace/DocumentEditor.tsx +++ b/frontend/src/features/workspace/DocumentEditor.tsx @@ -1029,6 +1029,25 @@ export const DocumentEditor: React.FC = ({ [effectiveCanEdit, isLiveEnabled, sendBlockUpdated] ); + const handleUpdateBlocks = useCallback( + (updatedBlocks: DocumentBlock[]) => { + if (!effectiveCanEdit || updatedBlocks.length === 0) return; + const map = new Map(); + for (const b of updatedBlocks) { + map.set(b.id, b); + } + setBlocks((prev) => + prev.map((b) => map.get(b.id) || b) + ); + if (isLiveEnabled) { + for (const b of updatedBlocks) { + sendBlockUpdated(b); + } + } + }, + [effectiveCanEdit, isLiveEnabled, sendBlockUpdated] + ); + const handleCreateBlock = useCallback( (newBlock: DocumentBlock, afterBlockId?: string) => { if (!effectiveCanEdit) return; @@ -1531,6 +1550,7 @@ export const DocumentEditor: React.FC = ({ remoteCursors={remoteCursors} sendCursorMoved={sendCursorMoved} onUpdateBlock={handleUpdateBlock} + onUpdateBlocks={handleUpdateBlocks} onCreateBlock={handleCreateBlock} onDeleteBlock={handleDeleteBlock} onMoveBlock={handleMoveBlock} @@ -1599,6 +1619,7 @@ export const DocumentEditor: React.FC = ({ remoteCursors={remoteCursors} sendCursorMoved={sendCursorMoved} onUpdateBlock={handleUpdateBlock} + onUpdateBlocks={handleUpdateBlocks} onCreateBlock={handleCreateBlock} onDeleteBlock={handleDeleteBlock} onMoveBlock={handleMoveBlock} diff --git a/frontend/src/features/workspace/OutlinerTree.tsx b/frontend/src/features/workspace/OutlinerTree.tsx index c3a2d6e..c8657d1 100644 --- a/frontend/src/features/workspace/OutlinerTree.tsx +++ b/frontend/src/features/workspace/OutlinerTree.tsx @@ -13,6 +13,7 @@ export interface OutlinerTreeProps { remoteCursors?: Record; sendCursorMoved?: (blockId: string | null, selectionStart?: number, selectionEnd?: number) => void; onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void; + onUpdateBlocks?: (updated: DocumentBlock[]) => void; onCreateBlock: (newBlock: DocumentBlock, afterBlockId?: string) => void; onDeleteBlock: (blockId: string) => void; onMoveBlock: (blockId: string, newIndex: number, level?: number) => void; @@ -34,6 +35,7 @@ export const OutlinerTree: React.FC = ({ remoteCursors, sendCursorMoved, onUpdateBlock, + onUpdateBlocks, onCreateBlock, onDeleteBlock, onMoveBlock, @@ -172,7 +174,7 @@ export const OutlinerTree: React.FC = ({ } return result; - }, [blocks]); + }, [blocks, levelOffset]); const handleCreateBlockBelow = useCallback( (afterBlockId: string, _currentContent: string) => { @@ -241,9 +243,9 @@ export const OutlinerTree: React.FC = ({ const current = blocks[idx]; const currentLevel = Math.max(0, current.level || 0); const prevLevel = idx > 0 ? Math.max(0, blocks[idx - 1].level || 0) : 0; - const maxLevel = idx === 0 ? 0 : prevLevel + 1; - const minLevel = levelOffset || 0; + const maxLevel = idx === 0 ? minLevel : prevLevel + 1; + const nextLevel = delta === 1 ? Math.min(maxLevel, currentLevel + 1) @@ -251,15 +253,38 @@ export const OutlinerTree: React.FC = ({ if (nextLevel === currentLevel) return; - onUpdateBlock( - { - ...current, - level: nextLevel, - }, - true - ); + const actualDelta = nextLevel - currentLevel; + + // Find all contiguous children / descendants: + // contiguous blocks with level > currentLevel + let count = 1; + for (let i = idx + 1; i < blocks.length; i++) { + if (Math.max(0, blocks[i].level || 0) > currentLevel) { + count++; + } else { + break; + } + } + + const updatedBlocks: DocumentBlock[] = []; + for (let i = idx; i < idx + count; i++) { + const b = blocks[i]; + const newLvl = Math.max(minLevel, (b.level || 0) + actualDelta); + updatedBlocks.push({ + ...b, + level: newLvl, + }); + } + + if (onUpdateBlocks) { + onUpdateBlocks(updatedBlocks); + } else { + for (const b of updatedBlocks) { + onUpdateBlock(b, true); + } + } }, - [blocks, canEdit, onUpdateBlock] + [blocks, canEdit, levelOffset, onUpdateBlock, onUpdateBlocks] ); const handleNavigateFocus = useCallback(