feat(fixed-layout): add input/output/multi-outputs/multi-inputs/break node (#246)

* feat(demo): add demo playground

* feat(fixed-layout): add multi-start and break node

* fix: form-model updateFormValues with formatOnInit

* feat(node-engine): formModel.values cloneDeep -> clone and add shllowEuqal checked

* feat(demo): demo-fixed-layout-simple add flow-select

* fix: use-node-render node undefined

* docs: docs error

* feat(fixed-layout): add input/output/multi-outputs/multi-inputs node
This commit is contained in:
xiamidaxia 2025-05-20 17:22:11 +08:00 committed by GitHub
parent ec6e5abe23
commit ce0c13393b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 894 additions and 113 deletions

View File

@ -1,6 +1,8 @@
import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
import { FlowNodeEntity, useNodeRender, useClientContext } from '@flowgram.ai/fixed-layout-editor';
import { IconDeleteStroked } from '@douyinfe/semi-icons';
export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
const ctx = useClientContext();
/**
* Provides methods related to node rendering
*
@ -36,6 +38,10 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),
}}
>
<IconDeleteStroked
style={{ position: 'absolute', right: 4, top: 4 }}
onClick={() => ctx.operation.deleteNode(nodeRender.node)}
/>
{form?.render()}
</div>
);

View File

@ -15,14 +15,26 @@ export function BranchAdder(props: PropsType) {
const { isVertical } = node;
function addBranch() {
const block = operation.addBlock(node, {
id: `branch_${nanoid(5)}`,
type: 'block',
data: {
title: 'New Branch',
content: '',
},
});
let block: FlowNodeEntity;
if (node.flowNodeType === 'multiOutputs') {
block = operation.addBlock(node, {
id: `output_${nanoid(5)}`,
type: 'output',
data: {
title: 'New Ouput',
content: '',
},
});
} else {
block = operation.addBlock(node, {
id: `branch_${nanoid(5)}`,
type: 'block',
data: {
title: 'New Branch',
content: '',
},
});
}
setTimeout(() => {
playground.scrollToView({

View File

@ -0,0 +1,49 @@
import { useEffect, useState } from 'react';
import { useClientContext, FlowLayoutDefault } from '@flowgram.ai/fixed-layout-editor';
import { FLOW_LIST } from '../data';
const url = new window.URL(window.location.href);
export function FlowSelect() {
const [demoKey, updateDemoKey] = useState(url.searchParams.get('demo') ?? 'condition');
const clientContext = useClientContext();
useEffect(() => {
const targetDemoJSON = FLOW_LIST[demoKey];
if (targetDemoJSON) {
clientContext.history.stop(); // Stop redo/undo
clientContext.document.fromJSON(targetDemoJSON);
console.log(clientContext.document.toString());
clientContext.history.start();
clientContext.document.setLayout(
targetDemoJSON.defaultLayout || FlowLayoutDefault.VERTICAL_FIXED_LAYOUT
);
// Update URL
if (url.searchParams.get('demo')) {
url.searchParams.set('demo', demoKey);
window.history.pushState({}, '', `/?${url.searchParams.toString()}`);
}
// Fit View
setTimeout(() => {
clientContext.playground.config.fitView(clientContext.document.root.bounds);
}, 20);
}
}, [demoKey]);
return (
<div style={{ position: 'absolute', zIndex: 100 }}>
<label style={{ marginRight: 12 }}>Select Demo:</label>
<select
style={{ width: '180px', height: '32px', fontSize: 16 }}
onChange={(e) => updateDemoKey(e.target.value)}
value={demoKey}
>
{Object.keys(FLOW_LIST).map((key) => (
<option key={key} value={key}>
{key}
</option>
))}
</select>
</div>
);
}

View File

@ -1,12 +1,17 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';
import { IconButton, Space } from '@douyinfe/semi-ui';
import { IconUnlock, IconLock } from '@douyinfe/semi-icons';
export function Tools() {
const { history } = useClientContext();
const { history, playground } = useClientContext();
const tools = usePlaygroundTools();
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const toggleReadonly = useCallback(() => {
playground.config.readonly = !playground.config.readonly;
}, [playground]);
useEffect(() => {
const disposable = history.undoRedoService.onChange(() => {
@ -17,7 +22,7 @@ export function Tools() {
}, [history]);
return (
<div
<Space
style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}
>
<button onClick={() => tools.zoomin()}>ZoomIn</button>
@ -30,7 +35,22 @@ export function Tools() {
<button onClick={() => history.redo()} disabled={!canRedo}>
Redo
</button>
{playground.config.readonly ? (
<IconButton
theme="borderless"
type="tertiary"
icon={<IconLock />}
onClick={toggleReadonly}
/>
) : (
<IconButton
theme="borderless"
type="tertiary"
icon={<IconUnlock />}
onClick={toggleReadonly}
/>
)}
<span>{Math.floor(tools.zoom * 100)}%</span>
</div>
</Space>
);
}

View File

@ -0,0 +1,81 @@
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
export const condition: FlowDocumentJSON = {
nodes: [
// 开始节点
{
id: 'start_0',
type: 'start',
data: {
title: 'Start',
content: 'start content',
},
blocks: [],
},
// 分支节点
{
id: 'condition_0',
type: 'condition',
data: {
title: 'Condition',
content: 'condition content',
},
blocks: [
{
id: 'branch_0',
type: 'block',
data: {
title: 'Branch 0',
content: 'branch 1 content',
},
blocks: [
{
id: 'custom_0',
type: 'custom',
data: {
title: 'Custom',
content: 'custom content',
},
},
],
},
{
id: 'branch_1',
type: 'block',
data: {
title: 'Branch 1',
content: 'branch 1 content',
},
blocks: [
{
id: 'break_0',
type: 'break',
data: {
title: 'Break',
content: 'Break content',
},
},
],
},
{
id: 'branch_2',
type: 'block',
data: {
title: 'Branch 2',
content: 'branch 2 content',
},
blocks: [],
},
],
},
// 结束节点
{
id: 'end_0',
type: 'end',
data: {
title: 'End',
content: 'end content',
},
},
],
};

View File

@ -0,0 +1,9 @@
import { FlowDocumentJSON, FlowLayoutDefault } from '@flowgram.ai/fixed-layout-editor';
import { mindmap } from './mindmap';
import { condition } from './condition';
export const FLOW_LIST: Record<string, FlowDocumentJSON & { defaultLayout?: FlowLayoutDefault }> = {
condition,
mindmap: { ...mindmap, defaultLayout: FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT },
};

View File

@ -0,0 +1,99 @@
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
export const mindmap: FlowDocumentJSON = {
nodes: [
{
id: 'multiInputs_0',
type: 'multiInputs',
blocks: [
{
id: 'input_0',
type: 'input',
data: {
title: 'input_0',
},
},
{
id: 'input_1',
type: 'input',
data: {
title: 'input_1',
},
},
// {
// id: 'multiInputs_2',
// type: 'multiInputs',
// blocks: [
// {
// id: 'input_2',
// type: 'input',
// data: {
// title: 'input_2'
// },
// },
// {
// id: 'input',
// type: 'input_3',
// data: {
// title: 'input_3'
// },
// }
// ],
// },
],
},
{
id: 'multiOutputs_0',
type: 'multiOutputs',
data: {
title: 'mindNode_0',
},
blocks: [
{
id: 'output_0',
type: 'output',
data: {
title: 'output_0',
},
},
{
id: 'multiOutputs_1',
type: 'multiOutputs',
data: {
title: 'mindNode_1',
},
blocks: [
{
id: 'output_1',
type: 'output',
data: {
title: 'output_1',
},
},
{
id: 'output_2',
type: 'output',
data: {
title: 'output_2',
},
},
{
id: 'output_3',
type: 'output',
data: {
title: 'output_3',
},
},
],
},
{
id: 'output_4',
type: 'output',
data: {
title: 'output_4',
},
},
],
},
],
};

View File

@ -8,6 +8,7 @@ import { initialData } from './initial-data';
import { useEditorProps } from './hooks/use-editor-props';
import { Tools } from './components/tools';
import { Minimap } from './components/minimap';
import { FlowSelect } from './components/flow-select';
export const Editor = () => {
const editorProps = useEditorProps(initialData, nodeRegistries);
@ -17,6 +18,7 @@ export const Editor = () => {
<EditorRenderer>{/* add child panel here */}</EditorRenderer>
</div>
<Tools />
<FlowSelect />
<Minimap />
</FixedLayoutEditorProvider>
);

