feat(outliner): indent and outdent entire subtree with all children on Tab and Shift+Tab
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s

This commit is contained in:
sandrodach committed 2026-10-27 16:06:31 +01:00
1 parent 1bdd7d245d
commit aea05e868a
2 files changed
+57 -11

No files matched your search

@@ -1029,6 +1029,25 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
[effectiveCanEdit, isLiveEnabled, sendBlockUpdated]
);
const handleUpdateBlocks = useCallback(
(updatedBlocks: DocumentBlock[]) => {
if (!effectiveCanEdit || updatedBlocks.length === 0) return;
const map = new Map<string, DocumentBlock>();
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<DocumentEditorProps> = ({
remoteCursors={remoteCursors}
sendCursorMoved={sendCursorMoved}
onUpdateBlock={handleUpdateBlock}
onUpdateBlocks={handleUpdateBlocks}
onCreateBlock={handleCreateBlock}
onDeleteBlock={handleDeleteBlock}
onMoveBlock={handleMoveBlock}
@@ -1599,6 +1619,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
remoteCursors={remoteCursors}
sendCursorMoved={sendCursorMoved}
onUpdateBlock={handleUpdateBlock}
onUpdateBlocks={handleUpdateBlocks}
onCreateBlock={handleCreateBlock}
onDeleteBlock={handleDeleteBlock}
onMoveBlock={handleMoveBlock}
@@ -13,6 +13,7 @@ export interface OutlinerTreeProps {
remoteCursors?: Record<string, RemoteCursor>;
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<OutlinerTreeProps> = ({
remoteCursors,
sendCursorMoved,
onUpdateBlock,
onUpdateBlocks,
onCreateBlock,
onDeleteBlock,
onMoveBlock,
@@ -172,7 +174,7 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
}
return result;
}, [blocks]);
}, [blocks, levelOffset]);
const handleCreateBlockBelow = useCallback(
(afterBlockId: string, _currentContent: string) => {
@@ -241,9 +243,9 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
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<OutlinerTreeProps> = ({
if (nextLevel === currentLevel) return;
onUpdateBlock(
{
...current,
level: nextLevel,
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);
}
}
},
true
);
},
[blocks, canEdit, onUpdateBlock]
[blocks, canEdit, levelOffset, onUpdateBlock, onUpdateBlocks]
);
const handleNavigateFocus = useCallback(