Skip to content

Commit 13854f9

Browse files
committed
allow reordering of horizontally positioned child tasks
1 parent b55499d commit 13854f9

File tree

2 files changed

+57
-48
lines changed

2 files changed

+57
-48
lines changed

web-ui/src/components/builder/graph/DropZoneNode.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { useDroppable } from '@dnd-kit/core';
44

55
export interface DropZoneNodeData {
66
dropId: string; // The drop zone ID for dnd-kit
7-
isHorizontal?: boolean; // For parallel lanes
7+
isHorizontal?: boolean; // For parallel lanes (add new lane)
8+
isInterLane?: boolean; // For vertical separators between parallel lanes (reorder)
89
laneIndex?: number; // Which lane this is in (for parallel containers)
910
parentTaskId?: string; // Parent glue task ID
1011
insertIndex: number; // Where to insert when dropped
@@ -13,7 +14,7 @@ export interface DropZoneNodeData {
1314
}
1415

1516
function DropZoneNode({ data }: NodeProps<DropZoneNodeData>) {
16-
const { dropId, isHorizontal, disabled } = data;
17+
const { dropId, isHorizontal, isInterLane, disabled } = data;
1718

1819
const { setNodeRef, isOver } = useDroppable({
1920
id: dropId,
@@ -22,6 +23,40 @@ function DropZoneNode({ data }: NodeProps<DropZoneNodeData>) {
2223

2324
const isActive = isOver && !disabled;
2425

26+
if (isInterLane) {
27+
// Vertical separator between parallel lanes (for reordering)
28+
return (
29+
<div
30+
ref={setNodeRef}
31+
className={`
32+
relative w-full h-full
33+
flex items-center justify-center
34+
transition-all duration-150
35+
${isActive
36+
? 'bg-primary-500/10'
37+
: ''
38+
}
39+
`}
40+
>
41+
<Handle
42+
type="target"
43+
position={Position.Left}
44+
className="!w-1 !h-1 !bg-transparent !border-0"
45+
/>
46+
{isActive ? (
47+
<div className="w-0.5 h-4/5 bg-primary-500 rounded-full" />
48+
) : (
49+
<div className="w-px h-1/3 bg-[var(--color-border)] opacity-30" />
50+
)}
51+
<Handle
52+
type="source"
53+
position={Position.Right}
54+
className="!w-1 !h-1 !bg-transparent !border-0"
55+
/>
56+
</div>
57+
);
58+
}
59+
2560
if (isHorizontal) {
2661
// Horizontal drop zone (for adding parallel lanes)
2762
return (

web-ui/src/components/builder/graph/useBuilderGraphLayout.ts

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ function calculateTaskSize(task: BuilderTask): SizeResult {
120120
const maxChildHeight = Math.max(...childSizes.map((s) => s.height));
121121
const totalChildWidth = childSizes.reduce((sum, s) => sum + s.width, 0);
122122

123-
// Lane content height = drop zone + child + drop zone
124-
const laneContentHeight = DROP_ZONE_HEIGHT + VERTICAL_GAP + maxChildHeight + VERTICAL_GAP + DROP_ZONE_HEIGHT;
123+
// Lane content height = just child height (inter-lane gaps handle reordering)
124+
const laneContentHeight = maxChildHeight;
125125

126-
// Total width = padding + all children + gaps + empty lane (if can add more)
127-
const lanesWidth = totalChildWidth + (children.length - 1) * LANE_GAP + (canAddMore ? LANE_GAP + EMPTY_LANE_WIDTH : 0);
126+
// Total width = inter-lane gaps (one before each lane) + children + empty lane
127+
const lanesWidth = children.length * LANE_GAP + totalChildWidth + (canAddMore ? LANE_GAP + EMPTY_LANE_WIDTH : 0);
128128

129129
return {
130130
width: CONTAINER_PADDING_X * 2 + Math.max(NODE_WIDTH, lanesWidth),
@@ -300,74 +300,48 @@ export function useBuilderGraphLayout(
300300
}
301301
});
302302
} else if (isConcurrent && childCount > 0) {
303-
// Other concurrent tasks: render parallel lanes
303+
// Other concurrent tasks: render parallel lanes with inter-lane drop zones
304304
const childSizes = children.map((child) => calculateTaskSize(child));
305+
const maxChildHeight = Math.max(...childSizes.map((s) => s.height));
305306

306307
let laneX = CONTAINER_PADDING_X;
307308
const laneTopY = CONTAINER_PADDING_TOP;
309+
const gapHeight = maxChildHeight;
308310

309311
for (let i = 0; i < childCount; i++) {
310312
const child = children[i];
311313
const childSize = childSizes[i];
312314

313-
// Center child in its lane
314-
const laneWidth = childSize.width;
315-
const childX = laneX;
316-
const childY = laneTopY + DROP_ZONE_HEIGHT + VERTICAL_GAP;
317-
318-
// Top drop zone for this lane
319-
const topDropId = generateId('drop');
315+
// Inter-lane drop zone before this lane (for reordering and inserting)
316+
const gapDropId = generateId('drop');
320317
nodes.push({
321-
id: topDropId,
318+
id: gapDropId,
322319
type: 'dropZone',
323-
position: { x: childX, y: laneTopY },
320+
position: { x: laneX, y: laneTopY },
324321
parentNode: nodeId,
325322
extent: 'parent',
326323
draggable: false,
327324
data: {
328325
dropId: isCleanup ? `cleanup-insert-before-${child.id}` : `insert-before-${child.id}`,
326+
isInterLane: true,
329327
parentTaskId: task.id,
330328
insertIndex: i,
331-
disabled: false,
332329
isCleanup,
333330
} as DropZoneNodeData,
334-
style: { width: laneWidth, height: DROP_ZONE_HEIGHT },
331+
style: { width: LANE_GAP, height: gapHeight },
335332
});
336333

337-
// Process child task
338-
const childResult = processTask(child, childX, childY, nodeId, undefined, isCleanup);
339-
340-
// Bottom drop zone for this lane
341-
const bottomDropId = generateId('drop');
342-
const bottomY = childY + childSize.height + VERTICAL_GAP;
343-
nodes.push({
344-
id: bottomDropId,
345-
type: 'dropZone',
346-
position: { x: childX, y: bottomY },
347-
parentNode: nodeId,
348-
extent: 'parent',
349-
draggable: false,
350-
data: {
351-
dropId: isCleanup ? `cleanup-insert-after-children-${child.id}` : `insert-after-children-${child.id}`,
352-
parentTaskId: task.id,
353-
insertIndex: i + 1,
354-
disabled: true, // Can't insert after in concurrent
355-
isCleanup,
356-
} as DropZoneNodeData,
357-
style: { width: laneWidth, height: DROP_ZONE_HEIGHT },
358-
});
334+
laneX += LANE_GAP;
359335

360-
// Edges for this lane
361-
addEdge(topDropId, childResult.nodeId, edgeColor);
362-
addEdge(childResult.nodeId, bottomDropId, edgeColor);
336+
// Process child task directly in the lane
337+
processTask(child, laneX, laneTopY, nodeId, undefined, isCleanup);
363338

364-
laneX += laneWidth + LANE_GAP;
339+
laneX += childSize.width;
365340
}
366341

367342
// Add empty lane for adding new parallel task (if allowed)
368343
if (canAddMore) {
369-
const maxChildHeight2 = Math.max(...childSizes.map((s) => s.height));
370-
const emptyLaneHeight = DROP_ZONE_HEIGHT + VERTICAL_GAP + maxChildHeight2 + VERTICAL_GAP + DROP_ZONE_HEIGHT;
344+
laneX += LANE_GAP;
371345
const emptyLaneDropId = generateId('drop');
372346
nodes.push({
373347
id: emptyLaneDropId,
@@ -377,13 +351,13 @@ export function useBuilderGraphLayout(
377351
extent: 'parent',
378352
draggable: false,
379353
data: {
380-
dropId: isCleanup ? `cleanup-insert-first-child-${task.id}` : `insert-first-child-${task.id}`,
354+
dropId: isCleanup ? `cleanup-insert-after-children-${task.id}` : `insert-after-children-${task.id}`,
381355
isHorizontal: true,
382356
parentTaskId: task.id,
383357
insertIndex: childCount,
384358
isCleanup,
385359
} as DropZoneNodeData,
386-
style: { width: EMPTY_LANE_WIDTH, height: emptyLaneHeight },
360+
style: { width: EMPTY_LANE_WIDTH, height: gapHeight },
387361
});
388362
}
389363
} else {

0 commit comments

Comments
 (0)