View File

@ -102,7 +102,7 @@ export function useEditorProps(
enableChangeNode: true, // Listen Node engine data change
onApply(ctx, opt) {
// Listen change to trigger auto save
// console.log('auto save: ', ctx.document.toJSON(), opt);
console.log('auto save: ', ctx.document.toJSON(), opt);
},
},
/**

View File

@ -8,7 +8,7 @@
flex-direction: column;
justify-content: center;
position: relative;
width: 360px;
width: 240px;
transition: all 0.3s ease;
}

View File

@ -1,65 +1,8 @@
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
import { condition as conditionDemo } from './data/condition';
/**
* blocks
* Initial Data
*/
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',
},
},
],
};
export const initialData: FlowDocumentJSON = conditionDemo;

View File

@ -1,5 +1,9 @@
import { nanoid } from 'nanoid';
import { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
import {
FlowNodeRegistry,
FlowNodeEntity,
FlowNodeBaseType,
} from '@flowgram.ai/fixed-layout-editor';
/**
*
@ -17,6 +21,7 @@ export const nodeRegistries: FlowNodeRegistry[] = [
* - dynamicSplit: 扩展为分支节点
* - end: 扩展为结束节点
* - tryCatch: 扩展为 tryCatch
* - break:
* - default: ()
*/
extend: 'dynamicSplit',
@ -72,4 +77,35 @@ export const nodeRegistries: FlowNodeRegistry[] = [
};
},
},
{
type: 'multiStart2',
extend: 'dynamicSplit',
meta: {
isStart: true,
},
onCreate(node, json) {
const doc = node.document;
const addedNodes: FlowNodeEntity[] = [];
const blocks = json.blocks || [];
if (blocks.length > 0) {
// 水平布局
const inlineBlocksNode = doc.addNode({
id: `$inlineBlocks$${node.id}`,
type: FlowNodeBaseType.INLINE_BLOCKS,
originParent: node,
parent: node,
});
addedNodes.push(inlineBlocksNode);
blocks.forEach((blockData) => {
doc.addBlock(node, blockData, addedNodes);
});
}
return addedNodes;
},
},
{
type: 'tree',
extend: 'simpleSplit',
},
];

