fix(outliner): move entire subtree with all children during drag and drop
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m4s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m4s
This commit is contained in:
1 parent
266026da4c
commit
1bdd7d245d
2 files changed
+94
-12
No files matched your search
@@ -1072,22 +1072,60 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleMoveBlock = useCallback(
|
const handleMoveBlock = useCallback(
|
||||||
(blockId: string, newIndex: number, level?: number) => {
|
(blockId: string, targetIndex: number, newLevel?: number) => {
|
||||||
if (!effectiveCanEdit) return;
|
if (!effectiveCanEdit) return;
|
||||||
setBlocks((prev) => {
|
setBlocks((prev) => {
|
||||||
const fromIdx = prev.findIndex((b) => b.id === blockId);
|
const fromIdx = prev.findIndex((b) => b.id === blockId);
|
||||||
if (fromIdx === -1) return prev;
|
if (fromIdx === -1) return prev;
|
||||||
|
|
||||||
|
const parentBlock = prev[fromIdx];
|
||||||
|
const parentLevel = Math.max(0, parentBlock.level || 0);
|
||||||
|
|
||||||
|
// Calculate full subtree count (dragged block + all contiguous descendants)
|
||||||
|
let count = 1;
|
||||||
|
for (let i = fromIdx + 1; i < prev.length; i++) {
|
||||||
|
if (Math.max(0, prev[i].level || 0) > parentLevel) {
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent dropping inside its own subtree
|
||||||
|
if (targetIndex >= fromIdx && targetIndex < fromIdx + count) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
const copy = [...prev];
|
const copy = [...prev];
|
||||||
const [moving] = copy.splice(fromIdx, 1);
|
const subtree = copy.splice(fromIdx, count);
|
||||||
const updatedMoving: DocumentBlock = {
|
|
||||||
...moving,
|
// Adjust insertion index because removing elements from before shifts target index
|
||||||
level: level !== undefined ? level : moving.level,
|
let insertIndex = targetIndex;
|
||||||
};
|
if (fromIdx < targetIndex) {
|
||||||
const boundedIdx = Math.max(0, Math.min(newIndex, copy.length));
|
insertIndex = targetIndex - count;
|
||||||
copy.splice(boundedIdx, 0, updatedMoving);
|
}
|
||||||
|
insertIndex = Math.max(0, Math.min(insertIndex, copy.length));
|
||||||
|
|
||||||
|
// Calculate level adjustments if moving to a constrained position
|
||||||
|
const prevBlockInPosition = insertIndex > 0 ? copy[insertIndex - 1] : null;
|
||||||
|
const maxAllowed = prevBlockInPosition
|
||||||
|
? Math.max(0, prevBlockInPosition.level || 0) + 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const targetLevel = newLevel !== undefined ? newLevel : parentLevel;
|
||||||
|
const clampedLevel = Math.min(targetLevel, maxAllowed);
|
||||||
|
const levelDelta = clampedLevel - parentLevel;
|
||||||
|
|
||||||
|
const updatedSubtree = subtree.map((b) => ({
|
||||||
|
...b,
|
||||||
|
level: Math.max(0, (b.level || 0) + levelDelta),
|
||||||
|
}));
|
||||||
|
|
||||||
|
copy.splice(insertIndex, 0, ...updatedSubtree);
|
||||||
const normalized = ensureNonEmptyBlocks(docId, copy);
|
const normalized = ensureNonEmptyBlocks(docId, copy);
|
||||||
|
|
||||||
if (isLiveEnabled) {
|
if (isLiveEnabled) {
|
||||||
sendBlockMoved(blockId, boundedIdx, updatedMoving.level, normalized);
|
sendBlockMoved(blockId, insertIndex, updatedSubtree[0].level, normalized);
|
||||||
}
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -282,10 +282,29 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
setDraggedIndex(index);
|
setDraggedIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDragOverBlock = useCallback((e: React.DragEvent, index: number) => {
|
const handleDragOverBlock = useCallback(
|
||||||
|
(e: React.DragEvent, index: number) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (draggedIndex !== null) {
|
||||||
|
const parentLevel = Math.max(0, blocks[draggedIndex]?.level || 0);
|
||||||
|
let count = 1;
|
||||||
|
for (let i = draggedIndex + 1; i < blocks.length; i++) {
|
||||||
|
if (Math.max(0, blocks[i].level || 0) > parentLevel) {
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Do not allow dropping inside its own subtree (self or descendants)
|
||||||
|
if (index >= draggedIndex && index < draggedIndex + count) {
|
||||||
|
setDragOverIndex(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setDragOverIndex(index);
|
setDragOverIndex(index);
|
||||||
}, []);
|
},
|
||||||
|
[blocks, draggedIndex]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDropBlock = useCallback(
|
const handleDropBlock = useCallback(
|
||||||
(targetIndex: number) => {
|
(targetIndex: number) => {
|
||||||
@@ -294,6 +313,20 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
setDragOverIndex(null);
|
setDragOverIndex(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const parentLevel = Math.max(0, blocks[draggedIndex]?.level || 0);
|
||||||
|
let count = 1;
|
||||||
|
for (let i = draggedIndex + 1; i < blocks.length; i++) {
|
||||||
|
if (Math.max(0, blocks[i].level || 0) > parentLevel) {
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetIndex >= draggedIndex && targetIndex < draggedIndex + count) {
|
||||||
|
setDraggedIndex(null);
|
||||||
|
setDragOverIndex(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const movingBlock = blocks[draggedIndex];
|
const movingBlock = blocks[draggedIndex];
|
||||||
if (movingBlock) {
|
if (movingBlock) {
|
||||||
onMoveBlock(movingBlock.id, targetIndex, movingBlock.level);
|
onMoveBlock(movingBlock.id, targetIndex, movingBlock.level);
|
||||||
@@ -372,7 +405,18 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="pt-2 pl-2">
|
<div
|
||||||
|
className={`pt-2 pl-2 transition-colors ${
|
||||||
|
dragOverIndex === blocks.length ? 'border-t-2 border-accent bg-accent/10 rounded' : ''
|
||||||
|
}`}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (draggedIndex !== null) {
|
||||||
|
setDragOverIndex(blocks.length);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDrop={() => handleDropBlock(blocks.length)}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
Reference in new issue
Block a user