mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
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:
parent
ec6e5abe23
commit
ce0c13393b
@ -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 }) => {
|
export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
|
||||||
|
const ctx = useClientContext();
|
||||||
/**
|
/**
|
||||||
* Provides methods related to node rendering
|
* Provides methods related to node rendering
|
||||||
* 提供节点渲染相关的方法
|
* 提供节点渲染相关的方法
|
||||||
@ -36,6 +38,10 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
|
|||||||
...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),
|
...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<IconDeleteStroked
|
||||||
|
style={{ position: 'absolute', right: 4, top: 4 }}
|
||||||
|
onClick={() => ctx.operation.deleteNode(nodeRender.node)}
|
||||||
|
/>
|
||||||
{form?.render()}
|
{form?.render()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,14 +15,26 @@ export function BranchAdder(props: PropsType) {
|
|||||||
const { isVertical } = node;
|
const { isVertical } = node;
|
||||||
|
|
||||||
function addBranch() {
|
function addBranch() {
|
||||||
const block = operation.addBlock(node, {
|
let block: FlowNodeEntity;
|
||||||
id: `branch_${nanoid(5)}`,
|
if (node.flowNodeType === 'multiOutputs') {
|
||||||
type: 'block',
|
block = operation.addBlock(node, {
|
||||||
data: {
|
id: `output_${nanoid(5)}`,
|
||||||
title: 'New Branch',
|
type: 'output',
|
||||||
content: '',
|
data: {
|
||||||
},
|
title: 'New Ouput',
|
||||||
});
|
content: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
block = operation.addBlock(node, {
|
||||||
|
id: `branch_${nanoid(5)}`,
|
||||||
|
type: 'block',
|
||||||
|
data: {
|
||||||
|
title: 'New Branch',
|
||||||
|
content: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
playground.scrollToView({
|
playground.scrollToView({
|
||||||
|
|||||||
49
apps/demo-fixed-layout-simple/src/components/flow-select.tsx
Normal file
49
apps/demo-fixed-layout-simple/src/components/flow-select.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 { 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() {
|
export function Tools() {
|
||||||
const { history } = useClientContext();
|
const { history, playground } = useClientContext();
|
||||||
const tools = usePlaygroundTools();
|
const tools = usePlaygroundTools();
|
||||||
const [canUndo, setCanUndo] = useState(false);
|
const [canUndo, setCanUndo] = useState(false);
|
||||||
const [canRedo, setCanRedo] = useState(false);
|
const [canRedo, setCanRedo] = useState(false);
|
||||||
|
const toggleReadonly = useCallback(() => {
|
||||||
|
playground.config.readonly = !playground.config.readonly;
|
||||||
|
}, [playground]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disposable = history.undoRedoService.onChange(() => {
|
const disposable = history.undoRedoService.onChange(() => {
|
||||||
@ -17,7 +22,7 @@ export function Tools() {
|
|||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Space
|
||||||
style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}
|
style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}
|
||||||
>
|
>
|
||||||
<button onClick={() => tools.zoomin()}>ZoomIn</button>
|
<button onClick={() => tools.zoomin()}>ZoomIn</button>
|
||||||
@ -30,7 +35,22 @@ export function Tools() {
|
|||||||
<button onClick={() => history.redo()} disabled={!canRedo}>
|
<button onClick={() => history.redo()} disabled={!canRedo}>
|
||||||
Redo
|
Redo
|
||||||
</button>
|
</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>
|
<span>{Math.floor(tools.zoom * 100)}%</span>
|
||||||
</div>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
81
apps/demo-fixed-layout-simple/src/data/condition.ts
Normal file
81
apps/demo-fixed-layout-simple/src/data/condition.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
9
apps/demo-fixed-layout-simple/src/data/index.ts
Normal file
9
apps/demo-fixed-layout-simple/src/data/index.ts
Normal 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 },
|
||||||
|
};
|
||||||
99
apps/demo-fixed-layout-simple/src/data/mindmap.ts
Normal file
99
apps/demo-fixed-layout-simple/src/data/mindmap.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
0
apps/demo-fixed-layout-simple/src/data/slots.ts
Normal file
0
apps/demo-fixed-layout-simple/src/data/slots.ts
Normal file
0
apps/demo-fixed-layout-simple/src/data/trycatch.ts
Normal file
0
apps/demo-fixed-layout-simple/src/data/trycatch.ts
Normal file
@ -8,6 +8,7 @@ import { initialData } from './initial-data';
|
|||||||
import { useEditorProps } from './hooks/use-editor-props';
|
import { useEditorProps } from './hooks/use-editor-props';
|
||||||
import { Tools } from './components/tools';
|
import { Tools } from './components/tools';
|
||||||
import { Minimap } from './components/minimap';
|
import { Minimap } from './components/minimap';
|
||||||
|
import { FlowSelect } from './components/flow-select';
|
||||||
|
|
||||||
export const Editor = () => {
|
export const Editor = () => {
|
||||||
const editorProps = useEditorProps(initialData, nodeRegistries);
|
const editorProps = useEditorProps(initialData, nodeRegistries);
|
||||||
@ -17,6 +18,7 @@ export const Editor = () => {
|
|||||||
<EditorRenderer>{/* add child panel here */}</EditorRenderer>
|
<EditorRenderer>{/* add child panel here */}</EditorRenderer>
|
||||||
</div>
|
</div>
|
||||||
<Tools />
|
<Tools />
|
||||||
|
<FlowSelect />
|
||||||
<Minimap />
|
<Minimap />
|
||||||
</FixedLayoutEditorProvider>
|
</FixedLayoutEditorProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -102,7 +102,7 @@ export function useEditorProps(
|
|||||||
enableChangeNode: true, // Listen Node engine data change
|
enableChangeNode: true, // Listen Node engine data change
|
||||||
onApply(ctx, opt) {
|
onApply(ctx, opt) {
|
||||||
// Listen change to trigger auto save
|
// Listen change to trigger auto save
|
||||||
// console.log('auto save: ', ctx.document.toJSON(), opt);
|
console.log('auto save: ', ctx.document.toJSON(), opt);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 360px;
|
width: 240px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,65 +1,8 @@
|
|||||||
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
|
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
|
||||||
|
|
||||||
|
import { condition as conditionDemo } from './data/condition';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置流程数据,数据为 blocks 嵌套的格式
|
* Initial Data
|
||||||
*/
|
*/
|
||||||
export const initialData: FlowDocumentJSON = {
|
export const initialData: FlowDocumentJSON = conditionDemo;
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { nanoid } from 'nanoid';
|
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: 扩展为分支节点
|
* - dynamicSplit: 扩展为分支节点
|
||||||
* - end: 扩展为结束节点
|
* - end: 扩展为结束节点
|
||||||
* - tryCatch: 扩展为 tryCatch 节点
|
* - tryCatch: 扩展为 tryCatch 节点
|
||||||
|
* - break: 分支断开
|
||||||
* - default: 扩展为普通节点 (默认)
|
* - default: 扩展为普通节点 (默认)
|
||||||
*/
|
*/
|
||||||
extend: 'dynamicSplit',
|
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',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
15
apps/demo-playground/.eslintrc.js
Normal file
15
apps/demo-playground/.eslintrc.js
Normal 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 版本
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
12
apps/demo-playground/index.html
Normal file
12
apps/demo-playground/index.html
Normal 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>
|
||||||
52
apps/demo-playground/package.json
Normal file
52
apps/demo-playground/package.json
Normal 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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
apps/demo-playground/rsbuild.config.ts
Normal file
14
apps/demo-playground/rsbuild.config.ts
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
66
apps/demo-playground/src/components/card.tsx
Normal file
66
apps/demo-playground/src/components/card.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
apps/demo-playground/src/components/playground-tools.tsx
Normal file
28
apps/demo-playground/src/components/playground-tools.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
73
apps/demo-playground/src/index.tsx
Normal file
73
apps/demo-playground/src/index.tsx
Normal 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 />);
|
||||||
23
apps/demo-playground/tsconfig.json
Normal file
23
apps/demo-playground/tsconfig.json
Normal 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"],
|
||||||
|
}
|
||||||
@ -40,17 +40,17 @@ const json = ctx.document.linesManager.toJSON()
|
|||||||
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
|
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
|
||||||
|
|
||||||
// get input nodes (calculated through connection lines)
|
// get input nodes (calculated through connection lines)
|
||||||
node.geData(WorkflowNodeLinesData).inputNodes
|
node.getData(WorkflowNodeLinesData).inputNodes
|
||||||
// get all input nodes (recursively gets all upstream nodes)
|
// get all input nodes (recursively gets all upstream nodes)
|
||||||
node.geData(WorkflowNodeLinesData).allInputNodes
|
node.getData(WorkflowNodeLinesData).allInputNodes
|
||||||
// get output nodes
|
// get output nodes
|
||||||
node.geData(WorkflowNodeLinesData).outputNodes
|
node.getData(WorkflowNodeLinesData).outputNodes
|
||||||
// get all output nodes
|
// get all output nodes
|
||||||
node.geData(WorkflowNodeLinesData).allOutputNodes
|
node.getData(WorkflowNodeLinesData).allOutputNodes
|
||||||
// input lines
|
// input lines
|
||||||
node.geData(WorkflowNodeLinesData).inputLines
|
node.getData(WorkflowNodeLinesData).inputLines
|
||||||
// output lines
|
// output lines
|
||||||
node.geData(WorkflowNodeLinesData).outputLines
|
node.getData(WorkflowNodeLinesData).outputLines
|
||||||
```
|
```
|
||||||
|
|
||||||
## Line Configuration
|
## Line Configuration
|
||||||
|
|||||||
@ -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'
|
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
|
||||||
|
|
||||||
// Get the input nodes of the current node (calculated through connection lines)
|
// 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)
|
// Get all input nodes (recursively get all upward)
|
||||||
node.geData(WorkflowNodeLinesData).allInputNodes;
|
node.getData(WorkflowNodeLinesData).allInputNodes;
|
||||||
// Get the output nodes
|
// Get the output nodes
|
||||||
node.geData(WorkflowNodeLinesData).outputNodes;
|
node.getData(WorkflowNodeLinesData).outputNodes;
|
||||||
// Get all output nodes
|
// Get all output nodes
|
||||||
node.geData(WorkflowNodeLinesData).allOutputNodes;
|
node.getData(WorkflowNodeLinesData).allOutputNodes;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Node listens to its own connection changes and refreshes
|
## Node listens to its own connection changes and refreshes
|
||||||
|
|||||||
@ -43,17 +43,17 @@ const json = ctx.document.linesManager.toJSON()
|
|||||||
import { WorkflowNodeLinesData } from '@flowgram.ai/free-layout-editor'
|
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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
49
common/config/rush/pnpm-lock.yaml
generated
49
common/config/rush/pnpm-lock.yaml
generated
@ -480,6 +480,52 @@ importers:
|
|||||||
specifier: ^8.54.0
|
specifier: ^8.54.0
|
||||||
version: 8.57.1
|
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:
|
../../apps/demo-react-16:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@flowgram.ai/free-layout-editor':
|
'@flowgram.ai/free-layout-editor':
|
||||||
@ -1928,6 +1974,9 @@ importers:
|
|||||||
'@flowgram.ai/utils':
|
'@flowgram.ai/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../common/utils
|
version: link:../../common/utils
|
||||||
|
fast-equals:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.4
|
||||||
lodash:
|
lodash:
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
|||||||
@ -471,6 +471,7 @@ export class FlowDocument<T = FlowDocumentJSON> implements Disposable {
|
|||||||
const customDefaultRegistry = this.options.getNodeDefaultRegistry?.(type);
|
const customDefaultRegistry = this.options.getNodeDefaultRegistry?.(type);
|
||||||
let register = this.registers.get(type) || { type };
|
let register = this.registers.get(type) || { type };
|
||||||
const extendRegisters: FlowNodeRegistry[] = [];
|
const extendRegisters: FlowNodeRegistry[] = [];
|
||||||
|
const extendKey = register.extend;
|
||||||
// 继承重载
|
// 继承重载
|
||||||
if (register.extend && this.registers.has(register.extend)) {
|
if (register.extend && this.registers.has(register.extend)) {
|
||||||
register = FlowNodeRegistry.merge(
|
register = FlowNodeRegistry.merge(
|
||||||
@ -505,6 +506,10 @@ export class FlowDocument<T = FlowDocumentJSON> implements Disposable {
|
|||||||
...register.meta,
|
...register.meta,
|
||||||
},
|
},
|
||||||
} as T;
|
} as T;
|
||||||
|
// Save the "extend" attribute
|
||||||
|
if (extendKey) {
|
||||||
|
res.extend = extendKey;
|
||||||
|
}
|
||||||
this.nodeRegistryCache.set(typeKey, res);
|
this.nodeRegistryCache.set(typeKey, res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,13 @@ export enum FlowNodeBaseType {
|
|||||||
BLOCK_ORDER_ICON = 'blockOrderIcon', // 带顺序的图标节点,一般为 block 第一个分支节点
|
BLOCK_ORDER_ICON = 'blockOrderIcon', // 带顺序的图标节点,一般为 block 第一个分支节点
|
||||||
GROUP = 'group', // 分组节点
|
GROUP = 'group', // 分组节点
|
||||||
END = 'end', // 结束节点
|
END = 'end', // 结束节点
|
||||||
|
BREAK = 'break', // 分支结束
|
||||||
CONDITION = 'condition', // 可以连接多条线的条件判断节点,目前只支持横向布局
|
CONDITION = 'condition', // 可以连接多条线的条件判断节点,目前只支持横向布局
|
||||||
SUB_CANVAS = 'subCanvas', // 自由布局子画布
|
SUB_CANVAS = 'subCanvas', // 自由布局子画布
|
||||||
|
MULTI_INPUTS = 'multiInputs', // 多输入
|
||||||
|
MULTI_OUTPUTS = 'multiOutputs', // 多输出
|
||||||
|
INPUT = 'input', // 输入节点
|
||||||
|
OUTPUT = 'output', // 输出节点
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FlowNodeSplitType {
|
export enum FlowNodeSplitType {
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Break 节点, 用于分支断开
|
||||||
|
*/
|
||||||
|
export const BreakRegistry: FlowNodeRegistry = {
|
||||||
|
type: FlowNodeBaseType.BREAK,
|
||||||
|
extend: FlowNodeBaseType.END,
|
||||||
|
};
|
||||||
@ -11,3 +11,8 @@ export * from './root';
|
|||||||
export * from './empty';
|
export * from './empty';
|
||||||
export * from './end';
|
export * from './end';
|
||||||
export * from './simple-split';
|
export * from './simple-split';
|
||||||
|
export * from './break';
|
||||||
|
export * from './input';
|
||||||
|
export * from './output';
|
||||||
|
export * from './multi-outputs';
|
||||||
|
export * from './multi-inputs';
|
||||||
|
|||||||
@ -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 [];
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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 [];
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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 [];
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -7,12 +7,11 @@ import {
|
|||||||
} from '@flowgram.ai/document';
|
} from '@flowgram.ai/document';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 可以动态添加分支的分支节点, 无 BlockOrderIcon 节点
|
* - simpleSplit: (最原始的 id)
|
||||||
* simpleSplit: (最原始的 id)
|
|
||||||
* blockIcon
|
* blockIcon
|
||||||
* inlineBlocks
|
* inlineBlocks
|
||||||
* block1
|
* node1
|
||||||
* block2
|
* node2
|
||||||
*/
|
*/
|
||||||
export const SimpleSplitRegistry: FlowNodeRegistry = {
|
export const SimpleSplitRegistry: FlowNodeRegistry = {
|
||||||
type: FlowNodeSplitType.SIMPLE_SPLIT,
|
type: FlowNodeSplitType.SIMPLE_SPLIT,
|
||||||
@ -24,23 +23,16 @@ export const SimpleSplitRegistry: FlowNodeRegistry = {
|
|||||||
) {
|
) {
|
||||||
const { document } = originParent;
|
const { document } = originParent;
|
||||||
const parent = document.getNode(`$inlineBlocks$${originParent.id}`);
|
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(
|
const realBlock = document.addNode(
|
||||||
{
|
{
|
||||||
...blockData,
|
...blockData,
|
||||||
type: blockData.type || FlowNodeBaseType.BLOCK,
|
type: blockData.type || FlowNodeBaseType.BLOCK,
|
||||||
parent: proxyBlock,
|
parent,
|
||||||
},
|
},
|
||||||
addedNodes
|
addedNodes
|
||||||
);
|
);
|
||||||
addedNodes.push(proxyBlock, realBlock);
|
addedNodes.push(realBlock);
|
||||||
return proxyBlock;
|
return realBlock;
|
||||||
},
|
},
|
||||||
// addChild(node, json, options = {}) {
|
// addChild(node, json, options = {}) {
|
||||||
// const { index } = options;
|
// const { index } = options;
|
||||||
|
|||||||
@ -32,6 +32,11 @@ import {
|
|||||||
StaticSplitRegistry,
|
StaticSplitRegistry,
|
||||||
TryCatchRegistry,
|
TryCatchRegistry,
|
||||||
SimpleSplitRegistry,
|
SimpleSplitRegistry,
|
||||||
|
BreakRegistry,
|
||||||
|
MultiOuputsRegistry,
|
||||||
|
MultiInputsRegistry,
|
||||||
|
InputRegistry,
|
||||||
|
OuputRegistry,
|
||||||
} from './activities';
|
} from './activities';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -59,7 +64,12 @@ export class FlowRegisters
|
|||||||
TryCatchRegistry, // TryCatch
|
TryCatchRegistry, // TryCatch
|
||||||
EndRegistry, // 结束节点
|
EndRegistry, // 结束节点
|
||||||
LoopRegistry, // 循环节点
|
LoopRegistry, // 循环节点
|
||||||
EmptyRegistry // 占位节点
|
EmptyRegistry, // 占位节点
|
||||||
|
BreakRegistry, // 分支断开
|
||||||
|
MultiOuputsRegistry,
|
||||||
|
MultiInputsRegistry,
|
||||||
|
InputRegistry,
|
||||||
|
OuputRegistry
|
||||||
);
|
);
|
||||||
/**
|
/**
|
||||||
* 注册节点数据 (ECS - Component)
|
* 注册节点数据 (ECS - Component)
|
||||||
|
|||||||
@ -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 { useObserve } from '@flowgram.ai/reactive';
|
||||||
import { useStartDragNode } from '@flowgram.ai/fixed-drag-plugin';
|
import { useStartDragNode } from '@flowgram.ai/fixed-drag-plugin';
|
||||||
@ -93,13 +93,17 @@ export interface NodeRenderReturnType {
|
|||||||
*/
|
*/
|
||||||
export function useNodeRender(nodeFromProps?: FlowNodeEntity): NodeRenderReturnType {
|
export function useNodeRender(nodeFromProps?: FlowNodeEntity): NodeRenderReturnType {
|
||||||
const renderNode = nodeFromProps || useContext<FlowNodeEntity>(PlaygroundEntityContext);
|
const renderNode = nodeFromProps || useContext<FlowNodeEntity>(PlaygroundEntityContext);
|
||||||
|
const nodeCache = useRef<FlowNodeEntity | undefined>();
|
||||||
const renderData = renderNode.getData<FlowNodeRenderData>(FlowNodeRenderData)!;
|
const renderData = renderNode.getData<FlowNodeRenderData>(FlowNodeRenderData)!;
|
||||||
const { expanded, dragging, activated } = renderData;
|
const { expanded, dragging, activated } = renderData;
|
||||||
const { startDrag: startDragOrigin } = useStartDragNode();
|
const { startDrag: startDragOrigin } = useStartDragNode();
|
||||||
const playground = usePlayground();
|
const playground = usePlayground();
|
||||||
const isBlockOrderIcon = renderNode.flowNodeType === FlowNodeBaseType.BLOCK_ORDER_ICON;
|
const isBlockOrderIcon = renderNode.flowNodeType === FlowNodeBaseType.BLOCK_ORDER_ICON;
|
||||||
const isBlockIcon = renderNode.flowNodeType === FlowNodeBaseType.BLOCK_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 operationService = useService<FlowOperationService>(FlowOperationService);
|
||||||
const deleteNode = useCallback(() => {
|
const deleteNode = useCallback(() => {
|
||||||
operationService.deleteNode(node);
|
operationService.deleteNode(node);
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@flowgram.ai/reactive": "workspace:*",
|
"@flowgram.ai/reactive": "workspace:*",
|
||||||
"@flowgram.ai/utils": "workspace:*",
|
"@flowgram.ai/utils": "workspace:*",
|
||||||
|
"fast-equals": "^2.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nanoid": "^4.0.2"
|
"nanoid": "^4.0.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 { Disposable, Emitter } from '@flowgram.ai/utils';
|
||||||
import { ReactiveState } from '@flowgram.ai/reactive';
|
import { ReactiveState } from '@flowgram.ai/reactive';
|
||||||
|
|
||||||
@ -78,11 +79,14 @@ export class FormModel<TValues = any> implements Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get values() {
|
get values() {
|
||||||
return cloneDeep(this.store.values) || cloneDeep(this.initialValues);
|
return clone(this.store.values) || clone(this.initialValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
set values(v) {
|
set values(v) {
|
||||||
const prevValues = this.values;
|
const prevValues = this.values;
|
||||||
|
if (shallowEqual(this.store.values || this.initialValues, v)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.store.values = v;
|
this.store.values = v;
|
||||||
this.fireOnFormValuesChange({
|
this.fireOnFormValuesChange({
|
||||||
values: this.values,
|
values: this.values,
|
||||||
|
|||||||
@ -147,7 +147,10 @@ export class FormModelV2 extends FormModel implements Disposable {
|
|||||||
|
|
||||||
updateFormValues(value: any) {
|
updateFormValues(value: any) {
|
||||||
if (this.nativeFormModel) {
|
if (this.nativeFormModel) {
|
||||||
this.nativeFormModel.values = value;
|
const finalValue = this.formMeta.formatOnInit
|
||||||
|
? this.formMeta.formatOnInit(value, this.nodeContext)
|
||||||
|
: value;
|
||||||
|
this.nativeFormModel.values = finalValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -777,6 +777,12 @@
|
|||||||
"projectFolder": "apps/demo-vite",
|
"projectFolder": "apps/demo-vite",
|
||||||
"versionPolicyName": "appPolicy",
|
"versionPolicyName": "appPolicy",
|
||||||
"tags": ["level-1", "team-flow", "demo"]
|
"tags": ["level-1", "team-flow", "demo"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"packageName": "@flowgram.ai/demo-playground",
|
||||||
|
"projectFolder": "apps/demo-playground",
|
||||||
|
"versionPolicyName": "appPolicy",
|
||||||
|
"tags": ["level-1", "team-flow", "demo"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user