Files
plane/web/ee/store/timeline/utils.ts
rahulramesha e181f7ae24 [WEB-2442] feat: Timeline dependencies (#1631)
* fix build

* Timeline Dependency changes

* fix build error
2024-10-30 14:03:10 +05:30

520 lines
18 KiB
TypeScript

import set from "lodash/set";
import isNil from "lodash/isNil";
// Plane
import { TIssueRelationMap } from "@plane/types";
// components
import { ChartDataType, IGanttBlock } from "@/components/gantt-chart";
import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants";
import { getItemPositionWidth } from "@/components/gantt-chart/views";
// Plane-web
import { EDependencyPosition, ETimelineRelation } from "@/plane-web/constants";
import { DependencyTree, Relation, TIssueRelationTypes } from "@/plane-web/types";
/**
* Dependency details for each relation ship type, Used while building dependency tree
*/
const TIMELINE_DEPENDENCIES: {
[key in TIssueRelationTypes]?: {
dependencyIndicator: 1 | -1;
parentDependency: EDependencyPosition;
childDependency: EDependencyPosition;
};
} = {
blocked_by: {
dependencyIndicator: -1,
parentDependency: EDependencyPosition.START,
childDependency: EDependencyPosition.END,
},
blocking: {
dependencyIndicator: 1,
parentDependency: EDependencyPosition.END,
childDependency: EDependencyPosition.START,
},
start_before: {
dependencyIndicator: 1,
parentDependency: EDependencyPosition.START,
childDependency: EDependencyPosition.START,
},
start_after: {
dependencyIndicator: -1,
parentDependency: EDependencyPosition.START,
childDependency: EDependencyPosition.START,
},
finish_before: {
dependencyIndicator: 1,
parentDependency: EDependencyPosition.END,
childDependency: EDependencyPosition.END,
},
finish_after: {
dependencyIndicator: -1,
parentDependency: EDependencyPosition.END,
childDependency: EDependencyPosition.END,
},
};
/**
* Map that define each relation type
*/
const RELATION_IDENTIFIER_MAP: {
[key in TIssueRelationTypes]:
| {
originDependencyPosition: EDependencyPosition;
destinationDependencyPosition: EDependencyPosition;
relationType: ETimelineRelation;
isParentOriginBlock: boolean;
}
| undefined;
} = {
blocked_by: {
originDependencyPosition: EDependencyPosition.END,
destinationDependencyPosition: EDependencyPosition.START,
relationType: ETimelineRelation.FS,
isParentOriginBlock: false,
},
blocking: {
originDependencyPosition: EDependencyPosition.END,
destinationDependencyPosition: EDependencyPosition.START,
relationType: ETimelineRelation.FS,
isParentOriginBlock: true,
},
start_before: {
originDependencyPosition: EDependencyPosition.START,
destinationDependencyPosition: EDependencyPosition.START,
relationType: ETimelineRelation.SS,
isParentOriginBlock: true,
},
start_after: {
originDependencyPosition: EDependencyPosition.START,
destinationDependencyPosition: EDependencyPosition.START,
relationType: ETimelineRelation.SS,
isParentOriginBlock: false,
},
finish_before: {
originDependencyPosition: EDependencyPosition.END,
destinationDependencyPosition: EDependencyPosition.END,
relationType: ETimelineRelation.FF,
isParentOriginBlock: true,
},
finish_after: {
originDependencyPosition: EDependencyPosition.END,
destinationDependencyPosition: EDependencyPosition.END,
relationType: ETimelineRelation.FF,
isParentOriginBlock: false,
},
duplicate: undefined,
relates_to: undefined,
};
/**
* This method returns a map of Relation calculated from based on relationShip
* @param affectedBlockIds Block Ids for which the relations are calculated
* @param allBlockIds All the block Ids on the Timeline charts
* @param blocksMap Map of Blocks
* @param relationsMap Map of relationship from the relation store
* @returns
*/
export function getNewRelationsMap(
affectedBlockIds: string[],
allBlockIds: string[],
blocksMap: Record<string, IGanttBlock>,
relationsMap: TIssueRelationMap
): Record<string, Relation> {
const newRelationsMap: Record<string, Relation> = {};
// loop through all the affected block Ids
affectedBlockIds.forEach((blockId) => {
const currBlockIndex = allBlockIds.indexOf(blockId);
const currBlock = blocksMap[blockId];
const relation = relationsMap[blockId];
const isCurrBlockVisible = !!currBlock?.start_date && !!currBlock?.target_date;
// if the blocks has no relations, and block not visible on the chart then return
if (!relation || currBlockIndex < 0 || !isCurrBlockVisible) return;
const relationKeys = Object.keys(relation);
// Loop through all the relation ship types available for the current block
for (const relationKey of relationKeys) {
const relatingIds = relation[relationKey as TIssueRelationTypes];
const currRelationData = RELATION_IDENTIFIER_MAP[relationKey as TIssueRelationTypes];
// if there are no related block Ids of the type the return
if (!relatingIds || !Array.isArray(relatingIds) || !currRelationData) continue;
const { originDependencyPosition, destinationDependencyPosition, relationType, isParentOriginBlock } =
currRelationData;
// Loop through all the related block Ids of the current relation
for (const blockId of relatingIds) {
const relatingBlock = blocksMap[blockId];
const relatingBlockIndex = allBlockIds.indexOf(blockId);
const isRelatingBlockVisible = !!relatingBlock?.start_date && !!relatingBlock?.target_date;
// if block data does not exist and block not visible on the chart then return
if (!relatingBlock || relatingBlockIndex < 0 || !isRelatingBlockVisible) continue;
let originBlock, destinationBlock, originBlockIndex, destinationBlockIndex;
// Determine the originating block and destination block
if (isParentOriginBlock) {
originBlock = currBlock;
destinationBlock = relatingBlock;
originBlockIndex = currBlockIndex;
destinationBlockIndex = relatingBlockIndex;
} else {
originBlock = relatingBlock;
destinationBlock = currBlock;
originBlockIndex = relatingBlockIndex;
destinationBlockIndex = currBlockIndex;
}
const relationId = `${originBlock.id}_${destinationBlock.id}`;
// Calculate Start and end co-ordinates of the relation
const startX =
originDependencyPosition === EDependencyPosition.START
? (originBlock.position?.marginLeft ?? 0)
: (originBlock.position?.marginLeft ?? 0) + (originBlock.position?.width ?? 0);
const endX =
destinationDependencyPosition === EDependencyPosition.START
? (destinationBlock.position?.marginLeft ?? 0)
: (destinationBlock.position?.marginLeft ?? 0) + (destinationBlock.position?.width ?? 0);
const startPosition = {
x: startX,
y: (originBlockIndex + 0.5) * BLOCK_HEIGHT,
};
const endPosition = {
x: endX,
y: (destinationBlockIndex + 0.5) * BLOCK_HEIGHT,
};
const relationObject: Relation = {
id: relationId,
startPosition,
endPosition,
originDependencyPosition,
destinationDependencyPosition,
originBlock: originBlock.id,
destinationBlock: destinationBlock.id,
relationType,
isAdhering: endPosition.x >= startPosition.x,
};
// Add it to the newRelationsMap
set(newRelationsMap, [relationId], relationObject);
}
}
});
return newRelationsMap;
}
/**
* Build a dependency tree while moving the blocks to see if any of the dependencies are affected
* @param id root Block Id
* @param blocksMap blocks map
* @param relationsMap relationship Map
* @param chartDataType current chart data
* @param ignoreDependencies if true just return the root block's data
* @param processedMap Map to keep track of all the blocks that are already processed to avoid cyclical dependencies
* @returns
*/
export function buildDependencyTree(
id: string,
blocksMap: Record<string, IGanttBlock>,
relationsMap: TIssueRelationMap,
chartDataType: ChartDataType,
ignoreDependencies = false,
processedMap: { [key: string]: true } = {}
): DependencyTree | undefined {
const currBlock = blocksMap[id];
if (!currBlock) return;
// get original positions based on block dates
const originalPosition = getItemPositionWidth(chartDataType, currBlock);
const originalValues = {
[EDependencyPosition.START]: originalPosition?.marginLeft ?? 0,
[EDependencyPosition.END]: (originalPosition?.marginLeft ?? 0) + (originalPosition?.width ?? 0),
};
processedMap[id] = true;
// get child dependencies for the block
const childDependencies = ignoreDependencies
? []
: getDependencyChildren(id, blocksMap, chartDataType, originalValues, relationsMap, processedMap);
return {
dependencyId: id,
[EDependencyPosition.START]: currBlock.position?.marginLeft ?? 0,
[EDependencyPosition.END]: (currBlock.position?.marginLeft ?? 0) + (currBlock.position?.width ?? 0),
originalValues,
dependencies: childDependencies,
};
}
/**
* get all the dependent children for the block
* This is a recursive method that is done with breadth first approach
* @param id parent block
* @param blocksMap blocksMap
* @param chartDataType current chart data
* @param parentOriginalValues original positions of the parent block before drg
* @param relationsMap relation Map
* @param processedMap Map to keep track of all the blocks that are already processed to avoid cyclical dependencies
* @returns
*/
const getDependencyChildren = (
id: string,
blocksMap: Record<string, IGanttBlock>,
chartDataType: ChartDataType,
parentOriginalValues: { [key in EDependencyPosition]: number },
relationsMap: TIssueRelationMap,
processedMap: { [key: string]: true } = {}
) => {
const currRelations = relationsMap[id];
const dependencies: DependencyTree[] = [];
if (!currRelations) return [];
const timeLineRelations = Object.keys(TIMELINE_DEPENDENCIES) as TIssueRelationTypes[];
// loop through all the available timeline relations
for (const timeLineRelation of timeLineRelations) {
const currRelation = currRelations[timeLineRelation];
if (currRelation && Array.isArray(currRelation)) {
// loop through all the children relation blockIds
for (const relationBlockId of currRelation) {
const currBlock = blocksMap[relationBlockId];
if (!currBlock || processedMap[relationBlockId]) continue;
const originalPosition = getItemPositionWidth(chartDataType, currBlock);
// Add the children dependencies to the dependencies list
dependencies.push({
...(timeLineRelation && TIMELINE_DEPENDENCIES[timeLineRelation]),
dependencyId: relationBlockId,
[EDependencyPosition.START]: currBlock.position?.marginLeft ?? 0,
[EDependencyPosition.END]: (currBlock.position?.marginLeft ?? 0) + (currBlock.position?.width ?? 0),
originalValues: {
[EDependencyPosition.START]: originalPosition?.marginLeft ?? 0,
[EDependencyPosition.END]: (originalPosition?.marginLeft ?? 0) + (originalPosition?.width ?? 0),
},
dependencies: [],
});
}
}
}
// sort all the dependencies, based on how close the dependent block is to the parent block
dependencies.sort((a: DependencyTree, b: DependencyTree) => {
const aDistance = getChildDependencyDistance(a, parentOriginalValues);
const bDistance = getChildDependencyDistance(b, parentOriginalValues);
return aDistance - bDistance;
});
// loop through the children to add their children through recursion
for (const dependency of dependencies) {
processedMap[dependency.dependencyId] = true;
dependency.dependencies = getDependencyChildren(
dependency.dependencyId,
blocksMap,
chartDataType,
dependency.originalValues,
relationsMap,
processedMap
);
}
return dependencies;
};
/**
* Get the origin block's position to the child block's position
* @param childDependencyTree
* @param parentOriginalValues
* @returns
*/
const getChildDependencyDistance = (
childDependencyTree: DependencyTree,
parentOriginalValues: { [key in EDependencyPosition]: number }
) => {
const { parentDependency, childDependency, originalValues } = childDependencyTree;
if (!childDependency || !parentDependency) return 0;
return Math.abs(originalValues[childDependency] - parentOriginalValues[parentDependency]);
};
/**
* Get all the position updates of the blocks while dragging based on dependencyTree
* @param dependencyTree dependency tree
* @param deltaLeft number
* @param deltaWidth number
* @returns
*/
export const getBlockUpdates = (
dependencyTree: DependencyTree,
deltaLeft: number,
deltaWidth: number
): { blockId: string; marginLeft: number; width?: number }[] => {
const currMarginLeft = dependencyTree[EDependencyPosition.START] + deltaLeft;
const currWidth = dependencyTree[EDependencyPosition.END] - dependencyTree[EDependencyPosition.START] + deltaWidth;
// Add all the current block to the block updates
const currBlockUpdate: { blockId: string; marginLeft: number; width?: number } = {
blockId: dependencyTree.dependencyId,
marginLeft: currMarginLeft,
width: currWidth,
};
let dependencyBlockUpdates = [currBlockUpdate];
// If it contains dependencies then calculate updates for all the dependencies
if (
dependencyTree.dependencies &&
Array.isArray(dependencyTree.dependencies) &&
dependencyTree.dependencies.length > 0
) {
const currOGValues = {
[EDependencyPosition.START]: dependencyTree[EDependencyPosition.START],
[EDependencyPosition.END]: dependencyTree[EDependencyPosition.END],
};
const currUpdatedValues = {
[EDependencyPosition.START]: currMarginLeft,
[EDependencyPosition.END]: currMarginLeft + currWidth,
};
// loop through to get all the block updates
for (const dependency of dependencyTree.dependencies) {
const currBlockUpdates = getChildBlockUpdates(dependency, currOGValues, currUpdatedValues);
dependencyBlockUpdates = [...dependencyBlockUpdates, ...currBlockUpdates];
}
}
return dependencyBlockUpdates;
};
/**
* Recursive method to get all children block updates
* @param dependencyTree
* @param OGValues
* @param updatedValues
* @returns
*/
const getChildBlockUpdates = (
dependencyTree: DependencyTree,
OGValues: { [EDependencyPosition.START]: number; [EDependencyPosition.END]: number },
updatedValues: { [EDependencyPosition.START]: number; [EDependencyPosition.END]: number }
): { blockId: string; marginLeft: number; width?: number }[] => {
const { dependencyId, parentDependency, childDependency, dependencyIndicator, originalValues, dependencies } =
dependencyTree;
// return if there is no dependents
if (!parentDependency || !childDependency || !dependencyIndicator) return [];
const OGParentValue = OGValues[parentDependency];
const updatedParentValue = updatedValues[parentDependency];
const childDependentValue = dependencyTree[childDependency];
const OGDelta = (OGParentValue - childDependentValue) * dependencyIndicator;
const updatedDelta = (updatedParentValue - childDependentValue) * dependencyIndicator;
const currBlockMovement =
(dependencyTree[EDependencyPosition.START] - originalValues[EDependencyPosition.START]) * dependencyIndicator;
// Main logic which determines if the current block is affected by the parent block's new position
if (currBlockMovement <= 0 && (OGDelta > 0 || updatedDelta < 0)) return [];
const comparatorMethod = dependencyIndicator > 0 ? Math.max : Math.min;
// block update of the child block
const currBlockUpdate = {
blockId: dependencyId,
marginLeft: comparatorMethod(
originalValues[EDependencyPosition.START],
dependencyTree[EDependencyPosition.START] + updatedDelta * dependencyIndicator
),
};
let dependencyBlockUpdates = [currBlockUpdate];
// loop through all the child dependencies and call getChildBlockUpdates recursively
if (dependencies && Array.isArray(dependencies) && dependencies.length > 0) {
const currOGValues = {
[EDependencyPosition.START]: dependencyTree[EDependencyPosition.START],
[EDependencyPosition.END]: dependencyTree[EDependencyPosition.END],
};
const currUpdatedValues = {
[EDependencyPosition.START]: dependencyTree[EDependencyPosition.START] + updatedDelta * dependencyIndicator,
[EDependencyPosition.END]: dependencyTree[EDependencyPosition.END] + updatedDelta * dependencyIndicator,
};
for (const dependency of dependencies) {
const currBlockUpdates = getChildBlockUpdates(dependency, currOGValues, currUpdatedValues);
dependencyBlockUpdates = [...dependencyBlockUpdates, ...currBlockUpdates];
}
}
return dependencyBlockUpdates;
};
/**
* Get Relation type based on origin and destination position
* @param draggedFromPosition
* @param draggedOnPosition
* @returns
*/
export function getRelationType(
draggedFromPosition: EDependencyPosition,
draggedOnPosition: EDependencyPosition
): TIssueRelationTypes | undefined {
if (draggedFromPosition === EDependencyPosition.START) {
return draggedOnPosition === EDependencyPosition.START ? "start_before" : undefined;
} else {
return draggedOnPosition === EDependencyPosition.START ? "blocking" : "finish_before";
}
}
/**
* Return position of block
* @param blocksMap
* @param blockIds
* @param blockId
* @param dependencyPosition
* @returns
*/
export const getPositionOfBlock = (
blocksMap: Record<string, IGanttBlock>,
blockIds: string[] | undefined,
blockId: string,
dependencyPosition: EDependencyPosition
) => {
const blockedByBlock = blocksMap[blockId];
const blockedByIndex = blockIds?.indexOf(blockId);
if (!blockedByBlock || isNil(blockedByIndex) || blockedByIndex < 0) return undefined;
const position = {
x: blockedByBlock.position?.marginLeft ?? 0,
y: (blockedByIndex + 0.5) * BLOCK_HEIGHT,
};
if (dependencyPosition === EDependencyPosition.END) {
position.x += blockedByBlock.position?.width ?? 0;
}
return position;
};