feat(graph-view): implement task deletion and dependency management enhancements

- Added onDeleteTask functionality to allow task deletion from both board and graph views.
- Integrated delete options for dependencies in the graph view, enhancing user interaction.
- Updated ancestor context section to clarify the role of parent tasks in task descriptions.
- Improved layout handling in graph view to preserve node positions during updates.

This update enhances task management capabilities and improves user experience in the graph view.
This commit is contained in:
James
2025-12-23 20:25:06 -05:00
parent 76b7cfec9e
commit 502043f6de
11 changed files with 546 additions and 55 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import dagre from 'dagre';
import { Node, Edge, useReactFlow } from '@xyflow/react';
import { TaskNode, DependencyEdge } from './use-graph-nodes';
@@ -18,6 +18,10 @@ interface UseGraphLayoutProps {
export function useGraphLayout({ nodes, edges }: UseGraphLayoutProps) {
const { fitView, setNodes } = useReactFlow();
// Cache the last computed positions to avoid recalculating layout
const positionCache = useRef<Map<string, { x: number; y: number }>>(new Map());
const lastStructureKey = useRef<string>('');
const getLayoutedElements = useCallback(
(
inputNodes: TaskNode[],
@@ -48,12 +52,15 @@ export function useGraphLayout({ nodes, edges }: UseGraphLayoutProps) {
const layoutedNodes = inputNodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
const position = {
x: nodeWithPosition.x - NODE_WIDTH / 2,
y: nodeWithPosition.y - NODE_HEIGHT / 2,
};
// Update cache
positionCache.current.set(node.id, position);
return {
...node,
position: {
x: nodeWithPosition.x - NODE_WIDTH / 2,
y: nodeWithPosition.y - NODE_HEIGHT / 2,
},
position,
targetPosition: isHorizontal ? 'left' : 'top',
sourcePosition: isHorizontal ? 'right' : 'bottom',
} as TaskNode;
@@ -64,13 +71,45 @@ export function useGraphLayout({ nodes, edges }: UseGraphLayoutProps) {
[]
);
// Initial layout
// Create a stable structure key based only on node IDs (not edge changes)
// Edges changing shouldn't trigger re-layout
const structureKey = useMemo(() => {
const nodeIds = nodes
.map((n) => n.id)
.sort()
.join(',');
return nodeIds;
}, [nodes]);
// Initial layout - only recalculate when node structure changes (new nodes added/removed)
const layoutedElements = useMemo(() => {
if (nodes.length === 0) {
positionCache.current.clear();
lastStructureKey.current = '';
return { nodes: [], edges: [] };
}
return getLayoutedElements(nodes, edges, 'LR');
}, [nodes, edges, getLayoutedElements]);
// Check if structure changed (new nodes added or removed)
const structureChanged = structureKey !== lastStructureKey.current;
if (structureChanged) {
// Structure changed - run full layout
lastStructureKey.current = structureKey;
return getLayoutedElements(nodes, edges, 'LR');
} else {
// Structure unchanged - preserve cached positions, just update node data
const layoutedNodes = nodes.map((node) => {
const cachedPosition = positionCache.current.get(node.id);
return {
...node,
position: cachedPosition || { x: 0, y: 0 },
targetPosition: 'left',
sourcePosition: 'right',
} as TaskNode;
});
return { nodes: layoutedNodes, edges };
}
}, [nodes, edges, structureKey, getLayoutedElements]);
// Manual re-layout function
const runLayout = useCallback(

View File

@@ -25,6 +25,7 @@ export interface TaskNodeData extends Feature {
onStopTask?: () => void;
onResumeTask?: () => void;
onSpawnTask?: () => void;
onDeleteTask?: () => void;
}
export type TaskNode = Node<TaskNodeData, 'task'>;
@@ -33,6 +34,7 @@ export type DependencyEdge = Edge<{
targetStatus: Feature['status'];
isHighlighted?: boolean;
isDimmed?: boolean;
onDeleteDependency?: (sourceId: string, targetId: string) => void;
}>;
export interface NodeActionCallbacks {
@@ -42,6 +44,8 @@ export interface NodeActionCallbacks {
onStopTask?: (featureId: string) => void;
onResumeTask?: (featureId: string) => void;
onSpawnTask?: (featureId: string) => void;
onDeleteTask?: (featureId: string) => void;
onDeleteDependency?: (sourceId: string, targetId: string) => void;
}
interface UseGraphNodesProps {
@@ -117,6 +121,9 @@ export function useGraphNodes({
onSpawnTask: actionCallbacks?.onSpawnTask
? () => actionCallbacks.onSpawnTask!(feature.id)
: undefined,
onDeleteTask: actionCallbacks?.onDeleteTask
? () => actionCallbacks.onDeleteTask!(feature.id)
: undefined,
},
};
@@ -146,6 +153,7 @@ export function useGraphNodes({
targetStatus: feature.status,
isHighlighted: edgeIsHighlighted,
isDimmed: edgeIsDimmed,
onDeleteDependency: actionCallbacks?.onDeleteDependency,
},
};
edgeList.push(edge);