View File

@ -0,0 +1,15 @@
const { defineConfig } = require('@flowgram.ai/eslint-config');
module.exports = defineConfig({
preset: 'web',
packageRoot: __dirname,
rules: {
'no-console': 'off',
'react/prop-types': 'off',
},
settings: {
react: {
version: 'detect', // 自动检测 React 版本
},
},
});

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" data-bundler="rspack">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flow FreeLayoutEditor Demo</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,52 @@
{
"name": "@flowgram.ai/demo-playground",
"version": "0.1.0",
"description": "",
"keywords": [],
"license": "MIT",
"main": "./src/index.tsx",
"files": [
"src/",
".eslintrc.js",
".gitignore",
"index.html",
"package.json",
"rsbuild.config.ts",
"tsconfig.json"
],
"scripts": {
"build": "exit 0",
"build:fast": "exit 0",
"build:watch": "exit 0",
"clean": "rimraf dist",
"dev": "cross-env MODE=app NODE_ENV=development rsbuild dev --open",
"lint": "eslint ./src --cache",
"lint:fix": "eslint ./src --fix",
"start": "cross-env NODE_ENV=development rsbuild dev --open",
"test": "exit",
"test:cov": "exit",
"watch": "exit 0"
},
"dependencies": {
"@flowgram.ai/playground-react": "workspace:*",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@flowgram.ai/ts-config": "workspace:*",
"@flowgram.ai/eslint-config": "workspace:*",
"@rsbuild/core": "^1.2.16",
"@rsbuild/plugin-react": "^1.1.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/styled-components": "^5",
"eslint": "^8.54.0",
"cross-env": "~7.0.3"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}

View File

@ -0,0 +1,14 @@
import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from '@rsbuild/core';
export default defineConfig({
plugins: [pluginReact()],
source: {
entry: {
index: './src/index.tsx',
},
},
html: {
title: 'demo-playground',
},
});

View File

@ -0,0 +1,66 @@
import React, { useCallback, useState } from 'react';
import { usePlayground, usePlaygroundDrag } from '@flowgram.ai/playground-react';
export function Card() {
return (
<div
style={{
width: 200,
height: 100,
position: 'absolute',
color: 'white',
backgroundColor: 'red',
left: 500,
top: 500,
}}
></div>
);
}
export function DragableCard() {
const [pos, setPos] = useState({ x: 200, y: 100 });
// 用于拖拽,拖拽到边缘时候会自动滚动画布
const dragger = usePlaygroundDrag();
const playground = usePlayground();
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
const startPos = { x: pos.x, y: pos.y };
dragger.start(e, {
onDragStart() {
playground.config.grabDisable = true;
// start drag
},
onDrag(dragEvent) {
// 需要 除去当前的缩放比例
setPos({
x: startPos.x + (dragEvent.endPos.x - dragEvent.startPos.x) / dragEvent.scale,
y: startPos.y + (dragEvent.endPos.y - dragEvent.startPos.y) / dragEvent.scale,
});
},
onDragEnd() {
playground.config.grabDisable = false;
// end drag
},
});
// e.stopPropagation();
// e.preventDefault();
},
[pos]
);
return (
<div
onMouseDown={handleMouseDown}
style={{
cursor: 'move',
width: 200,
height: 100,
position: 'absolute',
color: 'white',
backgroundColor: 'blue',
left: pos.x,
top: pos.y,
}}
></div>
);
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import { usePlaygroundTools } from '@flowgram.ai/playground-react';
export const PlaygroundTools: React.FC<{ minZoom?: number; maxZoom?: number }> = (props) => {
const tools = usePlaygroundTools(props);
return (
<div
style={{
position: 'absolute',
zIndex: 100,
right: 100,
bottom: 100,
padding: 13,
border: '1px solid #ccc',
backgroundColor: 'white',
borderRadius: 8,
userSelect: 'none',
cursor: 'pointer',
}}
>
<button onClick={() => tools.toggleIneractiveType()}>{tools.interactiveType}</button>
<button onClick={() => tools.zoomout()}>Zoom Out</button>
<button onClick={() => tools.zoomin()}>Zoom In</button>
<span>{Math.floor(tools.zoom * 100)}%</span>
</div>
);
};

