Skip to content

Commit f955657

Browse files
committed
fix: group and ungroup
1 parent cf8c4dc commit f955657

File tree

2 files changed

+90
-67
lines changed

2 files changed

+90
-67
lines changed

frontend/src/components/flow/flow-editor.tsx

Lines changed: 89 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -118,39 +118,66 @@ export const FlowEditor = ({ projectId }: { projectId: number }) => {
118118
const onNodesChange = useCallback(
119119
(changes: NodeChange[]) => {
120120
setNodes((nds) => {
121-
const nextNodes = applyNodeChanges(changes, nds);
122-
console.log('nextNodes', changes, nextNodes);
121+
let nextNodes = applyNodeChanges(changes, nds);
123122

123+
// Handle node dragging for grouping/ungrouping
124124
changes.forEach((change) => {
125-
if (change.type === 'position' && 'id' in change) {
126-
const node = nextNodes.find((n) => n.id === change.id);
127-
if (node && node.type && !isGroupType(node.type)) {
125+
if (change.type === 'position') {
126+
const draggedNode = nextNodes.find((n) => n.id === change.id);
127+
if (!draggedNode || (draggedNode.type && isGroupType(draggedNode.type))) {
128+
return;
129+
}
130+
131+
// Calculate absolute position for the node
132+
const absolutePosition = { ...draggedNode.position };
133+
const oldParent = draggedNode.parentId ? nextNodes.find(n => n.id === draggedNode.parentId) : null;
134+
if (oldParent) {
135+
absolutePosition.x += oldParent.position.x;
136+
absolutePosition.y += oldParent.position.y;
137+
}
138+
139+
// During dragging, update the node's position to be absolute
140+
if ('dragging' in change && change.dragging) {
141+
draggedNode.position = absolutePosition;
142+
draggedNode.parentId = undefined;
143+
draggedNode.extent = undefined;
144+
return;
145+
}
146+
147+
// When drag ends, handle grouping/ungrouping
148+
if ('dragging' in change && !change.dragging) {
149+
// Find potential group node at the dragged node's absolute center position
150+
const nodeCenterPosition = {
151+
x: absolutePosition.x + (draggedNode.width ?? 0) / 2,
152+
y: absolutePosition.y + (draggedNode.height ?? 0) / 2,
153+
};
154+
155+
// Find any group node that contains this position
128156
const groupNode = nextNodes.find(
129157
(n) =>
130-
n.type && isGroupType(n.type) &&
131-
isPositionInsideNode(
132-
{
133-
x: node.position.x + (node.width ?? 0) / 2,
134-
y: node.position.y + (node.height ?? 0) / 2,
135-
},
136-
n
137-
)
158+
n.type === 'groupchat' &&
159+
n.id !== draggedNode.id &&
160+
!n.parentId && // Prevent nested groups
161+
nodeCenterPosition.x > n.position.x &&
162+
nodeCenterPosition.x < (n.position.x + (n.width ?? 0)) &&
163+
nodeCenterPosition.y > n.position.y &&
164+
nodeCenterPosition.y < (n.position.y + (n.height ?? 0))
138165
);
166+
167+
// Handle grouping - if inside a group
139168
if (groupNode) {
140-
node.parentId = groupNode.id;
141-
node.position = {
142-
x: node.position.x - groupNode.position.x,
143-
y: node.position.y - groupNode.position.y,
169+
// Node is entering a group
170+
draggedNode.parentId = groupNode.id;
171+
draggedNode.position = {
172+
x: nodeCenterPosition.x - groupNode.position.x,
173+
y: nodeCenterPosition.y - groupNode.position.y,
144174
};
145-
// Move the node to be after the group node in the array
146-
const nodeIndex = nextNodes.indexOf(node);
147-
const groupNodeIndex = nextNodes.indexOf(groupNode);
148-
if (nodeIndex > groupNodeIndex) {
149-
nextNodes.splice(nodeIndex, 1);
150-
nextNodes.splice(groupNodeIndex + 1, 0, node);
151-
}
152-
} else {
153-
node.parentId = undefined;
175+
draggedNode.extent = 'parent';
176+
177+
// Ensure group node is before its children
178+
nextNodes = nextNodes.filter(n => n.id !== draggedNode.id);
179+
const groupIndex = nextNodes.findIndex(n => n.id === groupNode.id);
180+
nextNodes.splice(groupIndex + 1, 0, draggedNode);
154181
}
155182
}
156183
}
@@ -218,18 +245,7 @@ export const FlowEditor = ({ projectId }: { projectId: number }) => {
218245
(event: React.DragEvent) => {
219246
event.preventDefault();
220247
const data = JSON.parse(event.dataTransfer.getData('application/json'));
221-
console.log('onDrop', data);
222-
if (!data) {
223-
console.warn('No data found in onDrop');
224-
return;
225-
}
226-
227-
if (!flowParent.current) {
228-
console.warn(
229-
'Unexpected null value of flowParent, drag & drop failed.'
230-
);
231-
return;
232-
}
248+
if (!data || !flowParent.current) return;
233249

234250
const flowBounds = flowParent.current.getBoundingClientRect();
235251
const position = screenToFlowPosition({
@@ -239,16 +255,18 @@ export const FlowEditor = ({ projectId }: { projectId: number }) => {
239255

240256
const { offsetX, offsetY, ...cleanedData } = data;
241257
const newId = nanoid();
242-
const newNode: Node = {
258+
const newNode = {
243259
id: `node-${data.id}-${newId}`,
244260
type: data.id,
245261
position,
246262
data: cleanedData,
247-
selected: true,
248-
dragHandle: '.drag-handle',
263+
selected: false,
264+
draggable: true,
265+
selectable: true,
266+
focusable: true,
249267
parentId: undefined,
250268
style: undefined,
251-
};
269+
} satisfies Node;
252270

253271
// Find if we're dropping into a group node
254272
const groupNode = nodes.find(
@@ -260,32 +278,38 @@ export const FlowEditor = ({ projectId }: { projectId: number }) => {
260278
position.y < (n.position.y + (n.height ?? 0))
261279
);
262280

263-
if (groupNode) {
264-
// Adjust position to be relative to the group node
265-
newNode.position = {
266-
x: position.x - groupNode.position.x,
267-
y: position.y - groupNode.position.y,
268-
};
269-
newNode.parentId = groupNode.id;
270-
// If the node being dropped is also a group, adjust its size
271-
if (data.id === 'groupchat') {
272-
newNode.style = {
273-
width: Math.min(300, groupNode.width! - 50),
274-
height: Math.min(200, groupNode.height! - 50)
281+
setNodes((nds) => {
282+
const updatedNodes = nds.map((n) => ({ ...n, selected: false }));
283+
284+
if (groupNode) {
285+
// Adjust position to be relative to the group node
286+
const updatedNode = {
287+
...newNode,
288+
position: {
289+
x: position.x - groupNode.position.x,
290+
y: position.y - groupNode.position.y,
291+
},
292+
parentId: groupNode.id,
293+
// If the node being dropped is also a group, adjust its size
294+
...(data.id === 'groupchat' ? {
295+
style: {
296+
width: Math.min(300, groupNode.width! - 50),
297+
height: Math.min(200, groupNode.height! - 50)
298+
}
299+
} : {})
275300
};
301+
302+
// Insert new node right after its parent group
303+
const groupIndex = updatedNodes.findIndex(n => n.id === groupNode.id);
304+
if (groupIndex !== -1) {
305+
updatedNodes.splice(groupIndex + 1, 0, updatedNode);
306+
return updatedNodes;
307+
}
276308
}
277-
}
278309

279-
setNodes((nds) =>
280-
nds.map((n) => ({ ...n, selected: false }))
281-
.concat({
282-
...newNode,
283-
selected: false,
284-
draggable: true,
285-
selectable: true,
286-
focusable: true,
287-
})
288-
);
310+
// If not in a group or group not found, just append to the end
311+
return [...updatedNodes, newNode];
312+
});
289313
},
290314
[nodes, screenToFlowPosition, setNodes, flowParent]
291315
);

frontend/src/components/flow/node/group.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export const GroupNode = ({
1717
selected,
1818
...props
1919
}: NodeProps) => {
20-
const [showConfig, setShowConfig] = React.useState(false);
2120
const nodeData = data as GroupNodeData;
2221

2322
return (
@@ -34,7 +33,7 @@ export const GroupNode = ({
3433
'flex flex-col min-w-[400px] min-h-[300px] p-2',
3534
'bg-muted/5 backdrop-blur-sm rounded-xl',
3635
'border-2 border-dashed transition-colors',
37-
selected ? 'border-purple-500 text-purple-500' : 'border-primary/20',
36+
selected ? 'border-purple-500 text-purple-500' : 'border-primary/40 text-primary/40',
3837
nodeData.isHighlighted && 'border-primary border-2 bg-primary/5'
3938
)}>
4039
<Handle

0 commit comments

Comments
 (0)