mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
docs: advanced docs (#121)
* feat: add advanced docs * feat: workflow-port-entity add availableLines * docs: comment fixed
This commit is contained in:
parent
6a77afc8aa
commit
d5d9734eb2
@ -53,7 +53,7 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
||||
{props.useFx ? (
|
||||
<VariableSelector
|
||||
value={value.default}
|
||||
disabled={disabled}
|
||||
readonly={disabled}
|
||||
onChange={(val) => updateProperty('default', val)}
|
||||
style={{ flexGrow: 1, height: 32 }}
|
||||
/>
|
||||
|
||||
@ -4,6 +4,8 @@ import { WorkflowPortRender } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { useNodeRenderContext } from '../../hooks';
|
||||
import { SidebarContext } from '../../context';
|
||||
// import { scrollToView } from './utils'
|
||||
// import { useClientContext } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export interface NodeWrapperProps {
|
||||
children: React.ReactNode;
|
||||
@ -18,6 +20,7 @@ export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
|
||||
const { selected, startDrag, ports, selectNode, nodeRef, onFocus, onBlur } = nodeRender;
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const sidebar = useContext(SidebarContext);
|
||||
// const ctx = useClientContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -32,6 +35,9 @@ export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
|
||||
selectNode(e);
|
||||
if (!isDragging) {
|
||||
sidebar.setNodeRender(nodeRender);
|
||||
// 可选:如果需要让节点滚动到画布中间加上这个
|
||||
// Optional: Add this if you want the node to scroll to the middle of the canvas
|
||||
// scrollToView(ctx, nodeRender.node)
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => setIsDragging(false)}
|
||||
|
||||
18
apps/demo-free-layout/src/components/base-node/utils.ts
Normal file
18
apps/demo-free-layout/src/components/base-node/utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { FreeLayoutPluginContext, FlowNodeEntity } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export function scrollToView(
|
||||
ctx: FreeLayoutPluginContext,
|
||||
node: FlowNodeEntity,
|
||||
sidebarWidth = 448
|
||||
) {
|
||||
const bounds = node.transform.bounds;
|
||||
ctx.playground.scrollToView({
|
||||
bounds,
|
||||
scrollDelta: {
|
||||
x: sidebarWidth / 2,
|
||||
y: 0,
|
||||
},
|
||||
zoom: 1,
|
||||
scrollToCenter: true,
|
||||
});
|
||||
}
|
||||
@ -53,7 +53,7 @@ export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
||||
{props.useFx ? (
|
||||
<VariableSelector
|
||||
value={value.default}
|
||||
disabled={disabled}
|
||||
readonly={disabled}
|
||||
onChange={(val) => updateProperty('default', val)}
|
||||
style={{ flexGrow: 1, height: 32 }}
|
||||
/>
|
||||
|
||||
@ -57,6 +57,14 @@ export function useEditorProps(
|
||||
formMeta: defaultFormMeta,
|
||||
};
|
||||
},
|
||||
lineColor: {
|
||||
hidden: 'transparent',
|
||||
default: '#4d53e8',
|
||||
drawing: '#5DD6E3',
|
||||
hovered: '#37d0ff',
|
||||
selected: '#37d0ff',
|
||||
error: 'red',
|
||||
},
|
||||
/*
|
||||
* Check whether the line can be added
|
||||
* 判断是否连线
|
||||
@ -69,15 +77,15 @@ export function useEditorProps(
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Check whether the line can be deleted
|
||||
* 判断是否能删除连线
|
||||
* Check whether the line can be deleted, this triggers on the default shortcut `Bakspace` or `Delete`
|
||||
* 判断是否能删除连线, 这个会在默认快捷键 (Backspace or Delete) 触发
|
||||
*/
|
||||
canDeleteLine(ctx, line, newLineInfo, silent) {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Check whether the node can be deleted
|
||||
* 判断是否能删除节点
|
||||
* Check whether the node can be deleted, this triggers on the default shortcut `Bakspace` or `Delete`
|
||||
* 判断是否能删除节点, 这个会在默认快捷键 (Backspace or Delete) 触发
|
||||
*/
|
||||
canDeleteNode(ctx, node) {
|
||||
return true;
|
||||
@ -95,6 +103,7 @@ export function useEditorProps(
|
||||
if (toPort) {
|
||||
return;
|
||||
}
|
||||
// Open add panel
|
||||
await nodePanelService.call({
|
||||
fromPort,
|
||||
toPort: undefined,
|
||||
|
||||
@ -19,17 +19,33 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||
'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',
|
||||
},
|
||||
meta: {
|
||||
/**
|
||||
* Mark as subcanvas
|
||||
* 子画布标记
|
||||
*/
|
||||
isContainer: true,
|
||||
/**
|
||||
* The subcanvas default size setting
|
||||
* 子画布默认大小设置
|
||||
*/
|
||||
size: {
|
||||
width: 560,
|
||||
height: 400,
|
||||
},
|
||||
/**
|
||||
* The subcanvas padding setting
|
||||
* 子画布 padding 设置
|
||||
*/
|
||||
padding: () => ({
|
||||
top: 150,
|
||||
bottom: 100,
|
||||
left: 100,
|
||||
right: 100,
|
||||
}),
|
||||
/**
|
||||
* Controls the node selection status within the subcanvas
|
||||
* 控制子画布内的节点选中状态
|
||||
*/
|
||||
selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {
|
||||
if (!mousePos) {
|
||||
return true;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
outline: true
|
||||
---
|
||||
|
||||
# Free Layout - Basic Usage
|
||||
# Basic Usage
|
||||
|
||||
import { FreeLayoutSimplePreview } from '../../../../components';
|
||||
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
[
|
||||
{
|
||||
"type": "dir",
|
||||
"name": "free-layout",
|
||||
"label": "Free Layout"
|
||||
},
|
||||
{
|
||||
"type": "dir",
|
||||
"name": "fixed-layout",
|
||||
"label": "Fixed Layout"
|
||||
},
|
||||
"form",
|
||||
"variable",
|
||||
"history",
|
||||
"lines",
|
||||
"custom-service",
|
||||
"shortcuts",
|
||||
"minimap",
|
||||
"custom-plugin",
|
||||
{
|
||||
"type": "dir",
|
||||
"name": "interactive",
|
||||
"label": "Interactive"
|
||||
}
|
||||
"custom-service"
|
||||
]
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
[
|
||||
"load",
|
||||
"node"
|
||||
]
|
||||
|
||||
181
apps/docs/src/en/guide/advanced/fixed-layout/load.mdx
Normal file
181
apps/docs/src/en/guide/advanced/fixed-layout/load.mdx
Normal file
@ -0,0 +1,181 @@
|
||||
# Loading and Saving
|
||||
|
||||
Canvas data is stored through [FlowDocument](/api/core/flow-document.html)
|
||||
|
||||
## Canvas Data Format
|
||||
|
||||
Canvas document data uses a tree structure that supports nesting
|
||||
|
||||
:::note Document Data Basic Structure:
|
||||
|
||||
- nodes `array` Node list, supports nesting
|
||||
|
||||
:::
|
||||
|
||||
:::note Node Data Basic Structure:
|
||||
|
||||
- id: `string` Node unique identifier, must be unique
|
||||
- meta: `object` Node UI configuration information, such as free layout `position` information is stored here
|
||||
- type: `string | number` Node type, corresponds to the `type` in `nodeRegistries`
|
||||
- data: `object` Node form data
|
||||
- blocks: `array` Node branches, using `block` is closer to `Gramming`
|
||||
|
||||
:::
|
||||
|
||||
```tsx pure title="initial-data.tsx"
|
||||
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
/**
|
||||
* Configure flow data, data is in blocks nested format
|
||||
*/
|
||||
export const initialData: FlowDocumentJSON = {
|
||||
nodes: [
|
||||
// Start node
|
||||
{
|
||||
id: 'start_0',
|
||||
type: 'start',
|
||||
data: {
|
||||
title: 'Start',
|
||||
content: 'start content'
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
// Condition node
|
||||
{
|
||||
id: 'condition_0',
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: 'Condition'
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'branch_0',
|
||||
type: 'block',
|
||||
data: {
|
||||
title: 'Branch 0',
|
||||
content: 'branch 1 content'
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'custom_0',
|
||||
type: 'custom',
|
||||
data: {
|
||||
title: 'Custom',
|
||||
content: 'custrom content'
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'branch_1',
|
||||
type: 'block',
|
||||
data: {
|
||||
title: 'Branch 1',
|
||||
content: 'branch 1 content'
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
// End node
|
||||
{
|
||||
id: 'end_0',
|
||||
type: 'end',
|
||||
data: {
|
||||
title: 'End',
|
||||
content: 'end content'
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Loading
|
||||
|
||||
- Loading through initialData
|
||||
|
||||
```tsx pure
|
||||
import { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'
|
||||
|
||||
function App({ data }) {
|
||||
return (
|
||||
<FixedLayoutEditorProvider initialData={data} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FixedLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- Dynamic loading through ref
|
||||
|
||||
```tsx pure
|
||||
import { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'
|
||||
|
||||
function App() {
|
||||
const ref = useRef<FixedLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(async () => {
|
||||
const data = await request('https://xxxx/getJSON')
|
||||
ref.current.document.fromJSON(data)
|
||||
setTimeout(() => {
|
||||
// Trigger canvas fitview after loading to center nodes automatically
|
||||
ref.current.playground.config.fitView(ref.current.document.root.bounds.pad(30));
|
||||
}, 100)
|
||||
}, [])
|
||||
return (
|
||||
<FixedLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FixedLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- Dynamically reload data
|
||||
|
||||
```tsx pure
|
||||
import { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'
|
||||
|
||||
function App({ data }) {
|
||||
const ref = useRef<FixedLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(async () => {
|
||||
// Reload canvas data when data changes
|
||||
await ref.current.document.fromJSON(data)
|
||||
setTimeout(() => {
|
||||
// Trigger canvas fitview after loading to center nodes automatically
|
||||
ref.current.playground.config.fitView(ref.current.document.root.bounds.pad(30));
|
||||
}, 100)
|
||||
}, [data])
|
||||
return (
|
||||
<FixedLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FixedLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Monitor Changes and Auto-save
|
||||
|
||||
```tsx pure
|
||||
import { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
function App() {
|
||||
const ref = useRef<FixedLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
// Monitor canvas changes, delay 1 second to save data, avoid frequent canvas updates
|
||||
const toDispose = ref.current.history.onApply(debounce(() => {
|
||||
// Get the latest canvas data through toJSON
|
||||
request('https://xxxx/save', {
|
||||
data: ref.current.document.toJSON()
|
||||
})
|
||||
}, 1000))
|
||||
return () => toDispose.dispose()
|
||||
}, [])
|
||||
return (
|
||||
<FixedLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FixedLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
215
apps/docs/src/en/guide/advanced/fixed-layout/node.mdx
Normal file
215
apps/docs/src/en/guide/advanced/fixed-layout/node.mdx
Normal file
@ -0,0 +1,215 @@
|
||||
# Nodes
|
||||
|
||||
Nodes are defined through [FlowNodeEntity](/api/core/flow-node-entity.html)
|
||||
|
||||
## Node Data
|
||||
|
||||
Can be obtained through `node.toJSON()`
|
||||
|
||||
:::note Basic Structure:
|
||||
|
||||
- id: `string` Node unique identifier, must be unique
|
||||
- meta: `object` Node UI configuration information, such as free layout `position` information is stored here
|
||||
- type: `string | number` Node type, corresponds to `type` in `nodeRegistries`
|
||||
- data: `object` Node form data, can be customized by business
|
||||
- blocks: `array` Node branches, using `block` is closer to `Gramming`
|
||||
|
||||
:::
|
||||
|
||||
```ts pure
|
||||
const nodeData: FlowNodeJSON = {
|
||||
id: 'xxxx',
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: 'MyCondition',
|
||||
desc: 'xxxxx'
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Node Definition
|
||||
|
||||
Node declaration can be used to determine node type and rendering method
|
||||
|
||||
```tsx pure
|
||||
import { FlowNodeRegistry, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
/**
|
||||
* Custom node registration
|
||||
*/
|
||||
export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
{
|
||||
/**
|
||||
* Custom node type
|
||||
*/
|
||||
type: 'condition',
|
||||
/**
|
||||
* Custom node extension:
|
||||
* - loop: Extend as loop node
|
||||
* - start: Extend as start node
|
||||
* - dynamicSplit: Extend as branch node
|
||||
* - end: Extend as end node
|
||||
* - tryCatch: Extend as tryCatch node
|
||||
* - default: Extend as normal node (default)
|
||||
*/
|
||||
extend: 'dynamicSplit',
|
||||
/**
|
||||
* Node configuration information
|
||||
*/
|
||||
meta: {
|
||||
// isStart: false, // Whether it's a start node
|
||||
// isNodeEnd: false, // Whether it's an end node, no nodes can be added after end node
|
||||
// draggable: false, // Whether draggable, start and end nodes cannot be dragged
|
||||
// selectable: false, // Triggers and start nodes cannot be box-selected
|
||||
// deleteDisable: true, // Disable deletion
|
||||
// copyDisable: true, // Disable copying
|
||||
// addDisable: true, // Disable adding
|
||||
},
|
||||
/**
|
||||
* Configure node form validation and rendering,
|
||||
* Note: validate uses data and rendering separation to ensure nodes can validate data even without rendering
|
||||
*/
|
||||
formMeta: {
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
title: ({ value }) => (value ? undefined : 'Title is required'),
|
||||
},
|
||||
/**
|
||||
* Render form
|
||||
*/
|
||||
render: () => (
|
||||
<>
|
||||
<Field name="title">
|
||||
{({ field }) => <div className="demo-free-node-title">{field.value}</div>}
|
||||
</Field>
|
||||
<Field name="content">
|
||||
{({ field }) => <input onChange={field.onChange} value={field.value}/>}
|
||||
</Field>
|
||||
</>
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## Getting Current Rendered Node
|
||||
|
||||
Get node-related methods through [useNodeRender](/api/hooks/use-node-render.html)
|
||||
|
||||
## Creating Nodes
|
||||
|
||||
Create through [FlowOperationService](/api/services/flow-operation-service.html)
|
||||
|
||||
- Add node
|
||||
|
||||
```ts pure
|
||||
const ctx = useClientContext()
|
||||
|
||||
ctx.operation.addNode({
|
||||
id: 'xxx', // Must be unique within canvas
|
||||
type: 'custom',
|
||||
meta: {},
|
||||
data: {}, // Form-related data
|
||||
blocks: [], // Child nodes
|
||||
parent: someParent // Parent node, used for branches
|
||||
})
|
||||
```
|
||||
|
||||
- Add after specified node
|
||||
|
||||
```ts pure
|
||||
const ctx = useClientContext()
|
||||
|
||||
ctx.operation.addFromNode(targetNode, {
|
||||
id: 'xxx', // Must be unique within canvas
|
||||
type: 'custom',
|
||||
meta: {},
|
||||
data: {}, // Form-related data
|
||||
blocks: [], // Child nodes
|
||||
})
|
||||
```
|
||||
|
||||
- Add branch node (used for conditional branches)
|
||||
|
||||
```ts pure
|
||||
const ctx = useClientContext()
|
||||
|
||||
ctx.operation.addBlock(parentNode, {
|
||||
id: 'xxx', // Must be unique within canvas
|
||||
type: 'block',
|
||||
meta: {},
|
||||
data: {}, // Form-related data
|
||||
blocks: [], // Child nodes
|
||||
})
|
||||
```
|
||||
|
||||
## Deleting Nodes
|
||||
|
||||
```tsx pure
|
||||
function BaseNode({ node }) {
|
||||
const ctx = useClientContext()
|
||||
function onClick() {
|
||||
ctx.operation.deleteNode(node)
|
||||
}
|
||||
return (
|
||||
<button onClick={onClick}>Delete</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Updating Node Data
|
||||
|
||||
- Get node data through [useNodeRender](/api/hooks/use-node-render.html) or [getNodeForm](/api/utils/get-node-form.html)
|
||||
|
||||
```tsx pure
|
||||
function BaseNode() {
|
||||
const { form } = useNodeRender();
|
||||
// Corresponding node data
|
||||
console.log(form.values)
|
||||
|
||||
// Monitor node data changes
|
||||
useEffect(() => {
|
||||
const toDispose = form.onFormValuesChange(() => {
|
||||
// changed
|
||||
})
|
||||
return () => toDispose.dispose()
|
||||
}, [form])
|
||||
|
||||
function onChange(e) {
|
||||
form.setValueIn('title', e.target.value)
|
||||
}
|
||||
return <input value={form.getValueIn('title')} onChange={onChange}/>
|
||||
}
|
||||
```
|
||||
|
||||
- Update form data through Field, see [Form Usage](/guide/advanced/form.html) for details
|
||||
|
||||
```tsx pure
|
||||
function FormRender() {
|
||||
return (
|
||||
<Field name="title">
|
||||
<Input />
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Updating Node ExtInfo Data
|
||||
|
||||
ExtInfo is used to store some UI states. If node engine is not enabled, node data will be stored in extInfo by default
|
||||
|
||||
```tsx pure
|
||||
function BaseNode({ node }) {
|
||||
const times = node.getExtInfo()?.times || 0
|
||||
function onClick() {
|
||||
node.updateExtInfo({ times: times ++ })
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span>Click Times: {times}</span>
|
||||
<button onClick={onClick}>Click</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@ -1,2 +1,7 @@
|
||||
[
|
||||
"load",
|
||||
"node",
|
||||
"line",
|
||||
"port",
|
||||
"sub-canvas"
|
||||
]
|
||||
|
||||
320
apps/docs/src/en/guide/advanced/free-layout/line.mdx
Normal file
320
apps/docs/src/en/guide/advanced/free-layout/line.mdx
Normal file
@ -0,0 +1,320 @@
|
||||
# Lines
|
||||
|
||||
- [WorkflowLinesManager](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/workflow-lines-manager.ts) manages all lines
|
||||
- [WorkflowNodeLinesData](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-lines-data.ts) manages lines connected to nodes
|
||||
- [WorkflowLineEntity](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts) line entity
|
||||
|
||||
## Get All Line Entities
|
||||
|
||||
```ts pure
|
||||
const allLines = ctx.document.linesManager.getAllLines() // line entities containing from/to representing connected nodes
|
||||
```
|
||||
|
||||
## Create/Delete Lines
|
||||
|
||||
```ts pure
|
||||
// from and to are the node IDs to connect, fromPort, toPort are port IDs, if node has single port can be omitted
|
||||
const line = ctx.document.linesManager.createLine({ from, to, fromPort, toPort })
|
||||
|
||||
// delete line
|
||||
line.dispose()
|
||||
```
|
||||
|
||||
## Export Line Data
|
||||
|
||||
:::note Basic line structure:
|
||||
|
||||
- sourceNodeID: `string` source node id
|
||||
- targetNodeID: `string` target node id
|
||||
- sourcePortID?: `string | number` source port id, defaults to node's default port if omitted
|
||||
- targetPortID?: `string | number` target port id, defaults to node's default port if omitted
|
||||
|
||||
:::
|
||||
```ts pure
|
||||
const json = ctx.document.linesManager.toJSON()
|
||||
```
|
||||
|
||||
## Get Input/Output Nodes or Lines for Current Node
|
||||
|
||||
```ts pure
|
||||
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
// get input nodes (calculated through connection lines)
|
||||
node.geData(WorkflowNodeLinesData).inputNodes
|
||||
// get all input nodes (recursively gets all upstream nodes)
|
||||
node.geData(WorkflowNodeLinesData).allInputNodes
|
||||
// get output nodes
|
||||
node.geData(WorkflowNodeLinesData).outputNodes
|
||||
// get all output nodes
|
||||
node.geData(WorkflowNodeLinesData).allOutputNodes
|
||||
// input lines
|
||||
node.geData(WorkflowNodeLinesData).inputLines
|
||||
// output lines
|
||||
node.geData(WorkflowNodeLinesData).outputLines
|
||||
```
|
||||
|
||||
## Line Configuration
|
||||
|
||||
We provide rich line configuration parameters for `FreeLayoutEditorProvider`, see details in [FreeLayoutProps](https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/preset/free-layout-props.ts)
|
||||
|
||||
```tsx pure
|
||||
interface FreeLayoutProps {
|
||||
/**
|
||||
* Line color configuration
|
||||
*/
|
||||
lineColor?: LineColor;
|
||||
/**
|
||||
* Determine if line should be marked red
|
||||
* @param ctx
|
||||
* @param fromPort
|
||||
* @param toPort
|
||||
* @param lines
|
||||
*/
|
||||
isErrorLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity | undefined, lines: WorkflowLinesManager) => boolean;
|
||||
/**
|
||||
* Determine if port should be marked red
|
||||
* @param ctx
|
||||
* @param port
|
||||
*/
|
||||
isErrorPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;
|
||||
/**
|
||||
* Determine if port should be disabled
|
||||
* @param ctx
|
||||
* @param port
|
||||
*/
|
||||
isDisabledPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;
|
||||
/**
|
||||
* Determine if line arrow should be reversed
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isReverseLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* Determine if line arrow should be hidden
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isHideArrowLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* Determine if line should show flowing effect
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isFlowingLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* Determine if line should be disabled
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isDisabledLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* Determine if line should be vertical
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isVerticalLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* Line drag end
|
||||
* @param ctx
|
||||
* @param params
|
||||
*/
|
||||
onDragLineEnd?: (ctx: FreeLayoutPluginContext, params: onDragLineEndParams) => Promise<void>;
|
||||
/**
|
||||
* Set line renderer type
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
setLineRenderType?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => LineRenderType | undefined;
|
||||
/**
|
||||
* Set line style
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
setLineClassName?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => string | undefined;
|
||||
/**
|
||||
* Whether to allow line creation
|
||||
* @param ctx
|
||||
* @param fromPort - start point
|
||||
* @param toPort - target point
|
||||
*/
|
||||
canAddLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity, lines: WorkflowLinesManager, silent?: boolean) => boolean;
|
||||
/**
|
||||
* Whether to allow line deletion
|
||||
* @param ctx
|
||||
* @param line - target line
|
||||
* @param newLineInfo - new line info
|
||||
* @param silent - if false, can show toast
|
||||
*/
|
||||
canDeleteLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity, newLineInfo?: Required<WorkflowLinePortInfo>, silent?: boolean) => boolean;
|
||||
/**
|
||||
* Whether to allow line reset
|
||||
* @param fromPort - start point
|
||||
* @param oldToPort - old connection point
|
||||
* @param newToPort - new connection point
|
||||
* @param lines - line manager
|
||||
*/
|
||||
canResetLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, oldToPort: WorkflowPortEntity, newToPort: WorkflowPortEntity, lines: WorkflowLinesManager) => boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 1. Custom Colors
|
||||
|
||||
```tsx pure
|
||||
|
||||
function App() {
|
||||
const editorProps: FreeLayoutProps = {
|
||||
lineColor: {
|
||||
hidden: 'transparent',
|
||||
default: '#4d53e8',
|
||||
drawing: '#5DD6E3',
|
||||
hovered: '#37d0ff',
|
||||
selected: '#37d0ff',
|
||||
error: 'red',
|
||||
},
|
||||
// ...others
|
||||
}
|
||||
return (
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Limit Single Output Port to One Line
|
||||
|
||||
<img loading="lazy" style={{ width: 500, margin: '0 auto' }} className="invert-img" src="/free-layout/line-limit.gif"/>
|
||||
|
||||
```tsx pure
|
||||
|
||||
function App() {
|
||||
const editorProps: FreeLayoutProps = {
|
||||
/*
|
||||
* Check whether the line can be added
|
||||
*/
|
||||
canAddLine(ctx, fromPort, toPort) {
|
||||
// not the same node
|
||||
if (fromPort.node === toPort.node) {
|
||||
return false;
|
||||
}
|
||||
// control number of lines
|
||||
if (fromPort.availableLines.length >= 1) {
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// ...others
|
||||
}
|
||||
return (
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Add Node by Connecting to Empty Space
|
||||
|
||||
See code in free layout best practices
|
||||
|
||||
<img loading="lazy" style={{ width: 500, margin: '0 auto' }} className="invert-img" src="/free-layout/line-add-panel.gif"/>
|
||||
|
||||
```tsx pure
|
||||
|
||||
function App() {
|
||||
const editorProps: FreeLayoutProps = {
|
||||
/**
|
||||
* Drag the end of the line to create an add panel (feature optional)
|
||||
*/
|
||||
async onDragLineEnd(ctx, params) {
|
||||
const { fromPort, toPort, mousePos, line, originLine } = params;
|
||||
if (originLine || !line) {
|
||||
return;
|
||||
}
|
||||
if (toPort) {
|
||||
return;
|
||||
}
|
||||
// Can open add panel based on mousePos here
|
||||
await ctx.get(WorkflowNodePanelService).call({
|
||||
fromPort,
|
||||
toPort: undefined,
|
||||
panelPosition: mousePos,
|
||||
enableBuildLine: true,
|
||||
panelProps: {
|
||||
enableNodePlaceholder: true,
|
||||
enableScrollClose: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
// ...others
|
||||
}
|
||||
return (
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Add Label to Line
|
||||
|
||||
See code in free layout best practices
|
||||
|
||||
<img loading="lazy" style={{ width: 500, margin: '0 auto' }} className="invert-img" src="/free-layout/line-add-button.gif"/>
|
||||
|
||||
```ts pure
|
||||
|
||||
import { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';
|
||||
|
||||
const editorProps = {
|
||||
plugins: () => [
|
||||
/**
|
||||
* Line render plugin
|
||||
*/
|
||||
createFreeLinesPlugin({
|
||||
renderInsideLine: LineAddButton,
|
||||
}),
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Node Listening to Its Own Line Changes and Refresh
|
||||
|
||||
```tsx pure
|
||||
|
||||
import {
|
||||
useRefresh,
|
||||
WorkflowNodeLinesData,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
function NodeRender({ node }) {
|
||||
const refresh = useRefresh()
|
||||
const linesData = node.get(WorkflowNodeLinesData)
|
||||
useEffect(() => {
|
||||
const dispose = linesData.onDataChange(() => refresh())
|
||||
return () => dispose.dispose()
|
||||
}, [])
|
||||
return <div>xxxx</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Listen to All Line Connection Changes
|
||||
|
||||
This scenario is used when you want to monitor line connections in external components
|
||||
|
||||
```ts pure
|
||||
import { useEffect } from 'react'
|
||||
import { WorkflowLinesManager, useRefresh } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
|
||||
function SomeReact() {
|
||||
const refresh = useRefresh()
|
||||
const linesManager = useService(WorkflowLinesManager)
|
||||
useEffect(() => {
|
||||
const dispose = linesManager.onAvailableLinesChange(() => refresh())
|
||||
return () => dispose.dispose()
|
||||
}, [])
|
||||
console.log(ctx.document.linesManager.getAllLines())
|
||||
}
|
||||
```
|
||||
|
||||
175
apps/docs/src/en/guide/advanced/free-layout/load.mdx
Normal file
175
apps/docs/src/en/guide/advanced/free-layout/load.mdx
Normal file
@ -0,0 +1,175 @@
|
||||
# Loading and Saving
|
||||
|
||||
Canvas data is stored through [WorkflowDocument](/api/core/workflow-document.html)
|
||||
|
||||
## Canvas Data
|
||||
|
||||
:::note Basic Document Structure:
|
||||
|
||||
- nodes `array` List of nodes, supports nesting
|
||||
- edges `array` List of edges
|
||||
|
||||
:::
|
||||
|
||||
:::note Basic Node Structure:
|
||||
|
||||
- id: `string` Unique node identifier, must be unique
|
||||
- meta: `object` Node UI configuration information, such as `position` information for free layout
|
||||
- type: `string | number` Node type, corresponds to `type` in `nodeRegistries`
|
||||
- data: `object` Node form data, customizable by business
|
||||
- blocks: `array` Node branches, using `block` is closer to `Gramming`, currently stores nodes of sub-canvas
|
||||
- edges: `array` Edge data of sub-canvas
|
||||
|
||||
:::
|
||||
|
||||
:::note Basic Edge Structure:
|
||||
|
||||
- sourceNodeID: `string` Starting node id
|
||||
- targetNodeID: `string` Target node id
|
||||
- sourcePortID?: `string | number` Starting port id, defaults to the default port of the starting node if omitted
|
||||
- targetPortID?: `string | number` Target port id, defaults to the default port of the target node if omitted
|
||||
|
||||
:::
|
||||
|
||||
```tsx pure title="initial-data.ts"
|
||||
import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export const initialData: WorkflowJSON = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'start_0',
|
||||
type: 'start',
|
||||
meta: {
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
data: {
|
||||
title: 'Start',
|
||||
content: 'Start content'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'node_0',
|
||||
type: 'custom',
|
||||
meta: {
|
||||
position: { x: 400, y: 0 },
|
||||
},
|
||||
data: {
|
||||
title: 'Custom',
|
||||
content: 'Custom node content'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'end_0',
|
||||
type: 'end',
|
||||
meta: {
|
||||
position: { x: 800, y: 0 },
|
||||
},
|
||||
data: {
|
||||
title: 'End',
|
||||
content: 'End content'
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: 'start_0',
|
||||
targetNodeID: 'node_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'node_0',
|
||||
targetNodeID: 'end_0',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Loading
|
||||
|
||||
- Load through initialData
|
||||
|
||||
```tsx pure
|
||||
import { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
function App({ data }) {
|
||||
return (
|
||||
<FreeLayoutEditorProvider initialData={data} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- Dynamic loading through ref
|
||||
|
||||
```tsx pure
|
||||
import { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
function App() {
|
||||
const ref = useRef<FreeLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(async () => {
|
||||
const data = await request('https://xxxx/getJSON')
|
||||
ref.current.document.fromJSON(data)
|
||||
setTimeout(() => {
|
||||
// Trigger canvas fitview after loading to center nodes automatically
|
||||
ref.current.document.fitView()
|
||||
}, 100)
|
||||
}, [])
|
||||
return (
|
||||
<FreeLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- Dynamic reload of all data
|
||||
|
||||
```tsx pure
|
||||
import { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
function App({ data }) {
|
||||
const ref = useRef<FreeLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(async () => {
|
||||
// Reload canvas data when data changes
|
||||
await ref.current.document.reload(data)
|
||||
setTimeout(() => {
|
||||
// Trigger canvas fitview after loading to center nodes automatically
|
||||
ref.current.document.fitView()
|
||||
}, 100)
|
||||
}, [data])
|
||||
return (
|
||||
<FreeLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Listen for Changes and Auto-Save
|
||||
|
||||
```tsx pure
|
||||
import { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
function App() {
|
||||
const ref = useRef<FreeLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for canvas changes and save data after 1 second delay to avoid frequent updates
|
||||
const toDispose = ref.current.document.onContentChange(debounce(() => {
|
||||
// Get the latest canvas data through toJSON
|
||||
request('https://xxxx/save', {
|
||||
data: ref.current.document.toJSON()
|
||||
})
|
||||
}, 1000))
|
||||
return () => toDispose.dispose()
|
||||
}, [])
|
||||
return (
|
||||
<FreeLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
148
apps/docs/src/en/guide/advanced/free-layout/node.mdx
Normal file
148
apps/docs/src/en/guide/advanced/free-layout/node.mdx
Normal file
@ -0,0 +1,148 @@
|
||||
# Node
|
||||
|
||||
Nodes are defined through [FlowNodeEntity](/api/core/flow-node-entity.html)
|
||||
|
||||
## Node Data
|
||||
|
||||
Can be obtained through `node.toJSON()`
|
||||
|
||||
:::note Basic Structure:
|
||||
|
||||
- id: `string` Unique identifier for the node, must be unique
|
||||
- meta: `object` Node's UI configuration information, such as `position` information for free layout
|
||||
- type: `string | number` Node type, corresponds to `type` in `nodeRegistries`
|
||||
- data: `object` Node form data, can be customized by business
|
||||
- blocks: `array` Node branches, using `block` is more suitable for `Gramming` free layout scenarios, used in sub-nodes of sub-canvas
|
||||
- edges: `array` Edge data of sub-canvas
|
||||
|
||||
:::
|
||||
|
||||
```ts pure
|
||||
const nodeData: FlowNodeJSON = {
|
||||
id: 'xxxx',
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: 'MyCondition',
|
||||
desc: 'xxxxx'
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Node Definition
|
||||
|
||||
In free layout scenarios, node definition is used to declare node's initial position/size, ports, form rendering, etc. For details, see [Declare Node](guide/getting-started/create-free-layout-simple.html#4-declare-nodes)
|
||||
|
||||
## Get Current Rendering Node
|
||||
|
||||
Get node-related methods through [useNodeRender](api/hooks/use-node-render.html)
|
||||
|
||||
## Create Node
|
||||
|
||||
- Through [WorkflowDocument](/api/core/workflow-document.html)
|
||||
|
||||
```ts pure
|
||||
const ctx = useClientContext()
|
||||
|
||||
ctx.document.createWorkflowNode({
|
||||
id: 'xxx', // Must be unique within the canvas
|
||||
type: 'custom',
|
||||
meta: {
|
||||
/**
|
||||
* If not provided, defaults to creating in the center of the canvas
|
||||
* To get position from mouse position (e.g., creating node by clicking anywhere on canvas),
|
||||
* convert using `ctx.playground.config.getPosFromMouseEvent(mouseEvent)`
|
||||
*/
|
||||
position: { x: 100, y: 100 } //
|
||||
},
|
||||
data: {}, // Form-related data
|
||||
blocks: [], // Sub-canvas nodes
|
||||
edges: [] // Sub-canvas edges
|
||||
})
|
||||
|
||||
```
|
||||
- Through WorkflowDragService, see [Free Layout Basic Usage](/examples/free-layout/free-layout-simple.html)
|
||||
|
||||
```ts pure
|
||||
const dragService = useService<WorkflowDragService>(WorkflowDragService);
|
||||
|
||||
// mouseEvent here will automatically convert to canvas position
|
||||
dragService.startDragCard(nodeType, mouseEvent, {
|
||||
id: 'xxxx',
|
||||
data: {}, // Form-related data
|
||||
blocks: [], // Sub-canvas nodes
|
||||
edges: [] // Sub-canvas edges
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
## Delete Node
|
||||
|
||||
Delete node through `node.dispose`
|
||||
|
||||
```tsx pure
|
||||
function BaseNode({ node }) {
|
||||
function onClick() {
|
||||
node.dispose()
|
||||
}
|
||||
return (
|
||||
<button onClick={onClick}>Delete</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Update Node Data
|
||||
|
||||
- Get node's data through [useNodeRender](/api/hooks/use-node-render.html) or [getNodeForm](/api/utils/get-node-form.html)
|
||||
|
||||
```tsx pure
|
||||
function BaseNode() {
|
||||
const { form } = useNodeRender();
|
||||
// Corresponds to node's data
|
||||
console.log(form.values)
|
||||
|
||||
// Listen to node data changes
|
||||
useEffect(() => {
|
||||
const toDispose = form.onFormValuesChange(() => {
|
||||
// changed
|
||||
})
|
||||
return () => toDispose.dispose()
|
||||
}, [form])
|
||||
|
||||
function onChange(e) {
|
||||
form.setValueIn('title', e.target.value)
|
||||
}
|
||||
return <input value={form.getValueIn('title')} onChange={onChange}/>
|
||||
}
|
||||
```
|
||||
- Update form data through Field, see details in [Form Usage](/guide/advanced/form.html)
|
||||
|
||||
```tsx pure
|
||||
|
||||
function FormRender() {
|
||||
return (
|
||||
<Field name="title">
|
||||
<Input />
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Update Node's extInfo Data
|
||||
|
||||
extInfo is used to store UI states, if node engine is not enabled, node's data will be stored in extInfo by default
|
||||
|
||||
```tsx pure
|
||||
function BaseNode({ node }) {
|
||||
const times = node.getExtInfo()?.times || 0
|
||||
function onClick() {
|
||||
node.updateExtInfo({ times: times ++ })
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span>Click Times: {times}</span>
|
||||
<button onClick={onClick}>Click</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
90
apps/docs/src/en/guide/advanced/free-layout/port.mdx
Normal file
90
apps/docs/src/en/guide/advanced/free-layout/port.mdx
Normal file
@ -0,0 +1,90 @@
|
||||
# Port
|
||||
|
||||
- [WorkflowNodePortsData](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-ports-data.ts) manages all port information for nodes
|
||||
- [WorkflowPortEntity](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts) port instance
|
||||
- [WorkflowPortRender](https://github.com/bytedance/flowgram.ai/blob/main/packages/plugins/free-lines-plugin/src/components/workflow-port-render/index.tsx) port rendering component
|
||||
|
||||
|
||||
## Define Ports
|
||||
|
||||
- Static Ports
|
||||
|
||||
Add `defaultPorts` to node declaration, such as `{ type: 'input' }`, which will add an input port to the left side of the node
|
||||
|
||||
```ts pure title="node-registries.ts"
|
||||
{
|
||||
type: 'start',
|
||||
meta: {
|
||||
defaultPorts: [{ type: 'output' }, { type: 'input'}]
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
```ts pure title="node-registries.ts"
|
||||
{
|
||||
type: 'condition',
|
||||
meta: {
|
||||
defaultPorts: [{ type: 'input'}]
|
||||
dynamicPort: true
|
||||
},
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```tsx pure
|
||||
|
||||
/**
|
||||
* Dynamic ports are found through querySelectorAll('[data-port-id]')
|
||||
*/
|
||||
function BaseNode() {
|
||||
return (
|
||||
<div>
|
||||
<div data-port-id="condition-if-0" data-port-type="output"></div>
|
||||
<div data-port-id="condition-if-1" data-port-type="output"></div>
|
||||
{/* others */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Port Rendering
|
||||
|
||||
Ports are ultimately rendered through the `WorkflowPortRender` component, supporting custom styles, or businesses can reimplement this component based on the source code, see [Free Layout Best Practices - Node Rendering](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/base-node/node-wrapper.tsx)
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { WorkflowPortRender, useNodeRender } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
function BaseNode() {
|
||||
const { ports } = useNodeRender();
|
||||
return (
|
||||
<div>
|
||||
<div data-port-id="condition-if-0" data-port-type="output"></div>
|
||||
<div data-port-id="condition-if-1" data-port-type="output"></div>
|
||||
{ports.map((p) => (
|
||||
<WorkflowPortRender key={p.id} entity={p} className="xxx" style={{ /* custom style */}}/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Get Port Data
|
||||
|
||||
```ts pure
|
||||
const ports = node.getData(WorkflowNodePortsData)
|
||||
|
||||
console.log(ports.inputPorts) // Get all input ports of the current node
|
||||
console.log(ports.outputPorts) // Get all output ports of the current node
|
||||
|
||||
console.log(ports.inputPorts.map(port => port.availableLines)) // Find connected lines through ports
|
||||
|
||||
ports.updateDynamicPorts() // When dynamic ports modify DOM structure or position, you can manually refresh port positions through this method (DOM rendering has delay, best to execute in useEffect or setTimeout)
|
||||
```
|
||||
|
||||
|
||||
82
apps/docs/src/en/guide/advanced/free-layout/sub-canvas.mdx
Normal file
82
apps/docs/src/en/guide/advanced/free-layout/sub-canvas.mdx
Normal file
@ -0,0 +1,82 @@
|
||||
# Sub-canvas
|
||||
|
||||
<img loading="lazy" className="invert-img" src="/free-layout/loop2.png"/>
|
||||
|
||||
For detailed code, see [Free Layout Best Practices](/examples/free-layout/free-feature-overview.html)
|
||||
|
||||
## Add Sub-canvas Plugin
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
|
||||
|
||||
function App() {
|
||||
const editorProps = {
|
||||
plugins: () => [
|
||||
createContainerNodePlugin({}),
|
||||
]
|
||||
// ..others
|
||||
}
|
||||
return (
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Define Sub-canvas Node
|
||||
|
||||
```tsx pure
|
||||
import { SubCanvasRender } from '@flowgram.ai/free-container-plugin';
|
||||
|
||||
export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'loop',
|
||||
info: {
|
||||
icon: iconLoop,
|
||||
description:
|
||||
'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',
|
||||
},
|
||||
meta: {
|
||||
/**
|
||||
* Sub-canvas marker
|
||||
*/
|
||||
isContainer: true,
|
||||
/**
|
||||
* Sub-canvas default size settings
|
||||
*/
|
||||
size: {
|
||||
width: 560,
|
||||
height: 400,
|
||||
},
|
||||
/**
|
||||
* Sub-canvas padding settings
|
||||
*/
|
||||
padding: () => ({ // Container padding settings
|
||||
top: 150,
|
||||
bottom: 100,
|
||||
left: 100,
|
||||
right: 100,
|
||||
}),
|
||||
/**
|
||||
* Control node selection state within sub-canvas
|
||||
*/
|
||||
selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {
|
||||
if (!mousePos) {
|
||||
return true;
|
||||
}
|
||||
const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||
// Only selectable when mouse start position does not include current node
|
||||
return !transform.bounds.contains(mousePos.x, mousePos.y);
|
||||
},
|
||||
},
|
||||
formMeta: {
|
||||
render: () => (
|
||||
<div>
|
||||
{ /* others */ }
|
||||
<SubCanvasRender />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -1,4 +0,0 @@
|
||||
[
|
||||
"shortcuts",
|
||||
"minimap"
|
||||
]
|
||||
@ -1,14 +1,19 @@
|
||||
# ECS
|
||||
|
||||
## Why do we need ECS
|
||||
## Why ECS?
|
||||
|
||||
:::warning ECS (Entity-Component-System)
|
||||
It is suitable for decoupling large data objects and is often used in games. The data of each character (Entity) in a game is very large and needs to be split into data related to the physics engine, skin, character attributes, etc. (multiple Components), which are consumed by different subsystems (Systems). The data structure of the process is complex, and it is very suitable to be disassembled using ECS.
|
||||
ECS is suitable for decoupling large data objects, commonly used in games where each character (Entity) has extensive data that needs to be split into physics engine-related data, skin-related data, character attributes, etc. (multiple Components) for consumption by different subsystems (Systems). When workflow data structures are complex, ECS is very suitable for decomposition.
|
||||
:::
|
||||
|
||||
<img loading="lazy" className="invert-img" src="/ecs.png"/>
|
||||
|
||||
ReduxStore pseudo-code
|
||||
## Solution Comparison
|
||||
|
||||
Let's compare two data solutions:
|
||||
|
||||
### 1. ReduxStore Solution
|
||||
|
||||
```jsx pure
|
||||
const store = () => ({
|
||||
nodes: [{
|
||||
@ -26,69 +31,91 @@ function Playground() {
|
||||
return nodes.map(node => <Node data={node} />)
|
||||
}
|
||||
```
|
||||
|
||||
Advantages:
|
||||
- Centralized data management is easy to use.
|
||||
- Simple to use with centralized data management
|
||||
|
||||
Disadvantages:
|
||||
- Centralized data management cannot perform precise updates, leading to performance bottlenecks.
|
||||
- Poor scalability. When adding new data to a node, it is all coupled into a large JSON.
|
||||
- Centralized data management cannot update precisely, leading to performance bottlenecks
|
||||
- Poor extensibility, adding new node data couples everything into one large JSON
|
||||
|
||||
### 2. ECS Solution
|
||||
|
||||
ECS Solution
|
||||
Notes:
|
||||
- NodeData corresponds to ECS - Component.
|
||||
- Layer corresponds to ECS - System.
|
||||
- NodeData corresponds to ECS - Component
|
||||
- Layer corresponds to ECS - System
|
||||
|
||||
```jsx pure
|
||||
/**
|
||||
* Canvas document data
|
||||
*/
|
||||
class FlowDocument {
|
||||
dataDefines: [
|
||||
NodePositionData,
|
||||
NodeFormData,
|
||||
NodeLineData
|
||||
]
|
||||
nodeEntities: Entity[] = []
|
||||
/**
|
||||
* Node data definitions, node data will be instantiated when created
|
||||
*/
|
||||
nodeDefines: [
|
||||
NodePositionData,
|
||||
NodeFormData,
|
||||
NodeLineData
|
||||
]
|
||||
nodeEntities: Entity[] = []
|
||||
}
|
||||
|
||||
|
||||
class Entity {
|
||||
id: string // Only has an ID, no data.
|
||||
getData: (dataId: string) => EntityData
|
||||
/**
|
||||
* Node
|
||||
*/
|
||||
class FlowNodeEntity {
|
||||
id: string // Only has id, no data
|
||||
getData: (dataId: string) => EntityData
|
||||
}
|
||||
|
||||
// Render lines
|
||||
class LinesLayer {
|
||||
@observeEntityData(NodeLineData) lines
|
||||
render() {
|
||||
return lines.map(line => <Line data={line} />)
|
||||
}
|
||||
/**
|
||||
* Internally gets corresponding data via node.getData(NodeLineData), same below
|
||||
*/
|
||||
@observeEntityData(FlowNodeEntity, NodeLineData) lines: NodeLineData[]
|
||||
render() {
|
||||
// Render lines
|
||||
return this.lines.map(line => <Line data={line} />)
|
||||
}
|
||||
}
|
||||
|
||||
// Render node positions
|
||||
class NodePositionsLayer {
|
||||
@observeEntityData(NodePositionData) positions
|
||||
return() {
|
||||
|
||||
}
|
||||
@observeEntityData(FlowNodeEntity, NodePositionData) positions: NodePositionData[]
|
||||
render() {
|
||||
// Render positions and layout
|
||||
}
|
||||
}
|
||||
|
||||
// Render node forms
|
||||
class NodeFormsLayer {
|
||||
@observeEntityData(NodeFormData) contents
|
||||
return() {}
|
||||
class NodeFormsLayer {
|
||||
@observeEntityData(FlowNodeEntity, NodeFormData) contents: NodeFormData[]
|
||||
render() {
|
||||
// Render node content
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas instance, renders in layers via Layer
|
||||
*/
|
||||
class Playground {
|
||||
layers: [
|
||||
LinesLayer, // Line rendering
|
||||
NodePositionsLayer, // Position rendering
|
||||
NodeFormsLayer // Content rendering
|
||||
]
|
||||
render() {
|
||||
return this.layers.map(layer => layer.render())
|
||||
}
|
||||
layers: [
|
||||
LinesLayer, // Line rendering
|
||||
NodePositionsLayer, // Position rendering
|
||||
NodeFormsLayer // Content rendering
|
||||
],
|
||||
render() {
|
||||
// Canvas layer rendering
|
||||
return this.layers.map(layer => layer.render())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Advantages:
|
||||
- Node data is split for separate rendering control, enabling precise performance updates.
|
||||
- High scalability. When adding new node data, simply add a new XXXData + XXXLayer.
|
||||
- Node data is split for individual rendering control, enabling precise performance updates
|
||||
- Strong extensibility - adding new node data just requires adding a new XXXData + XXXLayer
|
||||
|
||||
Disadvantages:
|
||||
- There is a certain learning curve.
|
||||
- Has a certain learning curve
|
||||
|
||||
@ -137,6 +137,7 @@ Canvas document data uses a tree structure that supports nesting
|
||||
- type: `string | number` Node type, corresponds to `type` in `nodeRegistries`
|
||||
- data: `object` Node form data, customizable by business
|
||||
- blocks: `array` Node branches, using `block` is closer to `Gramming`
|
||||
- edges: `array` Edges for the sub-canvas
|
||||
|
||||
:::
|
||||
|
||||
|
||||
BIN
apps/docs/src/public/free-layout/line-add-button.gif
Normal file
BIN
apps/docs/src/public/free-layout/line-add-button.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
BIN
apps/docs/src/public/free-layout/line-add-panel.gif
Normal file
BIN
apps/docs/src/public/free-layout/line-add-panel.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
BIN
apps/docs/src/public/free-layout/line-limit.gif
Normal file
BIN
apps/docs/src/public/free-layout/line-limit.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
BIN
apps/docs/src/public/free-layout/loop2.png
Normal file
BIN
apps/docs/src/public/free-layout/loop2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@ -2,7 +2,7 @@
|
||||
outline: true
|
||||
---
|
||||
|
||||
# 自由布局 - 基础用法
|
||||
# 基础用法
|
||||
|
||||
import { FreeLayoutSimplePreview } from '../../../../components';
|
||||
|
||||
@ -336,9 +336,9 @@ import { MinimapService } from '@flowgram.ai/minimap-plugin';
|
||||
|
||||
export const Minimap: React.FC = () => {
|
||||
const minimapService = useService<MinimapService>(MinimapService);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className="demo-free-minimap"
|
||||
ref={minimapService?.setContainer}
|
||||
/>
|
||||
@ -539,9 +539,9 @@ export const nodeRegistries: Record<string, WorkflowNodeRegistry> = {
|
||||
|
||||
```tsx
|
||||
// 核心编辑器容器与渲染器
|
||||
import {
|
||||
FreeLayoutEditorProvider,
|
||||
EditorRenderer
|
||||
import {
|
||||
FreeLayoutEditorProvider,
|
||||
EditorRenderer
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
// 编辑器配置示例
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
[
|
||||
{
|
||||
"type": "dir",
|
||||
"name": "free-layout",
|
||||
"label": "自由布局"
|
||||
},
|
||||
{
|
||||
"type": "dir",
|
||||
"name": "fixed-layout",
|
||||
"label": "固定布局"
|
||||
},
|
||||
"form",
|
||||
"variable",
|
||||
"history",
|
||||
"lines",
|
||||
"custom-service",
|
||||
"shortcuts",
|
||||
"minimap",
|
||||
"custom-plugin",
|
||||
{
|
||||
"type": "dir",
|
||||
"name": "interactive",
|
||||
"label": "交互"
|
||||
}
|
||||
"custom-service"
|
||||
]
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
[
|
||||
"load",
|
||||
"node"
|
||||
]
|
||||
|
||||
189
apps/docs/src/zh/guide/advanced/fixed-layout/load.mdx
Normal file
189
apps/docs/src/zh/guide/advanced/fixed-layout/load.mdx
Normal file
@ -0,0 +1,189 @@
|
||||
# 加载与保存
|
||||
|
||||
画布的数据通过 [FlowDocument](/api/core/flow-document.html) 来存储
|
||||
|
||||
## 画布数据格式
|
||||
|
||||
画布文档数据采用树形结构,支持嵌套
|
||||
|
||||
:::note 文档数据基本结构:
|
||||
|
||||
- nodes `array` 节点列表, 支持嵌套
|
||||
|
||||
:::
|
||||
|
||||
:::note 节点数据基本结构:
|
||||
|
||||
|
||||
- id: `string` 节点唯一标识, 必须保证唯一
|
||||
- meta: `object` 节点的 ui 配置信息,如自由布局的 `position` 信息放这里
|
||||
- type: `string | number` 节点类型,会和 `nodeRegistries` 中的 `type` 对应
|
||||
- data: `object` 节点表单数据
|
||||
- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`
|
||||
|
||||
:::
|
||||
|
||||
```tsx pure title="initial-data.tsx"
|
||||
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
/**
|
||||
* 配置流程数据,数据为 blocks 嵌套的格式
|
||||
*/
|
||||
export const initialData: FlowDocumentJSON = {
|
||||
nodes: [
|
||||
// 开始节点
|
||||
{
|
||||
id: 'start_0',
|
||||
type: 'start',
|
||||
data: {
|
||||
title: 'Start',
|
||||
content: 'start content'
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
// 分支节点
|
||||
{
|
||||
id: 'condition_0',
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: 'Condition'
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'branch_0',
|
||||
type: 'block',
|
||||
data: {
|
||||
title: 'Branch 0',
|
||||
content: 'branch 1 content'
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'custom_0',
|
||||
type: 'custom',
|
||||
data: {
|
||||
title: 'Custom',
|
||||
content: 'custrom content'
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'branch_1',
|
||||
type: 'block',
|
||||
data: {
|
||||
title: 'Branch 1',
|
||||
content: 'branch 1 content'
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
// 结束节点
|
||||
{
|
||||
id: 'end_0',
|
||||
type: 'end',
|
||||
data: {
|
||||
title: 'End',
|
||||
content: 'end content'
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## 加载
|
||||
|
||||
- 通过 initialData 加载
|
||||
|
||||
```tsx pure
|
||||
import { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'
|
||||
|
||||
function App({ data }) {
|
||||
return (
|
||||
<FixedLayoutEditorProvider initialData={data} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FixedLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- 通过 ref 动态加载
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'
|
||||
|
||||
function App() {
|
||||
const ref = useRef<FixedLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(async () => {
|
||||
const data = await request('https://xxxx/getJSON')
|
||||
ref.current.document.fromJSON(data)
|
||||
setTimeout(() => {
|
||||
// 加载后触发画布的 fitview 让节点自动居中
|
||||
ref.current.playground.config.fitView(ref.current.document.root.bounds.pad(30));
|
||||
}, 100)
|
||||
}, [])
|
||||
return (
|
||||
<FixedLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FixedLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- 动态 reload 数据
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'
|
||||
|
||||
function App({ data }) {
|
||||
const ref = useRef<FixedLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(async () => {
|
||||
// 当 data 变化时候重新加载画布数据
|
||||
await ref.current.document.fromJSON(data)
|
||||
setTimeout(() => {
|
||||
// 加载后触发画布的 fitview 让节点自动居中
|
||||
ref.current.playground.config.fitView(ref.current.document.root.bounds.pad(30));
|
||||
}, 100)
|
||||
}, [data])
|
||||
return (
|
||||
<FixedLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FixedLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 监听变化并自动保存
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
function App() {
|
||||
const ref = useRef<FixedLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
// 监听画布变化 延迟 1 秒 保存数据, 避免画布频繁更新
|
||||
const toDispose = ref.current.history.onApply(debounce(() => {
|
||||
// 通过 toJSON 获取画布最新的数据
|
||||
request('https://xxxx/save', {
|
||||
data: ref.current.document.toJSON()
|
||||
})
|
||||
}, 1000))
|
||||
return () => toDispose.dispose()
|
||||
}, [])
|
||||
return (
|
||||
<FixedLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FixedLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
216
apps/docs/src/zh/guide/advanced/fixed-layout/node.mdx
Normal file
216
apps/docs/src/zh/guide/advanced/fixed-layout/node.mdx
Normal file
@ -0,0 +1,216 @@
|
||||
# 节点
|
||||
|
||||
节点通过 [FlowNodeEntity](/api/core/flow-node-entity.html) 定义
|
||||
|
||||
|
||||
## 节点数据
|
||||
|
||||
通过 `node.toJSON()` 可以获取
|
||||
|
||||
:::note 基本结构:
|
||||
|
||||
- id: `string` 节点唯一标识, 必须保证唯一
|
||||
- meta: `object` 节点的 ui 配置信息,如自由布局的 `position` 信息放这里
|
||||
- type: `string | number` 节点类型,会和 `nodeRegistries` 中的 `type` 对应
|
||||
- data: `object` 节点表单数据, 业务可自定义
|
||||
- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`
|
||||
|
||||
:::
|
||||
|
||||
```ts pure
|
||||
const nodeData: FlowNodeJSON = {
|
||||
id: 'xxxx',
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: 'MyCondition',
|
||||
desc: 'xxxxx'
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 节点定义
|
||||
|
||||
声明节点可以用于确定节点的类型及渲染方式
|
||||
|
||||
```tsx pure
|
||||
import { FlowNodeRegistry, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
/**
|
||||
* 自定义节点注册
|
||||
*/
|
||||
export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
{
|
||||
/**
|
||||
* 自定义节点类型
|
||||
*/
|
||||
type: 'condition',
|
||||
/**
|
||||
* 自定义节点扩展:
|
||||
* - loop: 扩展为循环节点
|
||||
* - start: 扩展为开始节点
|
||||
* - dynamicSplit: 扩展为分支节点
|
||||
* - end: 扩展为结束节点
|
||||
* - tryCatch: 扩展为 tryCatch 节点
|
||||
* - default: 扩展为普通节点 (默认)
|
||||
*/
|
||||
extend: 'dynamicSplit',
|
||||
/**
|
||||
* 节点配置信息
|
||||
*/
|
||||
meta: {
|
||||
// isStart: false, // 是否为开始节点
|
||||
// isNodeEnd: false, // 是否为结束节点,结束节点后边无法再添加节点
|
||||
// draggable: false, // 是否可拖拽,如开始节点和结束节点无法拖拽
|
||||
// selectable: false, // 触发器等开始节点不能被框选
|
||||
// deleteDisable: true, // 禁止删除
|
||||
// copyDisable: true, // 禁止copy
|
||||
// addDisable: true, // 禁止添加
|
||||
},
|
||||
/**
|
||||
* 配置节点表单的校验及渲染,
|
||||
* 注:validate 采用数据和渲染分离,保证节点即使不渲染也能对数据做校验
|
||||
*/
|
||||
formMeta: {
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
title: ({ value }) => (value ? undefined : 'Title is required'),
|
||||
},
|
||||
/**
|
||||
* Render form
|
||||
*/
|
||||
render: () => (
|
||||
<>
|
||||
<Field name="title">
|
||||
{({ field }) => <div className="demo-free-node-title">{field.value}</div>}
|
||||
</Field>
|
||||
<Field name="content">
|
||||
{({ field }) => <input onChange={field.onChange} value={field.value}/>}
|
||||
</Field>
|
||||
</>
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## 当前渲染节点获取
|
||||
|
||||
通过 [useNodeRender](/api/hooks/use-node-render.html) 获取节点相关方法
|
||||
|
||||
## 创建节点
|
||||
|
||||
通过 [FlowOperationService](/api/services/flow-operation-service.html) 创建
|
||||
|
||||
- 添加节点
|
||||
|
||||
```ts pure
|
||||
const ctx = useClientContext()
|
||||
|
||||
ctx.operation.addNode({
|
||||
id: 'xxx', // 要保证画布内唯一
|
||||
type: 'custom',
|
||||
meta: {},
|
||||
data: {}, // 表单相关数据
|
||||
blocks: [], // 子节点
|
||||
parent: someParent // 父亲节点,分支会用到
|
||||
})
|
||||
|
||||
```
|
||||
- 在指定节点之后添加
|
||||
|
||||
```ts pure
|
||||
const ctx = useClientContext()
|
||||
|
||||
ctx.operation.addFromNode(targetNode, {
|
||||
id: 'xxx', // 要保证画布内唯一
|
||||
type: 'custom',
|
||||
meta: {},
|
||||
data: {}, // 表单相关数据
|
||||
blocks: [], // 子节点
|
||||
})
|
||||
|
||||
```
|
||||
- 添加分支节点 (用于条件分支)
|
||||
|
||||
```ts pure
|
||||
const ctx = useClientContext()
|
||||
|
||||
ctx.operation.addBlock(parentNode, {
|
||||
id: 'xxx', // 要保证画布内唯一
|
||||
type: 'block',
|
||||
meta: {},
|
||||
data: {}, // 表单相关数据
|
||||
blocks: [], // 子节点
|
||||
})
|
||||
```
|
||||
|
||||
## 删除节点
|
||||
|
||||
```tsx pure
|
||||
function BaseNode({ node }) {
|
||||
const ctx = useClientContext()
|
||||
function onClick() {
|
||||
ctx.operation.deleteNode(node)
|
||||
}
|
||||
return (
|
||||
<button onClick={onClick}>Delete</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 更新节点 data 数据
|
||||
|
||||
- 通过 [useNodeRender](/api/hooks/use-node-render.html) 或 [getNodeForm](/api/utils/get-node-form.html) 获取节点的 data 数据
|
||||
|
||||
```tsx pure
|
||||
function BaseNode() {
|
||||
const { form } = useNodeRender();
|
||||
// 对应节点的 data 数据
|
||||
console.log(form.values)
|
||||
|
||||
// 监听节点的数据变化
|
||||
useEffect(() => {
|
||||
const toDispose = form.onFormValuesChange(() => {
|
||||
// changed
|
||||
})
|
||||
return () => toDispose.dispose()
|
||||
}, [form])
|
||||
|
||||
function onChange(e) {
|
||||
form.setValueIn('title', e.target.value)
|
||||
}
|
||||
return <input value={form.getValueIn('title')} onChange={onChange}/>
|
||||
}
|
||||
```
|
||||
- 通过 Field 更新表单数据, 详细见 [表单的使用](/guide/advanced/form.html)
|
||||
|
||||
```tsx pure
|
||||
|
||||
function FormRender() {
|
||||
return (
|
||||
<Field name="title">
|
||||
<Input />
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 更新节点的 extInfo 数据
|
||||
|
||||
extInfo 用于存储 一些 ui 状态, 如果未开启节点引擎,节点的 data 数据会默认存到 extInfo 里
|
||||
|
||||
```tsx pure
|
||||
function BaseNode({ node }) {
|
||||
const times = node.getExtInfo()?.times || 0
|
||||
function onClick() {
|
||||
node.updateExtInfo({ times: times ++ })
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span>Click Times: {times}</span>
|
||||
<button onClick={onClick}>Click</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@ -1,2 +1,7 @@
|
||||
[
|
||||
"load",
|
||||
"node",
|
||||
"line",
|
||||
"port",
|
||||
"sub-canvas"
|
||||
]
|
||||
|
||||
332
apps/docs/src/zh/guide/advanced/free-layout/line.mdx
Normal file
332
apps/docs/src/zh/guide/advanced/free-layout/line.mdx
Normal file
@ -0,0 +1,332 @@
|
||||
# 线条
|
||||
|
||||
- [WorkflowLinesManager](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/workflow-lines-manager.ts) 管理所有的线条
|
||||
- [WorkflowNodeLinesData](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-lines-data.ts) 节点上连接的线条管理
|
||||
- [WorkflowLineEntity](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts) 线条实体
|
||||
|
||||
|
||||
## 获取所有线条的实体
|
||||
|
||||
```ts pure
|
||||
const allLines = ctx.document.linesManager.getAllLines() // 线条实体 包含 from/to 分别代表线条连接的节点
|
||||
|
||||
```
|
||||
|
||||
## 创建/删除线条
|
||||
|
||||
```ts pure
|
||||
// from和 to 为对应要连线的节点id, fromPort, toPort 为 端口 id, 如果节点为单个端口可以不指定
|
||||
const line = ctx.document.linesManager.createLine({ from, to, fromPort, toPort })
|
||||
|
||||
// 删除线条
|
||||
line.dispose()
|
||||
|
||||
```
|
||||
|
||||
## 导出线条数据
|
||||
|
||||
:::note 线条基本结构:
|
||||
|
||||
- sourceNodeID: `string` 开始节点 id
|
||||
- targetNodeID: `string` 目标节点 id
|
||||
- sourcePortID?: `string | number` 开始端口 id, 缺省则采用开始节点的默认端口
|
||||
- targetPortID?: `string | number` 目标端口 id, 缺省则采用目标节点的默认端口
|
||||
|
||||
:::
|
||||
```ts pure
|
||||
const json = ctx.document.linesManager.toJSON()
|
||||
```
|
||||
|
||||
## 获取当前节点的输入/输出节点或线条
|
||||
|
||||
```ts pure
|
||||
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
// 获取当前节点的输入节点(通过连接线计算)
|
||||
node.geData(WorkflowNodeLinesData).inputNodes
|
||||
// 获取所有输入节点 (会往上递归获取所有)
|
||||
node.geData(WorkflowNodeLinesData).allInputNodes
|
||||
// 获取输出节点
|
||||
node.geData(WorkflowNodeLinesData).outputNodes
|
||||
// 获取所有输出节点
|
||||
node.geData(WorkflowNodeLinesData).allOutputNodes
|
||||
// 输入线条
|
||||
node.geData(WorkflowNodeLinesData).inputLines
|
||||
// 输出线条
|
||||
node.geData(WorkflowNodeLinesData).outputLines
|
||||
|
||||
```
|
||||
|
||||
## 线条配置
|
||||
|
||||
我们提供丰富的线条配置参数, 给 `FreeLayoutEditorProvider`, 详细见 [FreeLayoutProps](https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/preset/free-layout-props.ts)
|
||||
|
||||
```tsx pure
|
||||
interface FreeLayoutProps {
|
||||
/**
|
||||
* 线条颜色配置
|
||||
*/
|
||||
lineColor?: LineColor;
|
||||
/**
|
||||
* 判断线条是否标红
|
||||
* @param ctx
|
||||
* @param fromPort
|
||||
* @param toPort
|
||||
* @param lines
|
||||
*/
|
||||
isErrorLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity | undefined, lines: WorkflowLinesManager) => boolean;
|
||||
/**
|
||||
* 判断端口是否标红
|
||||
* @param ctx
|
||||
* @param port
|
||||
*/
|
||||
isErrorPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;
|
||||
/**
|
||||
* 判断端口是否禁用
|
||||
* @param ctx
|
||||
* @param port
|
||||
*/
|
||||
isDisabledPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;
|
||||
/**
|
||||
* 判断线条箭头是否反转
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isReverseLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* 判断线条是否隐藏箭头
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isHideArrowLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* 判断线条是否展示流动效果
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isFlowingLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* 判断线条是否禁用
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isDisabledLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* 判断线条是否竖向
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
isVerticalLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;
|
||||
/**
|
||||
* 拖拽线条结束
|
||||
* @param ctx
|
||||
* @param params
|
||||
*/
|
||||
onDragLineEnd?: (ctx: FreeLayoutPluginContext, params: onDragLineEndParams) => Promise<void>;
|
||||
/**
|
||||
* 设置线条渲染器类型
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
setLineRenderType?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => LineRenderType | undefined;
|
||||
/**
|
||||
* 设置线条样式
|
||||
* @param ctx
|
||||
* @param line
|
||||
*/
|
||||
setLineClassName?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => string | undefined;
|
||||
/**
|
||||
* 是否允许创建线条
|
||||
* @param ctx
|
||||
* @param fromPort - 开始点
|
||||
* @param toPort - 目标点
|
||||
*/
|
||||
canAddLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity, lines: WorkflowLinesManager, silent?: boolean) => boolean;
|
||||
/**
|
||||
*
|
||||
* 是否允许删除线条
|
||||
* @param ctx
|
||||
* @param line - 目标线条
|
||||
* @param newLineInfo - 新的线条信息
|
||||
* @param silent - 如果为false,可以加 toast 弹窗
|
||||
*/
|
||||
canDeleteLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity, newLineInfo?: Required<WorkflowLinePortInfo>, silent?: boolean) => boolean;
|
||||
/**
|
||||
* 是否允许重置线条
|
||||
* @param fromPort - 开始点
|
||||
* @param oldToPort - 旧的连接点
|
||||
* @param newToPort - 新的连接点
|
||||
* @param lines - 线条管理器
|
||||
*/
|
||||
canResetLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, oldToPort: WorkflowPortEntity, newToPort: WorkflowPortEntity, lines: WorkflowLinesManager) => boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.自定义颜色
|
||||
|
||||
```tsx pure
|
||||
|
||||
function App() {
|
||||
const editorProps: FreeLayoutProps = {
|
||||
lineColor: {
|
||||
hidden: 'transparent',
|
||||
default: '#4d53e8',
|
||||
drawing: '#5DD6E3',
|
||||
hovered: '#37d0ff',
|
||||
selected: '#37d0ff',
|
||||
error: 'red',
|
||||
},
|
||||
// ...others
|
||||
}
|
||||
return (
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 2.让单个输出端口只能连一条线
|
||||
|
||||
<img loading="lazy" style={{ width: 500, margin: '0 auto' }} className="invert-img" src="/free-layout/line-limit.gif"/>
|
||||
|
||||
```tsx pure
|
||||
|
||||
function App() {
|
||||
const editorProps: FreeLayoutProps = {
|
||||
/*
|
||||
* Check whether the line can be added
|
||||
* 判断是否连线
|
||||
*/
|
||||
canAddLine(ctx, fromPort, toPort) {
|
||||
// not the same node
|
||||
if (fromPort.node === toPort.node) {
|
||||
return false;
|
||||
}
|
||||
// 控制线条添加数目
|
||||
if (fromPort.availableLines.length >= 1) {
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// ...others
|
||||
}
|
||||
return (
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 3.连接到空白地方添加节点
|
||||
|
||||
代码见自由布局最佳实践
|
||||
|
||||
<img loading="lazy" style={{ width: 500, margin: '0 auto' }} className="invert-img" src="/free-layout/line-add-panel.gif"/>
|
||||
|
||||
```tsx pure
|
||||
|
||||
function App() {
|
||||
const editorProps: FreeLayoutProps = {
|
||||
/**
|
||||
* Drag the end of the line to create an add panel (feature optional)
|
||||
* 拖拽线条结束需要创建一个添加面板 (功能可选)
|
||||
*/
|
||||
async onDragLineEnd(ctx, params) {
|
||||
const { fromPort, toPort, mousePos, line, originLine } = params;
|
||||
if (originLine || !line) {
|
||||
return;
|
||||
}
|
||||
if (toPort) {
|
||||
return;
|
||||
}
|
||||
// 这里可以根据 mousePos 打开添加面板
|
||||
await ctx.get(WorkflowNodePanelService).call({
|
||||
fromPort,
|
||||
toPort: undefined,
|
||||
panelPosition: mousePos,
|
||||
enableBuildLine: true,
|
||||
panelProps: {
|
||||
enableNodePlaceholder: true,
|
||||
enableScrollClose: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
// ...others
|
||||
}
|
||||
return (
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 在线条上添加 Label
|
||||
|
||||
代码见自由布局最佳实践
|
||||
|
||||
<img loading="lazy" style={{ width: 500, margin: '0 auto' }} className="invert-img" src="/free-layout/line-add-button.gif"/>
|
||||
|
||||
```ts pure
|
||||
|
||||
import { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';
|
||||
|
||||
const editorProps = {
|
||||
plugins: () => [
|
||||
/**
|
||||
* Line render plugin
|
||||
* 连线渲染插件
|
||||
*/
|
||||
createFreeLinesPlugin({
|
||||
renderInsideLine: LineAddButton,
|
||||
}),
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 节点监听自身的连线变化并刷新
|
||||
|
||||
```tsx pure
|
||||
|
||||
import {
|
||||
useRefresh,
|
||||
WorkflowNodeLinesData,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
function NodeRender({ node }) {
|
||||
const refresh = useRefresh()
|
||||
const linesData = node.get(WorkflowNodeLinesData)
|
||||
useEffect(() => {
|
||||
const dispose = linesData.onDataChange(() => refresh())
|
||||
return () => dispose.dispose()
|
||||
}, [])
|
||||
return <div>xxxx</div>
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 监听所有线条的连线变化
|
||||
|
||||
这个场景用于当希望在外部组件监听线条连接情况
|
||||
|
||||
```ts pure
|
||||
import { useEffect } from 'react'
|
||||
import { WorkflowLinesManager, useRefresh } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
|
||||
function SomeReact() {
|
||||
const refresh = useRefresh()
|
||||
const linesManager = useService(WorkflowLinesManager)
|
||||
useEffect(() => {
|
||||
const dispose = linesManager.onAvailableLinesChange(() => refresh())
|
||||
return () => dispose.dispose()
|
||||
}, [])
|
||||
console.log(ctx.document.linesManager.getAllLines())
|
||||
}
|
||||
```
|
||||
|
||||
181
apps/docs/src/zh/guide/advanced/free-layout/load.mdx
Normal file
181
apps/docs/src/zh/guide/advanced/free-layout/load.mdx
Normal file
@ -0,0 +1,181 @@
|
||||
# 加载与保存
|
||||
|
||||
画布的数据通过 [WorkflowDocument](/api/core/workflow-document.html) 来存储
|
||||
|
||||
## 画布数据
|
||||
|
||||
:::note 文档数据基本结构:
|
||||
|
||||
- nodes `array` 节点列表, 支持嵌套
|
||||
- edges `array` 边列表
|
||||
|
||||
:::
|
||||
|
||||
:::note 节点数据基本结构:
|
||||
|
||||
- id: `string` 节点唯一标识, 必须保证唯一
|
||||
- meta: `object` 节点的 ui 配置信息,如自由布局的 `position` 信息放这里
|
||||
- type: `string | number` 节点类型,会和 `nodeRegistries` 中的 `type` 对应
|
||||
- data: `object` 节点表单数据, 业务可自定义
|
||||
- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`, 目前会存子画布的节点
|
||||
- edges: `array` 子画布的边数据
|
||||
|
||||
:::
|
||||
|
||||
:::note 边数据基本结构:
|
||||
|
||||
- sourceNodeID: `string` 开始节点 id
|
||||
- targetNodeID: `string` 目标节点 id
|
||||
- sourcePortID?: `string | number` 开始端口 id, 缺省则采用开始节点的默认端口
|
||||
- targetPortID?: `string | number` 目标端口 id, 缺省则采用目标节点的默认端口
|
||||
|
||||
:::
|
||||
|
||||
|
||||
```tsx pure title="initial-data.ts"
|
||||
import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export const initialData: WorkflowJSON = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'start_0',
|
||||
type: 'start',
|
||||
meta: {
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
data: {
|
||||
title: 'Start',
|
||||
content: 'Start content'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'node_0',
|
||||
type: 'custom',
|
||||
meta: {
|
||||
position: { x: 400, y: 0 },
|
||||
},
|
||||
data: {
|
||||
title: 'Custom',
|
||||
content: 'Custom node content'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'end_0',
|
||||
type: 'end',
|
||||
meta: {
|
||||
position: { x: 800, y: 0 },
|
||||
},
|
||||
data: {
|
||||
title: 'End',
|
||||
content: 'End content'
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: 'start_0',
|
||||
targetNodeID: 'node_0',
|
||||
},
|
||||
{
|
||||
sourceNodeID: 'node_0',
|
||||
targetNodeID: 'end_0',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
## 加载
|
||||
|
||||
- 通过 initialData 加载
|
||||
|
||||
```tsx pure
|
||||
import { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
function App({ data }) {
|
||||
return (
|
||||
<FreeLayoutEditorProvider initialData={data} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- 通过 ref 动态加载
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
function App() {
|
||||
const ref = useRef<FreeLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(async () => {
|
||||
const data = await request('https://xxxx/getJSON')
|
||||
ref.current.document.fromJSON(data)
|
||||
setTimeout(() => {
|
||||
// 加载后触发画布的 fitview 让节点自动居中
|
||||
ref.current.document.fitView()
|
||||
}, 100)
|
||||
}, [])
|
||||
return (
|
||||
<FreeLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- 动态 reload 所有数据
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
function App({ data }) {
|
||||
const ref = useRef<FreeLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(async () => {
|
||||
// 当 data 变化时候重新加载画布数据
|
||||
await ref.current.document.reload(data)
|
||||
setTimeout(() => {
|
||||
// 加载后触发画布的 fitview 让节点自动居中
|
||||
ref.current.document.fitView()
|
||||
}, 100)
|
||||
}, [data])
|
||||
return (
|
||||
<FreeLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 监听变化并自动保存
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
function App() {
|
||||
const ref = useRef<FreeLayoutPluginContext | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
// 监听画布变化 延迟 1 秒 保存数据, 避免画布频繁更新
|
||||
const toDispose = ref.current.document.onContentChange(debounce(() => {
|
||||
// 通过 toJSON 获取画布最新的数据
|
||||
request('https://xxxx/save', {
|
||||
data: ref.current.document.toJSON()
|
||||
})
|
||||
}, 1000))
|
||||
return () => toDispose.dispose()
|
||||
}, [])
|
||||
return (
|
||||
<FreeLayoutEditorProvider ref={ref} {...otherProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
148
apps/docs/src/zh/guide/advanced/free-layout/node.mdx
Normal file
148
apps/docs/src/zh/guide/advanced/free-layout/node.mdx
Normal file
@ -0,0 +1,148 @@
|
||||
# 节点
|
||||
|
||||
节点通过 [FlowNodeEntity](/api/core/flow-node-entity.html) 定义
|
||||
|
||||
|
||||
## 节点数据
|
||||
|
||||
通过 `node.toJSON()` 可以获取
|
||||
|
||||
:::note 基本结构:
|
||||
|
||||
- id: `string` 节点唯一标识, 必须保证唯一
|
||||
- meta: `object` 节点的 ui 配置信息,如自由布局的 `position` 信息放这里
|
||||
- type: `string | number` 节点类型,会和 `nodeRegistries` 中的 `type` 对应
|
||||
- data: `object` 节点表单数据, 业务可自定义
|
||||
- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming` 自由布局布局场景会用在子画布的子节点
|
||||
- edges: `array` 子画布的边数据
|
||||
|
||||
:::
|
||||
|
||||
```ts pure
|
||||
const nodeData: FlowNodeJSON = {
|
||||
id: 'xxxx',
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: 'MyCondition',
|
||||
desc: 'xxxxx'
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 节点定义
|
||||
|
||||
在自由布局场景,节点定义用于声明节点的初始化位置/大小,端口,表单渲染等, 详细见 [声明节点](guide/getting-started/create-free-layout-simple.html#4-声明节点)
|
||||
|
||||
## 当前渲染节点获取
|
||||
|
||||
通过 [useNodeRender](api/hooks/use-node-render.html) 获取节点相关方法
|
||||
|
||||
## 创建节点
|
||||
|
||||
- 通过 [WorkflowDocument](/api/core/workflow-document.html) 创建
|
||||
|
||||
```ts pure
|
||||
const ctx = useClientContext()
|
||||
|
||||
ctx.document.createWorkflowNode({
|
||||
id: 'xxx', // 要保证画布内唯一
|
||||
type: 'custom',
|
||||
meta: {
|
||||
/**
|
||||
* 如果不传入,则默认在画布中间创建
|
||||
* 如果要通过鼠标位置获取 position (如点击画布任意位置创建节点),可通过 `ctx.playground.config.getPosFromMouseEvent(mouseEvent)` 转换
|
||||
*/
|
||||
position: { x: 100, y: 100 } //
|
||||
},
|
||||
data: {}, // 表单相关数据
|
||||
blocks: [], // 子画布的节点
|
||||
edges: [] // 子画布的边
|
||||
})
|
||||
|
||||
```
|
||||
- 通过 WorkflowDragService 创建, 见[自由布局基础用法](/examples/free-layout/free-layout-simple.html)
|
||||
|
||||
```ts pure
|
||||
const dragService = useService<WorkflowDragService>(WorkflowDragService);
|
||||
|
||||
// 这里的 mouseEvent 会自动转成 画布的 position
|
||||
dragService.startDragCard(nodeType, mouseEvent, {
|
||||
id: 'xxxx',
|
||||
data: {}, // 表单相关数据
|
||||
blocks: [], // 子画布的节点
|
||||
edges: [] // 子画布的边
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
## 删除节点
|
||||
|
||||
通过 `node.dispose` 删除节点
|
||||
|
||||
```tsx pure
|
||||
function BaseNode({ node }) {
|
||||
function onClick() {
|
||||
node.dispose()
|
||||
}
|
||||
return (
|
||||
<button onClick={onClick}>Delete</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 更新节点 data 数据
|
||||
|
||||
- 通过 [useNodeRender](/api/hooks/use-node-render.html) 或 [getNodeForm](/api/utils/get-node-form.html) 获取节点的 data 数据
|
||||
|
||||
```tsx pure
|
||||
function BaseNode() {
|
||||
const { form } = useNodeRender();
|
||||
// 对应节点的 data 数据
|
||||
console.log(form.values)
|
||||
|
||||
// 监听节点的数据变化
|
||||
useEffect(() => {
|
||||
const toDispose = form.onFormValuesChange(() => {
|
||||
// changed
|
||||
})
|
||||
return () => toDispose.dispose()
|
||||
}, [form])
|
||||
|
||||
function onChange(e) {
|
||||
form.setValueIn('title', e.target.value)
|
||||
}
|
||||
return <input value={form.getValueIn('title')} onChange={onChange}/>
|
||||
}
|
||||
```
|
||||
- 通过 Field 更新表单数据, 详细见 [表单的使用](/guide/advanced/form.html)
|
||||
|
||||
```tsx pure
|
||||
|
||||
function FormRender() {
|
||||
return (
|
||||
<Field name="title">
|
||||
<Input />
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 更新节点的 extInfo 数据
|
||||
|
||||
extInfo 用于存储 一些 ui 状态, 如果未开启节点引擎,节点的 data 数据会默认存到 extInfo 里
|
||||
|
||||
```tsx pure
|
||||
function BaseNode({ node }) {
|
||||
const times = node.getExtInfo()?.times || 0
|
||||
function onClick() {
|
||||
node.updateExtInfo({ times: times ++ })
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span>Click Times: {times}</span>
|
||||
<button onClick={onClick}>Click</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
90
apps/docs/src/zh/guide/advanced/free-layout/port.mdx
Normal file
90
apps/docs/src/zh/guide/advanced/free-layout/port.mdx
Normal file
@ -0,0 +1,90 @@
|
||||
# 端口
|
||||
|
||||
- [WorkflowNodePortsData](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-ports-data.ts) 管理节点的所有端口信息
|
||||
- [WorkflowPortEntity](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts) 端口实例
|
||||
- [WorkflowPortRender](https://github.com/bytedance/flowgram.ai/blob/main/packages/plugins/free-lines-plugin/src/components/workflow-port-render/index.tsx) 端口渲染组件
|
||||
|
||||
|
||||
## 定义端口
|
||||
|
||||
- 静态端口
|
||||
|
||||
节点声明添加 `defaultPorts` , 如 `{ type: 'input' }`, 则会在节点左侧加入输入端口
|
||||
|
||||
```ts pure title="node-registries.ts"
|
||||
{
|
||||
type: 'start',
|
||||
meta: {
|
||||
defaultPorts: [{ type: 'output' }, { type: 'input'}]
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- 动态端口
|
||||
|
||||
节点声明添加 `dynamicPorts` , 当设置为 true 则会到节点dom 上寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
|
||||
|
||||
|
||||
```ts pure title="node-registries.ts"
|
||||
{
|
||||
type: 'condition',
|
||||
meta: {
|
||||
defaultPorts: [{ type: 'input'}]
|
||||
dynamicPort: true
|
||||
},
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```tsx pure
|
||||
|
||||
/**
|
||||
* 动态端口通过 querySelectorAll('[data-port-id]') 查找端口位置
|
||||
*/
|
||||
function BaseNode() {
|
||||
return (
|
||||
<div>
|
||||
<div data-port-id="condition-if-0" data-port-type="output"></div>
|
||||
<div data-port-id="condition-if-1" data-port-type="output"></div>
|
||||
{/* others */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 端口渲染
|
||||
|
||||
端口最终通过 `WorkflowPortRender` 组件渲染,支持自定义 style, 或者业务基于源码重新实现该组件, 参考 [自由布局最佳实践 - 节点渲染](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/base-node/node-wrapper.tsx)
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { WorkflowPortRender, useNodeRender } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
function BaseNode() {
|
||||
const { ports } = useNodeRender();
|
||||
return (
|
||||
<div>
|
||||
<div data-port-id="condition-if-0" data-port-type="output"></div>
|
||||
<div data-port-id="condition-if-1" data-port-type="output"></div>
|
||||
{ports.map((p) => (
|
||||
<WorkflowPortRender key={p.id} entity={p} className="xxx" style={{ /* custom style */}}/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 获取端口数据
|
||||
|
||||
```ts pure
|
||||
const ports = node.getData(WorkflowNodePortsData)
|
||||
|
||||
console.log(ports.inputPorts) // 获取当前节点的所有输入端口
|
||||
console.log(ports.outputPorts) // 获取当前节点的所有输出端口
|
||||
|
||||
console.log(ports.inputPorts.map(port => port.availableLines)) // 通过端口找到连接的线条
|
||||
|
||||
ports.updateDynamicPorts() // 当动态端口修改了 dom 结构或位置,可以通过该方法手动刷新端口位置(dom 渲染有延迟,最好在 useEffect 或者 setTimeout 执行)
|
||||
```
|
||||
|
||||
|
||||
82
apps/docs/src/zh/guide/advanced/free-layout/sub-canvas.mdx
Normal file
82
apps/docs/src/zh/guide/advanced/free-layout/sub-canvas.mdx
Normal file
@ -0,0 +1,82 @@
|
||||
# 子画布
|
||||
|
||||
<img loading="lazy" className="invert-img" src="/free-layout/loop2.png"/>
|
||||
|
||||
详细代码见 [自由布局最佳实践](/examples/free-layout/free-feature-overview.html)
|
||||
|
||||
## 添加子画布插件
|
||||
|
||||
```tsx pure
|
||||
|
||||
import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
|
||||
|
||||
function App() {
|
||||
const editorProps = {
|
||||
plugins: () => [
|
||||
createContainerNodePlugin({}),
|
||||
]
|
||||
// ..others
|
||||
}
|
||||
return (
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer className="demo-editor" />
|
||||
</FreeLayoutEditorProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 定义子画布节点
|
||||
|
||||
```tsx pure
|
||||
import { SubCanvasRender } from '@flowgram.ai/free-container-plugin';
|
||||
|
||||
export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'loop',
|
||||
info: {
|
||||
icon: iconLoop,
|
||||
description:
|
||||
'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',
|
||||
},
|
||||
meta: {
|
||||
/**
|
||||
* 子画布标记
|
||||
*/
|
||||
isContainer: true,
|
||||
/**
|
||||
* 子画布默认大小设置
|
||||
*/
|
||||
size: {
|
||||
width: 560,
|
||||
height: 400,
|
||||
},
|
||||
/**
|
||||
* 子画布 padding 设置
|
||||
*/
|
||||
padding: () => ({ // 容器 padding 设置
|
||||
top: 150,
|
||||
bottom: 100,
|
||||
left: 100,
|
||||
right: 100,
|
||||
}),
|
||||
/**
|
||||
* 控制子画布内的节点选中状态
|
||||
*/
|
||||
selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {
|
||||
if (!mousePos) {
|
||||
return true;
|
||||
}
|
||||
const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||
// 鼠标开始时所在位置不包括当前节点时才可选中
|
||||
return !transform.bounds.contains(mousePos.x, mousePos.y);
|
||||
},
|
||||
},
|
||||
formMeta: {
|
||||
render: () => (
|
||||
<div>
|
||||
{ /* others */ }
|
||||
<SubCanvasRender />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -1,4 +0,0 @@
|
||||
[
|
||||
"shortcuts",
|
||||
"minimap"
|
||||
]
|
||||
@ -1,58 +0,0 @@
|
||||
# 自由布局线条
|
||||
|
||||
自由布局的线条通过 [WorkflowLinesManager](/api/core/workflow-lines-manager.html) 管理
|
||||
|
||||
## 获取当前节点的输入/输出节点
|
||||
|
||||
```ts pure
|
||||
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
// 获取当前节点的输入节点(通过连接线计算)
|
||||
node.geData(WorkflowNodeLinesData).inputNodes
|
||||
// 获取所有输入节点 (会往上递归获取所有)
|
||||
node.geData(WorkflowNodeLinesData).allInputNodes
|
||||
// 获取输出节点
|
||||
node.geData(WorkflowNodeLinesData).outputNodes
|
||||
// 获取所有输出节点
|
||||
node.geData(WorkflowNodeLinesData).allOutputNodes
|
||||
|
||||
```
|
||||
|
||||
## 节点监听自身的连线变化并刷新
|
||||
|
||||
```tsx pure
|
||||
|
||||
import {
|
||||
useRefresh,
|
||||
WorkflowNodeLinesData,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
function NodeRender({ node }) {
|
||||
const refresh = useRefresh()
|
||||
const linesData = node.get(WorkflowNodeLinesData)
|
||||
useEffect(() => {
|
||||
const dispose = linesData.onDataChange(() => refresh())
|
||||
return () => dispose.dispose()
|
||||
}, [])
|
||||
return <div>xxxx</div>
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 监听所有线条的连线变化
|
||||
|
||||
```ts pure
|
||||
import { useEffect } from 'react'
|
||||
import { useClientContext, useRefresh } from '@flowgram.ai/free-layout-editor'
|
||||
|
||||
|
||||
function SomeReact() {
|
||||
const refresh = useRefresh()
|
||||
const linesManager = useClientContext().document.linesManager
|
||||
useEffect(() => {
|
||||
const dispose = linesManager.onAvailableLinesChange(() => refresh())
|
||||
return () => dispose.dispose()
|
||||
}, [])
|
||||
console.log(ctx.document.linesManager.getAllLines())
|
||||
}
|
||||
```
|
||||
@ -9,7 +9,13 @@
|
||||
|
||||
<img loading="lazy" className="invert-img" src="/ecs.png"/>
|
||||
|
||||
ReduxStore 伪代码
|
||||
|
||||
## 方案对比
|
||||
|
||||
我们对比两个数据方案:
|
||||
|
||||
### 1. ReduxStore 方案
|
||||
|
||||
```jsx pure
|
||||
const store = () => ({
|
||||
nodes: [{
|
||||
@ -27,6 +33,7 @@ function Playground() {
|
||||
return nodes.map(node => <Node data={node} />)
|
||||
}
|
||||
```
|
||||
|
||||
优点:
|
||||
- 中心化数据管理使用简单
|
||||
|
||||
@ -34,60 +41,83 @@ function Playground() {
|
||||
- 中心化数据管理无法精确更新,带来性能瓶颈
|
||||
- 扩展性差,节点新增一个数据,都耦合到一个 大JSON 里
|
||||
|
||||
ECS 方案
|
||||
### 2. ECS 方案
|
||||
|
||||
备注:
|
||||
- NodeData 对应的是 ECS - Component
|
||||
- Layer 对应 ECS - System
|
||||
|
||||
```jsx pure
|
||||
|
||||
/**
|
||||
* 画布文档数据
|
||||
*/
|
||||
class FlowDocument {
|
||||
dataDefines: [
|
||||
NodePositionData,
|
||||
NodeFormData,
|
||||
NodeLineData
|
||||
]
|
||||
nodeEntities: Entity[] = []
|
||||
/**
|
||||
* * 节点数据定义, 节点创建时候会把数据实例化
|
||||
*/
|
||||
nodeDefines: [
|
||||
NodePositionData,
|
||||
NodeFormData,
|
||||
NodeLineData
|
||||
]
|
||||
nodeEntities: Entity[] = []
|
||||
}
|
||||
|
||||
|
||||
class Entity {
|
||||
id: string // 只有id 不带数据
|
||||
getData: (dataId: string) => EntityData
|
||||
/**
|
||||
* 节点
|
||||
*/
|
||||
class FlowNodeEntity {
|
||||
id: string // 只有id 不带数据
|
||||
getData: (dataId: string) => EntityData
|
||||
}
|
||||
|
||||
// 渲染线条
|
||||
class LinesLayer {
|
||||
@observeEntityData(NodeLineData) lines
|
||||
render() {
|
||||
return lines.map(line => <Line data={line} />)
|
||||
}
|
||||
/**
|
||||
* 内部通过 node.getData(NodeLineData) 获取对应的数据,下同
|
||||
*/
|
||||
@observeEntityData(FlowNodeEntity, NodeLineData) lines: NodeLineData[]
|
||||
render() {
|
||||
// 渲染线条
|
||||
return this.lines.map(line => <Line data={line} />)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染节点位置
|
||||
class NodePositionsLayer {
|
||||
@observeEntityData(NodePositionData) positions
|
||||
return() {
|
||||
|
||||
}
|
||||
@observeEntityData(FlowNodeEntity, NodePositionData) positions: NodePositionData[]
|
||||
render() {
|
||||
// 渲染位置及排版
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染节点表单
|
||||
class NodeFormsLayer {
|
||||
@observeEntityData(NodeFormData) contents
|
||||
return() {}
|
||||
@observeEntityData(FlowNodeEntity, NodeFormData) contents: NodeFormData[]
|
||||
render() {
|
||||
// 渲染节点内容
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 画布实例,通过 Layer 分层渲染
|
||||
*/
|
||||
class Playground {
|
||||
layers: [
|
||||
LinesLayer, // 线条渲染
|
||||
NodePositionsLayer, // 位置渲染
|
||||
NodeFormsLayer // 内容渲染
|
||||
],
|
||||
render() {
|
||||
return this.layers.map(layer => layer.render())
|
||||
}
|
||||
layers: [
|
||||
LinesLayer, // 线条渲染
|
||||
NodePositionsLayer, // 位置渲染
|
||||
NodeFormsLayer // 内容渲染
|
||||
],
|
||||
render() {
|
||||
// 画布分层渲染
|
||||
return this.layers.map(layer => layer.render())
|
||||
}
|
||||
}
|
||||
```
|
||||
优点:
|
||||
|
||||
- 节点数据拆开来单独控制渲染,性能可做到精确更新
|
||||
- 扩展性强,新增一个节点数据,则新增一个 XXXData + XXXLayer
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
[
|
||||
"install",
|
||||
"create-fixed-layout-simple",
|
||||
"create-free-layout-simple"
|
||||
"create-free-layout-simple",
|
||||
"create-fixed-layout-simple"
|
||||
]
|
||||
|
||||
@ -141,7 +141,8 @@ export function useEditorProps(
|
||||
- meta: `object` 节点的 ui 配置信息,如自由布局的 `position` 信息放这里
|
||||
- type: `string | number` 节点类型,会和 `nodeRegistries` 中的 `type` 对应
|
||||
- data: `object` 节点表单数据, 业务可自定义
|
||||
- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`
|
||||
- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`, 目前会存子画布的节点
|
||||
- edges: `array` 子画布的边数据
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@ -199,11 +199,19 @@ export class WorkflowPortEntity extends Entity<WorkflowPortEntityOpts> {
|
||||
|
||||
/**
|
||||
* 当前点位上连接的线条
|
||||
* @deprecated use `availableLines` instead
|
||||
*/
|
||||
get lines(): WorkflowLineEntity[] {
|
||||
return this.allLines.filter((line) => !line.isDrawing);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前有效的线条,不包含正在画的线条和隐藏的线条(这个出现在线条重连会先把原来的线条隐藏)
|
||||
*/
|
||||
get availableLines(): WorkflowLineEntity[] {
|
||||
return this.allLines.filter((line) => !line.isDrawing && !line.isHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前点位上连接的线条(包含 isDrawing === true 的线条)
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user