View File

@ -0,0 +1,73 @@
import { useMemo } from 'react';
import { createRoot } from 'react-dom/client';
import {
Command,
PlaygroundReact,
PlaygroundReactContent,
PlaygroundReactProps,
} from '@flowgram.ai/playground-react';
import { PlaygroundTools } from './components/playground-tools';
import { Card, DragableCard } from './components/card';
// 加载画布样式
import '@flowgram.ai/playground-react/index.css';
/**
*
*/
export function App() {
const playgroundProps = useMemo<PlaygroundReactProps>(
() => ({
// 是否增加背景
background: true,
playground: {
ineractiveType: 'MOUSE', // 鼠标模式, MOUSE | PAD
},
// 自定义快捷键
shortcuts(registry, ctx) {
registry.addHandlers(
/**
*
*/
{
commandId: Command.Default.ZOOM_IN,
shortcuts: ['meta =', 'ctrl ='],
execute: () => {
ctx.playground.config.zoomin();
},
},
/**
*
*/
{
commandId: Command.Default.ZOOM_OUT,
shortcuts: ['meta -', 'ctrl -'],
execute: () => {
ctx.playground.config.zoomout();
},
}
);
},
}),
[]
);
/*
* PlaygroundReact react , background
* PlaygroundReactContent
*/
return (
<PlaygroundReact {...playgroundProps}>
<PlaygroundReactContent>
<Card />
<DragableCard />
</PlaygroundReactContent>
<PlaygroundTools />
</PlaygroundReact>
);
}
const app = createRoot(document.getElementById('root')!);
app.render(<App />);

View File

