feat(docs): add custom layer docs (#383)

This commit is contained in:
xiamidaxia 2025-06-17 15:41:17 +08:00 committed by GitHub
parent fd423d9cb5
commit 77d8a893cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 343 additions and 21 deletions

View File

@ -2,7 +2,7 @@ import React from 'react';
import { WorkflowDragService, useService } from '@flowgram.ai/free-layout-editor';
const cardkeys = ['Node1', 'Node2'];
const cardkeys = ['Node1', 'Node2', 'Condition'];
export const NodeAddPanel: React.FC = (props) => {
const startDragSerivce = useService<WorkflowDragService>(WorkflowDragService);
@ -14,7 +14,7 @@ export const NodeAddPanel: React.FC = (props) => {
key={nodeType}
className="demo-free-card"
onMouseDown={(e) =>
startDragSerivce.startDragCard(nodeType, e, {
startDragSerivce.startDragCard(nodeType.toLowerCase(), e, {
data: {
title: `New ${nodeType}`,
content: 'xxxx',

View File

@ -86,10 +86,24 @@ export const useEditorProps = () =>
* Render Node
*/
renderDefaultNode: (props: WorkflowNodeProps) => {
const { form } = useNodeRender();
const { form, node } = useNodeRender();
return (
<WorkflowNodeRenderer className="demo-free-node" node={props.node}>
{form?.render()}
{node.flowNodeType === 'condition' && (
<div
data-port-id="if"
data-port-type="output"
style={{ position: 'absolute', right: 0, top: '33%' }}
/>
)}
{node.flowNodeType === 'condition' && (
<div
data-port-id="else"
data-port-type="output"
style={{ position: 'absolute', right: 0, top: '66%' }}
/>
)}
</WorkflowNodeRenderer>
);
},
@ -98,7 +112,7 @@ export const useEditorProps = () =>
* Content change
*/
onContentChange(ctx, event) {
// console.log('Auto Save: ', event, ctx.document.toJSON());
console.log('Auto Save: ', event, ctx.document.toJSON());
},
// /**
// * Node engine enable, you can configure formMeta in the FlowNodeRegistry

View File

@ -6,7 +6,10 @@ export const initialData: WorkflowJSON = {
id: 'start_0',
type: 'start',
meta: {
position: { x: 0, y: 0 },
position: {
x: 150,
y: 100,
},
},
data: {
title: 'Start',
@ -15,26 +18,60 @@ export const initialData: WorkflowJSON = {
},
{
id: 'node_0',
type: 'custom',
type: 'condition',
meta: {
position: { x: 400, y: 0 },
position: {
x: 550,
y: 100,
},
},
data: {
title: 'Custom',
content: 'Custom node content',
title: 'Condition',
content: 'Condition node content',
},
},
{
id: 'end_0',
type: 'end',
meta: {
position: { x: 800, y: 0 },
position: {
x: 1350,
y: 100,
},
},
data: {
title: 'End',
content: 'End content',
},
},
{
id: '144150',
type: 'node1',
meta: {
position: {
x: 950,
y: 0,
},
},
data: {
title: 'New Node1',
content: 'xxxx',
},
},
{
id: '118937',
type: 'node2',
meta: {
position: {
x: 950,
y: 200,
},
},
data: {
title: 'New Node2',
content: 'xxxx',
},
},
],
edges: [
{
@ -43,6 +80,20 @@ export const initialData: WorkflowJSON = {
},
{
sourceNodeID: 'node_0',
targetNodeID: '144150',
sourcePortID: 'if',
},
{
sourceNodeID: 'node_0',
targetNodeID: '118937',
sourcePortID: 'else',
},
{
sourceNodeID: '118937',
targetNodeID: 'end_0',
},
{
sourceNodeID: '144150',
targetNodeID: 'end_0',
},
],

View File

@ -14,6 +14,13 @@ export const nodeRegistries: WorkflowNodeRegistry[] = [
defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
},
},
{
type: 'condition',
meta: {
defaultPorts: [{ type: 'input' }],
useDynamicPort: true,
},
},
{
type: 'end',
meta: {

View File

@ -6286,14 +6286,14 @@ url: /guide/advanced/free-layout/port.md
* 动态端口
节点声明添加 `dynamicPorts` , 当设置为 true 则会到节点dom 上寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
节点声明添加 `useDynamicPort` , 当设置为 true 则会到节点dom 上寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
```ts pure title="node-registries.ts"
{
type: 'condition',
meta: {
defaultPorts: [{ type: 'input'}]
dynamicPort: true
useDynamicPort: true
},
}
@ -7528,7 +7528,7 @@ export const useEditorProps = () => {
return {
// ...
nodeEngine: {
enable: false, // 不开启节点引擎,则无法使用 from
enable: false, // 不开启节点引擎,则无法使用 form
},
history: {
enable: true,
@ -8745,7 +8745,7 @@ export const nodeRegistries: WorkflowNodeRegistry[] = [
deleteDisable: true, // 开始节点不能删除
copyDisable: true, // 开始节点不能复制
defaultPorts: [{ type: 'output' }], // 用于定义节点的输入和输出端口, 开始节点只有输出端口
// dynamicPort: true, // 用于动态端口,会寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
// useDynamicPort: true, // 用于动态端口,会寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
},
/**
* 配置节点表单的校验及渲染,

View File

@ -21,5 +21,6 @@
"minimap",
"custom-plugin",
"custom-service",
"custom-layer",
"form-materials"
]

View File

@ -0,0 +1,113 @@
# Custom Layer
We split the canvas into multiple Layers, implementing the concept of interaction layering for better plugin management. For more details, see [Canvas Engine](guide/concepts/canvas-engine.html)
1. Use `observeEntityDatas`, `observeEntities`, and `observeEntity` to monitor updates to any data module of canvas nodes
2. Use `onZoom`, `onScroll`, `onViewportChange`, etc. to monitor canvas zooming or scrolling
3. Use `render` to insert React elements into the canvas, such as drawing SVG lines
![Aspect-oriented programming](@/public/en-layer-uml.jpg)
## Creating a Layer
```tsx pure
import { FreeLayoutPluginContext, inject, injectable, FlowNodeEntity, FlowNodeTransformData, FlowNodeFormData } from '@flowgram.ai/free-layout-editor'
@injectable()
export class MyLayer extends Layer {
@inject(FreeLayoutPluginContext) ctx: FreeLayoutPluginContext
// Can monitor node width, height, and position changes
@observeEntityDatas(FlowNodeEntity, FlowNodeTransformData) transformDatas: FlowNodeTransformData[];
// Can monitor form data changes, connection data can be stored in forms
@observeEntityDatas(FlowNodeEntity, FlowNodeFormData) formDatas: FlowNodeFormData[];
onReady() {
// Can also add styles
// zIndex controls whether to overlay nodes, nodes default to 10, greater than 10 will be above nodes
this.node.style.zIndex = 11;
}
onZoom(scale) {
// Scale with canvas
this.node.style.transform = `scale(${scale})`;
}
render() {
return <div>{...}</div>
}
}
```
## Adding to Canvas
- Through use-editor-props
```ts pure
{
onInit: (ctx) => {
ctx.playground.registerLayer(MyLayer)
}
}
```
- Through plugin
```tsx pure
import { FreeLayoutPluginContext } from '@flowgram.ai/free-layout-editor'
export const createMyPlugin = definePluginCreator<{}, FreeLayoutPluginContext>({
onInit: (ctx, opts) => {
ctx.playground.registerLayer(MyLayer)
},
});
```
## Layer Lifecycle Description
```ts
interface Layer {
/**
* Triggered during initialization
*/
onReady?(): void;
/**
* Triggered when playground size changes
*/
onResize?(size: PipelineDimension): void;
/**
* Triggered when playground is focused
*/
onFocus?(): void;
/**
* Triggered when playground loses focus
*/
onBlur?(): void;
/**
* Monitor zoom
*/
onZoom?(scale: number): void;
/**
* Monitor scroll
*/
onScroll?(scroll: { scrollX: number; scrollY: number }): void;
/**
* Triggered when viewport updates
*/
onViewportChange?(): void;
/**
* Triggered when readonly or disabled state changes
* @param state
*/
onReadonlyOrDisabledChange?(state: { disabled: boolean; readonly: boolean }): void;
/**
* Data updates automatically trigger React render, if not provided React rendering won't be called
*/
render?(): JSX.Element
}
```

View File

@ -22,7 +22,7 @@ Add `defaultPorts` to node declaration, such as `{ type: 'input' }`, which will
- Dynamic Ports
Add `dynamicPorts` to node declaration, when set to true it will look for DOM elements with `data-port-id` and `data-port-type` attributes as ports
Add `useDynamicPort` to node declaration, when set to true it will look for DOM elements with `data-port-id` and `data-port-type` attributes as ports
```ts pure title="node-registries.ts"
@ -30,7 +30,7 @@ Add `dynamicPorts` to node declaration, when set to true it will look for DOM el
type: 'condition',
meta: {
defaultPorts: [{ type: 'input'}]
dynamicPort: true
useDynamicPort: true
},
}

View File

@ -220,7 +220,7 @@ export const nodeRegistries: WorkflowNodeRegistry[] = [
deleteDisable: true, // Start node cannot be deleted
copyDisable: true, // Start node cannot be copied
defaultPorts: [{ type: 'output' }], // Define node input and output ports, start node only has output port
// dynamicPort: true, // For dynamic ports, will look for DOM with data-port-id and data-port-type attributes as ports
// useDynamicPort: true, // For dynamic ports, will look for DOM with data-port-id and data-port-type attributes as ports
},
/**
* Configure node form validation and rendering

View File

@ -21,5 +21,6 @@
"minimap",
"custom-plugin",
"custom-service",
"custom-layer",
"form-materials"
]

View File

@ -0,0 +1,113 @@
# 自定义 Layer
我们将画布拆分成多个 Layer实现交互分层的思想便于插件化管理详细见 [画布引擎](guide/concepts/canvas-engine.html)
1. 通过 `observeEntityDatas` `observeEntities` `observeEntity` 监听画布节点任意数据模块的更新
2. 通过 `onZoom` `onScroll` `onViewportChange` 等监听画布的缩放或者滚动
3. 通过 `render` 往画布中插入 react 元素, 如绘制 svg 线条
![Layer](@/public/layer-uml.jpg)
## 创建 Layer
```tsx pure
import { FreeLayoutPluginContext, inject, injectable, FlowNodeEntity, FlowNodeTransformData, FlowNodeFormData } from '@flowgram.ai/free-layout-editor'
@injectable()
export class MyLayer extends Layer {
@inject(FreeLayoutPluginContext) ctx: FreeLayoutPluginContext
// 可以监听节点的宽高位置变化
@observeEntityDatas(FlowNodeEntity, FlowNodeTransformData) transformDatas: FlowNodeTransformData[];
// 可以监听表单数据变化,连线数据可以存在表单里
@observeEntityDatas(FlowNodeEntity, FlowNodeFormData) formDatas: FlowNodeFormData[];
onReady() {
// 也可以添加样式
// zIndex可以控制是否要盖在节点, 节点默认是 10大于 10 则在节点上边
this.node.style.zIndex = 11;
}
onZoom(scale) {
// 跟着画布缩放
this.node.style.transform = `scale(${scale})`;
}
render() {
return <div>{...}</div>
}
}
```
## 加入到画布
- 通过 use-editor-props
```ts pure
{
onInit: (ctx) => {
ctx.playground.registerLayer(MyLayer)
}
}
```
- 通过插件添加
```tsx pure
import { FreeLayoutPluginContext } from '@flowgram.ai/free-layout-editor'
export const createMyPlugin = definePluginCreator<{}, FreeLayoutPluginContext>({
onInit: (ctx, opts) => {
ctx.playground.registerLayer(MyLayer)
},
});
```
## Layer 生命周期说明
```ts
interface Layer {
/**
* 初始化时候触发
*/
onReady?(): void;
/**
* playground 大小变化时候会触发
*/
onResize?(size: PipelineDimension): void;
/**
* playground focus 时候触发
*/
onFocus?(): void;
/**
* playground blur 时候触发
*/
onBlur?(): void;
/**
* 监听缩放
*/
onZoom?(scale: number): void;
/**
* 监听滚动
*/
onScroll?(scroll: { scrollX: number; scrollY: number }): void;
/**
* viewport 更新触发
*/
onViewportChange?(): void;
/**
* readonly 或 disable 状态变化
* @param state
*/
onReadonlyOrDisabledChange?(state: { disabled: boolean; readonly: boolean }): void;
/**
* 数据更新自动触发react render如果不提供则不会调用react渲染
*/
render?(): JSX.Element
}
```

View File

@ -22,7 +22,7 @@
- 动态端口
节点声明添加 `dynamicPorts` , 当设置为 true 则会到节点dom 上寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
节点声明添加 `useDynamicPort` , 当设置为 true 则会到节点dom 上寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
```ts pure title="node-registries.ts"
@ -30,7 +30,7 @@
type: 'condition',
meta: {
defaultPorts: [{ type: 'input'}]
dynamicPort: true
useDynamicPort: true
},
}

View File

@ -8,7 +8,7 @@ export const useEditorProps = () => {
return {
// ...
nodeEngine: {
enable: false, // 不开启节点引擎,则无法使用 from
enable: false, // 不开启节点引擎,则无法使用 form
},
history: {
enable: true,

View File

@ -229,7 +229,7 @@ export const nodeRegistries: WorkflowNodeRegistry[] = [
deleteDisable: true, // 开始节点不能删除
copyDisable: true, // 开始节点不能复制
defaultPorts: [{ type: 'output' }], // 用于定义节点的输入和输出端口, 开始节点只有输出端口
// dynamicPort: true, // 用于动态端口,会寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
// useDynamicPort: true, // 用于动态端口,会寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
},
/**
* 配置节点表单的校验及渲染,

View File

@ -1,3 +1,25 @@
# 注意事项及常见问题
# 注意事项
1. 画布的 editor 版本及导入的插件版本必须一致,如果出现以下错误都是这个问题造成
2. 画布的事件监听,在 react 中使用都要配套写销毁逻辑,防止无限注册导致内存泄漏
```tsx pure
function SomeReactComp() {
useEffect(() => {
const toDispose = ctx.document.onContentChange(() => {
// DO Something
})
return () => toDispose.dispose() // Destroy Event
}, [])
}
```
# 常见问题