@accounter/client
Version:
Accounter client application
149 lines (137 loc) • 5.87 kB
text/typescript
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
import {
getDescendantIds,
isFinancialEntityNode,
type CustomData,
type FlatNode,
} from './types.js';
export type DragPayload = {
nodeId: string;
sourceTreeId: 'bank' | 'report';
};
/**
* Applies a Pragmatic DnD tree-item Instruction to the flat node arrays.
*
* @param bankTree Current bank flat nodes
* @param reportTree Current report flat nodes
* @param payload Drag payload attached to the draggable element
* @param targetNodeId Id of the node the item was dropped on (or tree root id)
* @param targetTreeId Which panel received the drop ('bank' | 'report')
* @param instruction Tree-item hitbox instruction — or null for root-level drops
*
* @returns { nextBankTree, nextReportTree }
*/
export function handleCrossTreeDrop(
bankTree: FlatNode<CustomData>[],
reportTree: FlatNode<CustomData>[],
payload: DragPayload,
targetNodeId: string,
targetTreeId: 'bank' | 'report',
instruction: Instruction | null,
): { nextBankTree: FlatNode<CustomData>[]; nextReportTree: FlatNode<CustomData>[] } {
const sourceTree = payload.sourceTreeId === 'bank' ? bankTree : reportTree;
const targetTree = targetTreeId === 'bank' ? bankTree : reportTree;
// Guard: dragging onto self
if (
targetNodeId === payload.nodeId ||
(payload.sourceTreeId === targetTreeId &&
getDescendantIds(sourceTree, payload.nodeId).includes(targetNodeId))
) {
return { nextBankTree: bankTree, nextReportTree: reportTree };
}
const draggedNode = sourceTree.find(n => n.id === payload.nodeId);
if (!draggedNode) return { nextBankTree: bankTree, nextReportTree: reportTree };
// canDrop guard: cannot make a financial-entity a child
if (instruction?.type === 'make-child') {
const targetNode = targetTree.find(n => n.id === targetNodeId);
if (targetNode && isFinancialEntityNode(targetNode)) {
return { nextBankTree: bankTree, nextReportTree: reportTree };
}
}
// Collect all ids to move (dragged node + all descendants)
const movedIds = new Set([payload.nodeId, ...getDescendantIds(sourceTree, payload.nodeId)]);
const movedNodes = sourceTree.filter(n => movedIds.has(n.id));
const isSameTree = payload.sourceTreeId === targetTreeId;
// After removing moved nodes from the source, what remains
const prunedSourceTree = sourceTree.filter(n => !movedIds.has(n.id));
// The base for building the target (deduped: if same tree, use pruned)
const baseTarget = isSameTree ? prunedSourceTree : targetTree;
let nextTargetTree: FlatNode<CustomData>[];
if (!instruction || instruction.type === 'instruction-blocked') {
// Append at root level of the target tree
const updatedMoved = movedNodes.map(n =>
n.id === payload.nodeId ? { ...n, parent: targetTreeId } : n,
);
nextTargetTree = [...baseTarget, ...updatedMoved];
} else if (instruction.type === 'make-child') {
const updatedMoved = movedNodes.map(n =>
n.id === payload.nodeId ? { ...n, parent: targetNodeId } : n,
);
nextTargetTree = [...baseTarget, ...updatedMoved];
} else if (instruction.type === 'reorder-above') {
const targetNode = baseTarget.find(n => n.id === targetNodeId);
const newParent = targetNode?.parent ?? targetTreeId;
const updatedMoved = movedNodes.map(n =>
n.id === payload.nodeId ? { ...n, parent: newParent } : n,
);
const targetIndex = baseTarget.findIndex(n => n.id === targetNodeId);
if (targetIndex === -1) {
nextTargetTree = [...baseTarget, ...updatedMoved];
} else {
nextTargetTree = [
...baseTarget.slice(0, targetIndex),
...updatedMoved,
...baseTarget.slice(targetIndex),
];
}
} else if (instruction.type === 'reorder-below') {
const targetNode = baseTarget.find(n => n.id === targetNodeId);
const newParent = targetNode?.parent ?? targetTreeId;
const updatedMoved = movedNodes.map(n =>
n.id === payload.nodeId ? { ...n, parent: newParent } : n,
);
const targetIndex = baseTarget.findIndex(n => n.id === targetNodeId);
if (targetIndex === -1) {
nextTargetTree = [...baseTarget, ...updatedMoved];
} else {
nextTargetTree = [
...baseTarget.slice(0, targetIndex + 1),
...updatedMoved,
...baseTarget.slice(targetIndex + 1),
];
}
} else if (instruction.type === 'reparent') {
// Walk up from targetNodeId by (currentLevel - desiredLevel) steps to find the new parent.
// Stop early if we reach a root sentinel or the node is not found, to prevent over-walking.
const ROOT_SENTINELS = new Set(['bank', 'report']);
const levelsUp = instruction.currentLevel - instruction.desiredLevel;
let ancestorId: string = targetNodeId;
for (let i = 0; i < levelsUp; i++) {
if (ROOT_SENTINELS.has(ancestorId)) break;
const ancestor = baseTarget.find(n => n.id === ancestorId);
if (!ancestor) break;
ancestorId = ancestor.parent;
}
const updatedMoved = movedNodes.map(n =>
n.id === payload.nodeId ? { ...n, parent: ancestorId } : n,
);
const targetIndex = baseTarget.findIndex(n => n.id === targetNodeId);
nextTargetTree = [
...baseTarget.slice(0, targetIndex + 1),
...updatedMoved,
...baseTarget.slice(targetIndex + 1),
];
} else {
nextTargetTree = [...baseTarget, ...movedNodes];
}
if (isSameTree) {
if (targetTreeId === 'bank') {
return { nextBankTree: nextTargetTree, nextReportTree: reportTree };
}
return { nextBankTree: bankTree, nextReportTree: nextTargetTree };
}
if (payload.sourceTreeId === 'bank') {
return { nextBankTree: prunedSourceTree, nextReportTree: nextTargetTree };
}
return { nextBankTree: nextTargetTree, nextReportTree: prunedSourceTree };
}