@ -0,0 +1,23 @@
{
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"experimentalDecorators": true,
"target": "es2020",
"module": "esnext",
"strictPropertyInitialization": false,
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"allowJs": true,
"resolveJsonModule": true,
"types": ["node"],
"jsx": "react-jsx",
"lib": ["es6", "dom", "es2020", "es2019.Array"]
},
"include": ["./src"],
}

View File

@ -40,17 +40,17 @@ const json = ctx.document.linesManager.toJSON()
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
// get input nodes (calculated through connection lines)
node.geData(WorkflowNodeLinesData).inputNodes
node.getData(WorkflowNodeLinesData).inputNodes
// get all input nodes (recursively gets all upstream nodes)
node.geData(WorkflowNodeLinesData).allInputNodes
node.getData(WorkflowNodeLinesData).allInputNodes
// get output nodes
node.geData(WorkflowNodeLinesData).outputNodes
node.getData(WorkflowNodeLinesData).outputNodes
// get all output nodes
node.geData(WorkflowNodeLinesData).allOutputNodes
node.getData(WorkflowNodeLinesData).allOutputNodes
// input lines
node.geData(WorkflowNodeLinesData).inputLines
node.getData(WorkflowNodeLinesData).inputLines
// output lines
node.geData(WorkflowNodeLinesData).outputLines
node.getData(WorkflowNodeLinesData).outputLines
```
## Line Configuration

View File

@ -8,13 +8,13 @@ The lines in the free layout are managed by [WorkflowLinesManager](/api/core/wor
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
// Get the input nodes of the current node (calculated through connection lines)
node.geData(WorkflowNodeLinesData).inputNodes;
node.getData(WorkflowNodeLinesData).inputNodes;
// Get all input nodes (recursively get all upward)
node.geData(WorkflowNodeLinesData).allInputNodes;
node.getData(WorkflowNodeLinesData).allInputNodes;
// Get the output nodes
node.geData(WorkflowNodeLinesData).outputNodes;
node.getData(WorkflowNodeLinesData).outputNodes;
// Get all output nodes
node.geData(WorkflowNodeLinesData).allOutputNodes;
node.getData(WorkflowNodeLinesData).allOutputNodes;
```
## Node listens to its own connection changes and refreshes

View File

