Skip to content

Commit 805b262

Browse files
authored
fix(group): auto layout adapts group (bytedance#223)
* fix(group): line inside multi-layer nested group cannot be selected * feat(group): auto layout adapts group * docs: update free-layout-demo example image * chore(demo): update initial data * feat(container): removeNodeLines api set to public
1 parent de3c5da commit 805b262

File tree

8 files changed

+139
-63
lines changed

8 files changed

+139
-63
lines changed

apps/demo-free-layout/src/components/group/components/header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const GroupHeader: FC<GroupHeaderProps> = ({
2525
return (
2626
<div
2727
className="workflow-group-header"
28+
data-flow-editor-selectable="false"
2829
onMouseDown={onMouseDown}
2930
onFocus={onFocus}
3031
onBlur={onBlur}

apps/demo-free-layout/src/components/group/components/node-render.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useEffect } from 'react';
2+
13
import {
24
FlowNodeFormData,
35
Form,
@@ -21,6 +23,13 @@ export const GroupNodeRender = () => {
2123

2224
const { height, width } = nodeSize ?? {};
2325
const nodeHeight = height ?? 0;
26+
27+
useEffect(() => {
28+
// prevent lines in outside cannot be selected - 防止外层线条不可选中
29+
const element = node.renderData.node;
30+
element.style.pointerEvents = 'none';
31+
}, [node]);
32+
2433
return (
2534
<div
2635
className={`workflow-group-render ${selected ? 'selected' : ''}`}

apps/demo-free-layout/src/initial-data.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const initialData: FlowDocumentJSON = {
88
meta: {
99
position: {
1010
x: 180,
11-
y: 298,
11+
y: 381.75,
1212
},
1313
},
1414
data: {
@@ -30,7 +30,7 @@ export const initialData: FlowDocumentJSON = {
3030
meta: {
3131
position: {
3232
x: 640,
33-
y: 279.5,
33+
y: 363.25,
3434
},
3535
},
3636
data: {
@@ -80,7 +80,7 @@ export const initialData: FlowDocumentJSON = {
8080
meta: {
8181
position: {
8282
x: 2220,
83-
y: 298,
83+
y: 381.75,
8484
},
8585
},
8686
data: {
@@ -101,7 +101,7 @@ export const initialData: FlowDocumentJSON = {
101101
meta: {
102102
position: {
103103
x: 1020,
104-
y: 452,
104+
y: 547.96875,
105105
},
106106
},
107107
data: {
@@ -222,7 +222,7 @@ export const initialData: FlowDocumentJSON = {
222222
meta: {
223223
position: {
224224
x: 640,
225-
y: 478,
225+
y: 522.46875,
226226
},
227227
},
228228
data: {
@@ -238,8 +238,8 @@ export const initialData: FlowDocumentJSON = {
238238
type: 'group',
239239
meta: {
240240
position: {
241-
x: -1.5112031149433278,
242-
y: 0,
241+
x: 1020,
242+
y: 96.25,
243243
},
244244
},
245245
data: {
@@ -252,8 +252,8 @@ export const initialData: FlowDocumentJSON = {
252252
type: 'llm',
253253
meta: {
254254
position: {
255-
x: 1660.1942854301792,
256-
y: 1.8635936030104148,
255+
x: 640,
256+
y: 0,
257257
},
258258
},
259259
data: {
@@ -297,8 +297,8 @@ export const initialData: FlowDocumentJSON = {
297297
type: 'llm',
298298
meta: {
299299
position: {
300-
x: 1202.8281207997074,
301-
y: 1.8635936030104148,
300+
x: 180,
301+
y: 0,
302302
},
303303
},
304304
data: {
-506 KB
Loading

packages/plugins/free-auto-layout-plugin/src/layout/store.ts

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { WorkflowLineEntity, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
2-
import { FlowNodeTransformData } from '@flowgram.ai/document';
2+
import { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
33

44
import { LayoutEdge, LayoutNode, LayoutParams } from './type';
55

@@ -52,11 +52,21 @@ export class LayoutStore {
5252
edges: WorkflowLineEntity[];
5353
}): LayoutStoreData {
5454
const { nodes, edges } = params;
55+
const layoutNodes = this.createLayoutNodes(nodes);
56+
const layoutEdges = this.createEdgesStore(edges);
57+
const virtualEdges = this.createVirtualEdges(params);
5558
const store = {
5659
nodes: new Map(),
5760
edges: new Map(),
5861
};
59-
nodes.forEach((node, index) => {
62+
layoutNodes.forEach((node) => store.nodes.set(node.id, node));
63+
layoutEdges.concat(virtualEdges).forEach((edge) => store.edges.set(edge.id, edge));
64+
return store;
65+
}
66+
67+
/** 创建节点布局数据 */
68+
private createLayoutNodes(nodes: WorkflowNodeEntity[]): LayoutNode[] {
69+
const layoutNodes = nodes.map((node, index) => {
6070
const { bounds } = node.getData(FlowNodeTransformData);
6171
const layoutNode: LayoutNode = {
6272
id: node.id,
@@ -69,27 +79,89 @@ export class LayoutStore {
6979
size: { width: bounds.width, height: bounds.height },
7080
hasChildren: node.collapsedChildren?.length > 0,
7181
};
72-
store.nodes.set(layoutNode.id, layoutNode);
82+
return layoutNode;
7383
});
84+
return layoutNodes;
85+
}
7486

75-
edges.forEach((edge) => {
76-
const { from, to } = edge.info;
77-
if (!from || !to || edge.vertical) {
78-
return;
79-
}
80-
const layoutEdge: LayoutEdge = {
81-
id: edge.id,
82-
entity: edge,
83-
from,
84-
to,
85-
fromIndex: '', // 初始化时,index 未计算
86-
toIndex: '', // 初始化时,index 未计算
87-
name: edge.id,
88-
};
89-
store.edges.set(layoutEdge.id, layoutEdge);
90-
});
87+
/** 创建线条布局数据 */
88+
private createEdgesStore(edges: WorkflowLineEntity[]): LayoutEdge[] {
89+
const layoutEdges = edges
90+
.map((edge) => {
91+
const { from, to } = edge.info;
92+
if (!from || !to || edge.vertical) {
93+
return;
94+
}
95+
const layoutEdge: LayoutEdge = {
96+
id: edge.id,
97+
entity: edge,
98+
from,
99+
to,
100+
fromIndex: '', // 初始化时,index 未计算
101+
toIndex: '', // 初始化时,index 未计算
102+
name: edge.id,
103+
};
104+
return layoutEdge;
105+
})
106+
.filter(Boolean) as LayoutEdge[];
107+
return layoutEdges;
108+
}
91109

92-
return store;
110+
/** 创建虚拟线条数据 */
111+
private createVirtualEdges(params: {
112+
nodes: WorkflowNodeEntity[];
113+
edges: WorkflowLineEntity[];
114+
}): LayoutEdge[] {
115+
const { nodes, edges } = params;
116+
const groupNodes = nodes.filter((n) => n.flowNodeType === FlowNodeBaseType.GROUP);
117+
const virtualEdges = groupNodes
118+
.map((group) => {
119+
const { id: groupId, blocks = [] } = group;
120+
const blockIdSet = new Set(blocks.map((b) => b.id));
121+
const groupFromEdges = edges
122+
.filter((edge) => blockIdSet.has(edge.to?.id ?? ''))
123+
.map((edge) => {
124+
const { from, to } = edge.info;
125+
if (!from || !to || edge.vertical) {
126+
return;
127+
}
128+
const id = `virtual_${groupId}_${to}`;
129+
const layoutEdge: LayoutEdge = {
130+
id: id,
131+
entity: edge,
132+
from,
133+
to: groupId,
134+
fromIndex: '', // 初始化时,index 未计算
135+
toIndex: '', // 初始化时,index 未计算
136+
name: id,
137+
};
138+
return layoutEdge;
139+
})
140+
.filter(Boolean) as LayoutEdge[];
141+
const groupToEdges = edges
142+
.filter((edge) => blockIdSet.has(edge.from.id ?? ''))
143+
.map((edge) => {
144+
const { from, to } = edge.info;
145+
if (!from || !to || edge.vertical) {
146+
return;
147+
}
148+
const id = `virtual_${groupId}_${from}`;
149+
const layoutEdge: LayoutEdge = {
150+
id: id,
151+
entity: edge,
152+
from: groupId,
153+
to,
154+
fromIndex: '', // 初始化时,index 未计算
155+
toIndex: '', // 初始化时,index 未计算
156+
name: id,
157+
};
158+
return layoutEdge;
159+
})
160+
.filter(Boolean) as LayoutEdge[];
161+
return [...groupFromEdges, ...groupToEdges];
162+
})
163+
.flat();
164+
return virtualEdges;
93165
}
94166

95167
/** 创建节点索引映射 */
Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { inject, injectable } from 'inversify';
22
import {
33
WorkflowDocument,
4+
WorkflowLineEntity,
45
WorkflowNodeEntity,
56
WorkflowNodeLinesData,
67
} from '@flowgram.ai/free-layout-core';
7-
import { FlowNodeBaseType } from '@flowgram.ai/document';
88

99
import { Layout, type LayoutOptions } from './layout';
1010

@@ -18,18 +18,13 @@ export class AutoLayoutService {
1818

1919
private async layoutNode(node: WorkflowNodeEntity, options: LayoutOptions): Promise<void> {
2020
// 获取子节点
21-
const nodes = this.getAvailableBlocks(node);
21+
const nodes = node.blocks;
2222
if (!nodes || !Array.isArray(nodes) || !nodes.length) {
2323
return;
2424
}
2525

2626
// 获取子线条
27-
const edges = node.blocks
28-
.map((child) => {
29-
const childLinesData = child.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
30-
return childLinesData.outputLines.filter(Boolean);
31-
})
32-
.flat();
27+
const edges = this.getNodesAllLines(nodes);
3328

3429
// 先递归执行子节点 autoLayout
3530
await Promise.all(nodes.map(async (child) => this.layoutNode(child, options)));
@@ -40,17 +35,16 @@ export class AutoLayoutService {
4035
await layout.position();
4136
}
4237

43-
private getAvailableBlocks(node: WorkflowNodeEntity): WorkflowNodeEntity[] {
44-
const commonNodes = node.blocks.filter((n) => !this.shouldFlatNode(n));
45-
const flatNodes = node.blocks
46-
.filter((n) => this.shouldFlatNode(n))
47-
.map((flatNode) => flatNode.blocks)
38+
private getNodesAllLines(nodes: WorkflowNodeEntity[]): WorkflowLineEntity[] {
39+
const lines = nodes
40+
.map((node) => {
41+
const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
42+
const outputLines = linesData.outputLines.filter(Boolean);
43+
const inputLines = linesData.inputLines.filter(Boolean);
44+
return [...outputLines, ...inputLines];
45+
})
4846
.flat();
49-
return [...commonNodes, ...flatNodes];
50-
}
5147

52-
private shouldFlatNode(node: WorkflowNodeEntity): boolean {
53-
// Group 节点不参与自动布局
54-
return node.flowNodeType === FlowNodeBaseType.GROUP;
48+
return lines;
5549
}
5650
}

packages/plugins/free-container-plugin/src/node-into-container/service.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ export class NodeIntoContainerService {
142142
await this.removeNodeLines(dragNode);
143143
}
144144

145+
/** 移除节点连线 */
146+
public async removeNodeLines(node: WorkflowNodeEntity): Promise<void> {
147+
const lines = this.linesManager.getAllLines();
148+
lines.forEach((line) => {
149+
if (line.from.id !== node.id && line.to?.id !== node.id) {
150+
return;
151+
}
152+
line.dispose();
153+
});
154+
await this.nextFrame();
155+
}
156+
145157
/** 初始化状态 */
146158
private initState(): void {
147159
this.state = {
@@ -216,18 +228,6 @@ export class NodeIntoContainerService {
216228
this.dragService.startDragSelectedNodes(event.triggerEvent);
217229
}
218230

219-
/** 移除节点连线 */
220-
private async removeNodeLines(node: WorkflowNodeEntity): Promise<void> {
221-
const lines = this.linesManager.getAllLines();
222-
lines.forEach((line) => {
223-
if (line.from.id !== node.id && line.to?.id !== node.id) {
224-
return;
225-
}
226-
line.dispose();
227-
});
228-
await this.nextFrame();
229-
}
230-
231231
/** 获取重叠位置 */
232232
private getCollisionTransform(params: {
233233
transforms: FlowNodeTransformData[];

packages/plugins/free-hover-plugin/src/hover-layer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
WorkflowSelectService,
1313
} from '@flowgram.ai/free-layout-core';
1414
import { WorkflowPortEntity } from '@flowgram.ai/free-layout-core';
15-
import { FlowNodeTransformData } from '@flowgram.ai/document';
15+
import { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
1616
import {
1717
EditorState,
1818
EditorStateConfigEntity,
@@ -79,7 +79,7 @@ export class HoverLayer extends Layer<HoverLayerOptions> {
7979
autorun(): void {
8080
const { activatedNode } = this.selectionService;
8181
this.nodeTransformsWithSort = this.nodeTransforms
82-
.filter((n) => n.entity.id !== 'root')
82+
.filter((n) => n.entity.id !== 'root' && n.entity.flowNodeType !== FlowNodeBaseType.GROUP)
8383
.reverse() // 后创建的排在前面
8484
.sort((n1) => (n1.entity === activatedNode ? -1 : 0));
8585
}

0 commit comments

Comments
 (0)