Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save the column positions #2453

Merged
merged 25 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 2 additions & 35 deletions src/components/graph/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,9 @@ function compressTreePlacements(nodes: CurrentTreeNode[], placements: PlacementG
// to the maximum column placement values (per row) of the nodes on the left.
// The resulting space we find represents how much we can shift the current column to the left.

const currentNodeFamilySize = 1 + countNodes(nodes, currentNodeId as UUID);
const currentSubTreeSize = 1 + countNodes(nodes, currentNodeId as UUID);
const indexOfCurrentNode = nodes.findIndex((n) => n.id === currentNodeId);
const nodesOfTheCurrentBranch = nodes.slice(indexOfCurrentNode, indexOfCurrentNode + currentNodeFamilySize);
const nodesOfTheCurrentBranch = nodes.slice(indexOfCurrentNode, indexOfCurrentNode + currentSubTreeSize);
const currentBranchMinimumColumnByRow = getMinimumColumnByRows(nodesOfTheCurrentBranch, placements);

// We have to compare with all the left nodes, not only the current branch's left neighbor, because in some
Expand Down Expand Up @@ -343,39 +343,6 @@ export function getFirstAncestorWithSibling(
return undefined;
}

/**
* Will find the sibling node whose X position is closer to xDestination in the X range provided.
*/
export function findClosestSiblingInRange(
nodes: CurrentTreeNode[],
node: CurrentTreeNode,
xOrigin: number,
xDestination: number
): CurrentTreeNode | null {
const minX = Math.min(xOrigin, xDestination);
const maxX = Math.max(xOrigin, xDestination);
const siblingNodes = findSiblings(nodes, node);
const nodesBetween = siblingNodes.filter((n) => n.position.x < maxX && n.position.x > minX);
if (nodesBetween.length > 0) {
const closestNode = nodesBetween.reduce(
(closest, current) =>
Math.abs(current.position.x - xDestination) < Math.abs(closest.position.x - xDestination)
? current
: closest,
nodesBetween[0]
);
return closestNode;
}
return null;
}

/**
* Will find the siblings of a provided node (all siblings have the same parent).
*/
function findSiblings(nodes: CurrentTreeNode[], node: CurrentTreeNode): CurrentTreeNode[] {
return nodes.filter((n) => n.parentId === node.parentId && n.id !== node.id);
}

/**
* Computes the absolute position of a node by calculating the sum of all the relative positions of
* the node's lineage.
Expand Down
158 changes: 45 additions & 113 deletions src/components/graph/network-modification-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { BUILD_STATUS } from '../network/constants';
import { UUID } from 'crypto';
import { CurrentTreeNode, isReactFlowRootNodeData } from '../../redux/reducer';
import { Edge } from '@xyflow/react';
import { NetworkModificationNodeData, RootNodeData } from './tree-node.type';
import { AbstractNode, NetworkModificationNodeData, RootNodeData } from './tree-node.type';

// Function to count children nodes for a given parentId recursively in an array of nodes.
// TODO refactoring when changing NetworkModificationTreeModel as it becomes an object containing nodes
Expand All @@ -30,119 +30,54 @@ export default class NetworkModificationTreeModel {

isAnyNodeBuilding = false;

/**
* Will switch the order of two nodes in the tree.
* The nodeToMove will be moved, either to the left or right of the destinationNode, depending
* on their initial positions.
* Both nodes should have the same parent.
*/
switchSiblingsOrder(nodeToMove: CurrentTreeNode, destinationNode: CurrentTreeNode) {
if (!nodeToMove.parentId || nodeToMove.parentId !== destinationNode.parentId) {
console.error('Both nodes should have the same parent to switch their order');
return;
}
const nodeToMoveIndex = this.treeNodes.findIndex((node) => node.id === nodeToMove.id);
const destinationNodeIndex = this.treeNodes.findIndex((node) => node.id === destinationNode.id);

const numberOfNodesToMove: number = 1 + countNodes(this.treeNodes, nodeToMove.id);
const nodesToMove = this.treeNodes.splice(nodeToMoveIndex, numberOfNodesToMove);

if (nodeToMoveIndex > destinationNodeIndex) {
this.treeNodes.splice(destinationNodeIndex, 0, ...nodesToMove);
} else {
// When moving nodeToMove to the right, we have to take into account the splice function that changed the nodes' indexes.
// We also need to find the correct position of nodeToMove, to the right of the destination node, meaning we need to find
// how many children the destination node has and add all of them to the new index.
const destinationNodeIndexAfterSplice = this.treeNodes.findIndex((node) => node.id === destinationNode.id);
const destinationNodeFamilySize: number = 1 + countNodes(this.treeNodes, destinationNode.id);
this.treeNodes.splice(destinationNodeIndexAfterSplice + destinationNodeFamilySize, 0, ...nodesToMove);
// Will sort if columnPosition is defined, and not move the nodes if undefined
childrenNodeSorter(a: AbstractNode, b: AbstractNode) {
if (a.columnPosition !== undefined && b.columnPosition !== undefined) {
return a.columnPosition - b.columnPosition;
}

this.treeNodes = [...this.treeNodes];
return 0;
}

/**
* Finds the lowest common ancestor of two nodes in the tree.
*
* Example tree:
* A
* / \
* B D
* / / \
* C E F
*
* Examples:
* - getCommonAncestor(B, E) will return A
* - getCommonAncestor(E, F) will return D
*/
getCommonAncestor(nodeA: CurrentTreeNode, nodeB: CurrentTreeNode): CurrentTreeNode | null {
const getAncestors = (node: CurrentTreeNode) => {
const ancestors = [];
let current: CurrentTreeNode | undefined = node;
while (current && current.parentId) {
const parentId: string = current.parentId;
ancestors.push(parentId);
current = this.treeNodes.find((n) => n.id === parentId);
}
return ancestors;
};
// We get the entire ancestors of one of the nodes in an array, then iterate over the other node's ancestors
// until we find a node that is in the first array : this common node is an ancestor of both intial nodes.
const ancestorsA: string[] = getAncestors(nodeA);
let current: CurrentTreeNode | undefined = nodeB;
while (current && current.parentId) {
const parentId: string = current.parentId;
current = this.treeNodes.find((n) => n.id === parentId);
if (current && ancestorsA.includes(current.id)) {
return current;
}
}
console.warn('No common ancestor found !');
return null;
getChildren(parentNodeId: string): CurrentTreeNode[] {
return this.treeNodes.filter((n) => n.parentId === parentNodeId);
}

/**
* Finds the child of the ancestor node that is on the path to the descendant node.
*
* Example tree:
* A
* / \
* B D
* / / \
* C E F
*
* Examples:
* - getChildOfAncestorInLineage(A, E) will return D
* - getChildOfAncestorInLineage(D, F) will return F
*
* @param ancestor node, must be an ancestor of descendant node
* @param descendant node, must be a descendant of ancestor
* @returns The child of the ancestor node in the lineage or null if not found.
* @private
* Will reorganize treeNodes and put the children of parentNodeId in the order provided in nodeIds array.
* @param parentNodeId parent ID of the to be reordered children nodes
* @param orderedNodeIds array of children ID in the order we want
* @returns true if the order was changed
*/
getChildOfAncestorInLineage(ancestor: CurrentTreeNode, descendant: CurrentTreeNode): CurrentTreeNode | null {
let current: CurrentTreeNode | undefined = descendant;
while (current && current.parentId) {
const parentId: string = current.parentId;
if (parentId === ancestor.id) {
return current;
}
current = this.treeNodes.find((n) => n.id === parentId);
reorderChildrenNodes(parentNodeId: string, orderedNodeIds: string[]) {
// We check if the current position is already correct
const children = this.getChildren(parentNodeId);
if (orderedNodeIds.length !== children.length) {
console.warn('reorderNodes : synchronization error, reorder cancelled');
return false;
}
console.warn('The ancestor and descendant do not share the same branch !');
return null;
}

switchBranches(nodeToMove: CurrentTreeNode, destinationNode: CurrentTreeNode) {
// We find the nodes from the two branches that share the same parent
const commonAncestor = this.getCommonAncestor(nodeToMove, destinationNode);
if (commonAncestor) {
const siblingFromNodeToMoveBranch = this.getChildOfAncestorInLineage(commonAncestor, nodeToMove);
const siblingFromDestinationNodeBranch = this.getChildOfAncestorInLineage(commonAncestor, destinationNode);
if (siblingFromNodeToMoveBranch && siblingFromDestinationNodeBranch) {
this.switchSiblingsOrder(siblingFromNodeToMoveBranch, siblingFromDestinationNodeBranch);
}
if (children.map((child) => child.id).join(',') === orderedNodeIds.join(',')) {
// Alreay in the same order.
EstherDarkish marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
// Let's reorder the children :
// We create a map of children node ids and number of nodes in each of these child's subtree,
// then in nodeIds order, we cut and paste the corresponding number of nodes in treeNodes.
const justAfterParentIndex = 1 + this.treeNodes.findIndex((n) => n.id === parentNodeId); // we add 1 here to set the index just after the parent node
let insertedNodes = 0;
const nodeIdAndCorrespondingSubTreeSize = new Map(
EstherDarkish marked this conversation as resolved.
Show resolved Hide resolved
orderedNodeIds.map((id) => [
id,
1 + countNodes(this.treeNodes, id as UUID), // We add 1 here to include the current node in its subtree size
])
);

nodeIdAndCorrespondingSubTreeSize.forEach((subTreeSize, nodeId) => {
const nodesToMoveIndex = this.treeNodes.findIndex((n) => n.id === nodeId);
const nodesToMove = this.treeNodes.splice(nodesToMoveIndex, subTreeSize);
this.treeNodes.splice(justAfterParentIndex + insertedNodes, 0, ...nodesToMove);
insertedNodes += subTreeSize;
});
return true;
}

addChild(
Expand Down Expand Up @@ -224,7 +159,7 @@ export default class NetworkModificationTreeModel {
});

// overwrite old children nodes parentUuid when inserting new nodes
const nextNodes = this.treeNodes.map((node) => {
this.treeNodes = this.treeNodes.map((node) => {
if (newNode.childrenIds.includes(node.id)) {
return {
...node,
Expand All @@ -233,14 +168,13 @@ export default class NetworkModificationTreeModel {
}
return node;
});

this.treeNodes = nextNodes;
this.treeEdges = filteredEdges;
}

if (!skipChildren) {
// Add children of this node recursively
if (newNode.children) {
newNode.children.sort(this.childrenNodeSorter);
newNode.children.forEach((child) => {
this.addChild(child, newNode.id, undefined, undefined);
});
Expand Down Expand Up @@ -279,7 +213,7 @@ export default class NetworkModificationTreeModel {
if (!nodeToDelete) {
return;
}
const nextTreeNodes = filteredNodes.map((node) => {
this.treeNodes = filteredNodes.map((node) => {
if (node.parentId === nodeId) {
return {
...node,
Expand All @@ -288,8 +222,6 @@ export default class NetworkModificationTreeModel {
}
return node;
});

this.treeNodes = nextTreeNodes;
});
}

Expand Down Expand Up @@ -321,6 +253,7 @@ export default class NetworkModificationTreeModel {
// handle root node
this.treeNodes.push(convertNodetoReactFlowModelNode(elements));
// handle root children
elements.children.sort(this.childrenNodeSorter);
elements.children.forEach((child) => {
this.addChild(child, elements.id);
});
Expand All @@ -329,8 +262,7 @@ export default class NetworkModificationTreeModel {

newSharedForUpdate() {
/* shallow clone of the network https://stackoverflow.com/a/44782052 */
let newTreeModel = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
return newTreeModel;
return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
}

setBuildingStatus() {
Expand Down
1 change: 1 addition & 0 deletions src/components/graph/tree-node.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type AbstractNode = {
readOnly?: boolean;
reportUuid?: UUID;
type: NodeType;
columnPosition?: number;
};

export interface NodeBuildStatus {
Expand Down
9 changes: 9 additions & 0 deletions src/components/network-modification-tree-pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
networkModificationHandleSubtree,
setNodeSelectionForCopy,
resetLogsFilter,
reorderNetworkModificationTreeNodes,
} from '../redux/actions';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -214,6 +215,14 @@ export const NetworkModificationTreePane = ({ studyUuid, studyMapTreeDisplay })
);
}
);
} else if (studyUpdatedForce.eventData.headers['updateType'] === 'nodesColumnPositionsChanged') {
const orderedChildrenNodeIds = JSON.parse(studyUpdatedForce.eventData.payload);
dispatch(
reorderNetworkModificationTreeNodes(
studyUpdatedForce.eventData.headers['parentNode'],
orderedChildrenNodeIds
)
);
} else if (studyUpdatedForce.eventData.headers['updateType'] === 'nodeMoved') {
fetchNetworkModificationTreeNode(studyUuid, studyUpdatedForce.eventData.headers['movedNode']).then(
(node) => {
Expand Down
Loading
Loading