@ -43,17 +43,17 @@ const json = ctx.document.linesManager.toJSON()
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
// 获取当前节点的输入节点(通过连接线计算)
node.geData(WorkflowNodeLinesData).inputNodes
node.getData(WorkflowNodeLinesData).inputNodes
// 获取所有输入节点 (会往上递归获取所有)
node.geData(WorkflowNodeLinesData).allInputNodes
node.getData(WorkflowNodeLinesData).allInputNodes
// 获取输出节点
node.geData(WorkflowNodeLinesData).outputNodes
node.getData(WorkflowNodeLinesData).outputNodes
// 获取所有输出节点
node.geData(WorkflowNodeLinesData).allOutputNodes
node.getData(WorkflowNodeLinesData).allOutputNodes
// 输入线条
node.geData(WorkflowNodeLinesData).inputLines
node.getData(WorkflowNodeLinesData).inputLines
// 输出线条
node.geData(WorkflowNodeLinesData).outputLines
node.getData(WorkflowNodeLinesData).outputLines
```

View File

@ -480,6 +480,52 @@ importers:
specifier: ^8.54.0
version: 8.57.1
../../apps/demo-playground:
dependencies:
'@flowgram.ai/playground-react':
specifier: workspace:*
version: link:../../packages/client/playground-react
react:
specifier: ^18
version: 18.3.1
react-dom:
specifier: ^18
version: 18.3.1(react@18.3.1)
devDependencies:
'@flowgram.ai/eslint-config':
specifier: workspace:*
version: link:../../config/eslint-config
'@flowgram.ai/ts-config':
specifier: workspace:*
version: link:../../config/ts-config
'@rsbuild/core':
specifier: ^1.2.16
version: 1.2.19
'@rsbuild/plugin-react':
specifier: ^1.1.1
version: 1.1.1(@rsbuild/core@1.2.19)
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
'@types/node':
specifier: ^18
version: 18.19.68
'@types/react':
specifier: ^18
version: 18.3.16
'@types/react-dom':
specifier: ^18
version: 18.3.5(@types/react@18.3.16)
'@types/styled-components':
specifier: ^5
version: 5.1.34
cross-env:
specifier: ~7.0.3
version: 7.0.3
eslint:
specifier: ^8.54.0
version: 8.57.1
../../apps/demo-react-16:
dependencies:
'@flowgram.ai/free-layout-editor':
@ -1928,6 +1974,9 @@ importers:
'@flowgram.ai/utils':
specifier: workspace:*
version: link:../../common/utils
fast-equals:
specifier: ^2.0.0
version: 2.0.4
lodash:
specifier: ^4.17.21
version: 4.17.21

View File

@ -471,6 +471,7 @@ export class FlowDocument<T = FlowDocumentJSON> implements Disposable {
const customDefaultRegistry = this.options.getNodeDefaultRegistry?.(type);
let register = this.registers.get(type) || { type };
const extendRegisters: FlowNodeRegistry[] = [];
const extendKey = register.extend;
// 继承重载
if (register.extend && this.registers.has(register.extend)) {
register = FlowNodeRegistry.merge(
@ -505,6 +506,10 @@ export class FlowDocument<T = FlowDocumentJSON> implements Disposable {
...register.meta,
},
} as T;
// Save the "extend" attribute
if (extendKey) {
res.extend = extendKey;
}
this.nodeRegistryCache.set(typeKey, res);
return res;
}

View File

@ -29,8 +29,13 @@ export enum FlowNodeBaseType {
BLOCK_ORDER_ICON = 'blockOrderIcon', // 带顺序的图标节点,一般为 block 第一个分支节点
GROUP = 'group', // 分组节点
END = 'end', // 结束节点
BREAK = 'break', // 分支结束
CONDITION = 'condition', // 可以连接多条线的条件判断节点,目前只支持横向布局
SUB_CANVAS = 'subCanvas', // 自由布局子画布
MULTI_INPUTS = 'multiInputs', // 多输入
MULTI_OUTPUTS = 'multiOutputs', // 多输出
INPUT = 'input', // 输入节点
OUTPUT = 'output', // 输出节点
}
export enum FlowNodeSplitType {

View File

@ -0,0 +1,9 @@
import { FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';
/**
* Break ,
*/
export const BreakRegistry: FlowNodeRegistry = {
type: FlowNodeBaseType.BREAK,
extend: FlowNodeBaseType.END,
};

View File

@ -11,3 +11,8 @@ export * from './root';
export * from './empty';
export * from './end';
export * from './simple-split';
export * from './break';
export * from './input';
export * from './output';
export * from './multi-outputs';
export * from './multi-inputs';

View File

@ -0,0 +1,77 @@
import {
FlowNodeBaseType,
type FlowNodeRegistry,
type FlowTransitionLine,
FlowTransitionLineEnum,
LABEL_SIDE_TYPE,
} from '@flowgram.ai/document';
/**
*
*/
export const InputRegistry: FlowNodeRegistry = {
type: FlowNodeBaseType.INPUT,
extend: FlowNodeBaseType.BLOCK,
meta: {
hidden: false,
},
getLines(transition) {
const currentTransform = transition.transform;
const { isVertical } = transition.entity;
const lines: FlowTransitionLine[] = [];
const hasBranchDraggingAdder =
currentTransform && currentTransform.entity.isInlineBlock && transition.renderData.draggable;
// 分支拖拽场景线条 push
// 当有其余分支的时候,绘制一条两个分支之间的线条
if (hasBranchDraggingAdder) {
if (isVertical) {
const currentOffsetRightX = currentTransform.firstChild?.bounds?.right || 0;
const nextOffsetLeftX = currentTransform.next?.firstChild?.bounds?.left || 0;
const currentInputPointY = currentTransform.inputPoint.y;
if (currentTransform?.next) {
lines.push({
type: FlowTransitionLineEnum.DRAGGING_LINE,
from: currentTransform.parent!.inputPoint,
to: {
x: (currentOffsetRightX + nextOffsetLeftX) / 2,
y: currentInputPointY,
},
side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
});
}
} else {
const currentOffsetRightY = currentTransform.firstChild?.bounds?.bottom || 0;
const nextOffsetLeftY = currentTransform.next?.firstChild?.bounds?.top || 0;
const currentInputPointX = currentTransform.inputPoint.x;
if (currentTransform?.next) {
lines.push({
type: FlowTransitionLineEnum.DRAGGING_LINE,
from: currentTransform.parent!.inputPoint,
to: {
x: currentInputPointX,
y: (currentOffsetRightY + nextOffsetLeftY) / 2,
},
side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
});
}
}
}
// 最后一个节点是 end 节点,不绘制 mergeLine
if (!transition.isNodeEnd) {
lines.push({
type: FlowTransitionLineEnum.MERGE_LINE,
from: currentTransform.outputPoint,
to: currentTransform.parent!.outputPoint,
side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
});
}
return lines;
},
getLabels() {
return [];
},
};

View File

@ -0,0 +1,36 @@
import { FlowNodeBaseType, FlowNodeSplitType, type FlowNodeRegistry } from '@flowgram.ai/document';
/**
* ,
* - multiInputs:
* - inlineBlocks
* - input
* - input
*/
export const MultiInputsRegistry: FlowNodeRegistry = {
type: FlowNodeBaseType.MULTI_INPUTS,
extend: FlowNodeSplitType.SIMPLE_SPLIT,
extendChildRegistries: [
{
type: FlowNodeBaseType.BLOCK_ICON,
meta: {
hidden: true,
},
getLines() {
return [];
},
getLabels() {
return [];
},
},
{
type: FlowNodeBaseType.INLINE_BLOCKS,
getLabels() {
return [];
},
},
],
getLabels() {
return [];
},
};

View File

@ -0,0 +1,22 @@
import { FlowNodeBaseType, type FlowNodeRegistry, FlowNodeSplitType } from '@flowgram.ai/document';
import { BlockRegistry } from './block';
/**
*
* - multiOutputs:
* - blockIcon
* - inlineBlocks
* - output or multiOutputs
* - output or multiOutputs
*/
export const MultiOuputsRegistry: FlowNodeRegistry = {
type: FlowNodeBaseType.MULTI_OUTPUTS,
extend: FlowNodeSplitType.SIMPLE_SPLIT,
getLines: (transition, layout) => {
if (transition.entity.parent?.flowNodeType === FlowNodeBaseType.INLINE_BLOCKS) {
return BlockRegistry.getLines!(transition, layout);
}
return [];
},
};

View File

@ -0,0 +1,13 @@
import { FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';
/**
* , end
*/
export const OuputRegistry: FlowNodeRegistry = {
type: FlowNodeBaseType.OUTPUT,
extend: FlowNodeBaseType.BLOCK,
meta: {
hidden: false,
isNodeEnd: true,
},
};

View File

@ -7,12 +7,11 @@ import {
} from '@flowgram.ai/document';
/**
* , BlockOrderIcon
* simpleSplit: ( id)
* - simpleSplit: ( id)
* blockIcon
* inlineBlocks
* block1
* block2
* node1
* node2
*/
export const SimpleSplitRegistry: FlowNodeRegistry = {
type: FlowNodeSplitType.SIMPLE_SPLIT,
@ -24,23 +23,16 @@ export const SimpleSplitRegistry: FlowNodeRegistry = {
) {
const { document } = originParent;
const parent = document.getNode(`$inlineBlocks$${originParent.id}`);
// 块节点会生成一个空的 Block 节点用来切割 Block
const proxyBlock = document.addNode({
id: `$block$${blockData.id}`,
type: FlowNodeBaseType.BLOCK,
originParent,
parent,
});
const realBlock = document.addNode(
{
...blockData,
type: blockData.type || FlowNodeBaseType.BLOCK,
parent: proxyBlock,
parent,
},
addedNodes
);
addedNodes.push(proxyBlock, realBlock);
return proxyBlock;
addedNodes.push(realBlock);
return realBlock;
},
// addChild(node, json, options = {}) {
// const { index } = options;

View File

@ -32,6 +32,11 @@ import {
StaticSplitRegistry,
TryCatchRegistry,
SimpleSplitRegistry,
BreakRegistry,
MultiOuputsRegistry,
MultiInputsRegistry,
InputRegistry,
OuputRegistry,
} from './activities';
@injectable()
@ -59,7 +64,12 @@ export class FlowRegisters
TryCatchRegistry, // TryCatch
EndRegistry, // 结束节点
LoopRegistry, // 循环节点
EmptyRegistry // 占位节点
EmptyRegistry, // 占位节点
BreakRegistry, // 分支断开
MultiOuputsRegistry,
MultiInputsRegistry,
InputRegistry,
OuputRegistry
);
/**
* (ECS - Component)

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useContext, useMemo } from 'react';
import React, { useCallback, useEffect, useContext, useMemo, useRef } from 'react';
import { useObserve } from '@flowgram.ai/reactive';
import { useStartDragNode } from '@flowgram.ai/fixed-drag-plugin';
@ -93,13 +93,17 @@ export interface NodeRenderReturnType {
*/
export function useNodeRender(nodeFromProps?: FlowNodeEntity): NodeRenderReturnType {
const renderNode = nodeFromProps || useContext<FlowNodeEntity>(PlaygroundEntityContext);
const nodeCache = useRef<FlowNodeEntity | undefined>();
const renderData = renderNode.getData<FlowNodeRenderData>(FlowNodeRenderData)!;
const { expanded, dragging, activated } = renderData;
const { startDrag: startDragOrigin } = useStartDragNode();
const playground = usePlayground();
const isBlockOrderIcon = renderNode.flowNodeType === FlowNodeBaseType.BLOCK_ORDER_ICON;
const isBlockIcon = renderNode.flowNodeType === FlowNodeBaseType.BLOCK_ICON;
const node = isBlockOrderIcon || isBlockIcon ? renderNode.parent! : renderNode;
// 在 BlockIcon 情况,如果在触发 fromJSON 时候更新表单数据导致刷新节点会存在 renderNode.parent 为 undefined所以这里 nodeCache 进行缓存
const node =
(isBlockOrderIcon || isBlockIcon ? renderNode.parent! : renderNode) || nodeCache.current;
nodeCache.current = node;
const operationService = useService<FlowOperationService>(FlowOperationService);
const deleteNode = useCallback(() => {
operationService.deleteNode(node);

View File

@ -33,6 +33,7 @@
"dependencies": {
"@flowgram.ai/reactive": "workspace:*",
"@flowgram.ai/utils": "workspace:*",
"fast-equals": "^2.0.0",
"lodash": "^4.17.21",
"nanoid": "^4.0.2"
},

View File

@ -1,4 +1,5 @@
import { cloneDeep, flatten, get } from 'lodash';
import { clone, flatten, get } from 'lodash';
import { shallowEqual } from 'fast-equals';
import { Disposable, Emitter } from '@flowgram.ai/utils';
import { ReactiveState } from '@flowgram.ai/reactive';
@ -78,11 +79,14 @@ export class FormModel<TValues = any> implements Disposable {
}
get values() {
return cloneDeep(this.store.values) || cloneDeep(this.initialValues);
return clone(this.store.values) || clone(this.initialValues);
}
set values(v) {
const prevValues = this.values;
if (shallowEqual(this.store.values || this.initialValues, v)) {
return;
}
this.store.values = v;
this.fireOnFormValuesChange({
values: this.values,

View File

@ -147,7 +147,10 @@ export class FormModelV2 extends FormModel implements Disposable {
updateFormValues(value: any) {
if (this.nativeFormModel) {
this.nativeFormModel.values = value;
const finalValue = this.formMeta.formatOnInit
? this.formMeta.formatOnInit(value, this.nodeContext)
: value;
this.nativeFormModel.values = finalValue;
}
}

View File

@ -777,6 +777,12 @@
"projectFolder": "apps/demo-vite",
"versionPolicyName": "appPolicy",
"tags": ["level-1", "team-flow", "demo"]
},
{
"packageName": "@flowgram.ai/demo-playground",
"projectFolder": "apps/demo-playground",
"versionPolicyName": "appPolicy",
"tags": ["level-1", "team-flow", "demo"]
}
]
}