chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
@@ -0,0 +1,15 @@
import '@flowgram.ai/fixed-layout-editor/index.css';
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
import { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';
const FlowGramApp = () => (
<FixedLayoutEditorProvider
materials={{
components: defaultFixedSemiMaterials,
}}
>
<EditorRenderer />
</FixedLayoutEditorProvider>
);
export default FlowGramApp;
@@ -0,0 +1,79 @@
import '@flowgram.ai/fixed-layout-editor/index.css';
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
import {
FixedLayoutEditorProvider,
EditorRenderer,
FlowNodeEntity,
useNodeRender,
} from '@flowgram.ai/fixed-layout-editor';
export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {
const {
onMouseEnter,
onMouseLeave,
startDrag,
form,
dragging,
isBlockOrderIcon,
isBlockIcon,
activated,
} = useNodeRender();
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={(e) => {
startDrag(e);
e.stopPropagation();
}}
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',
borderRadius: 8,
boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
padding: 12,
cursor: 'move',
opacity: dragging ? 0.3 : 1,
...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),
}}
>
{form?.render()}
</div>
);
};
const FlowGramApp = () => (
<FixedLayoutEditorProvider
nodeRegistries={[
{
type: 'custom',
},
]}
initialData={{
nodes: [
{
id: 'custom_0',
type: 'custom',
},
],
}}
materials={{
renderDefaultNode: NodeRender,
components: defaultFixedSemiMaterials,
}}
>
<EditorRenderer />
</FixedLayoutEditorProvider>
);
export default FlowGramApp;
@@ -0,0 +1,206 @@
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FC } from 'react';
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
import {
FixedLayoutEditorProvider,
EditorRenderer,
FlowNodeEntity,
useNodeRender,
FlowNodeJSON,
FlowOperationService,
usePlayground,
useService,
FlowRendererKey,
useClientContext,
} from '@flowgram.ai/fixed-layout-editor';
export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {
const {
onMouseEnter,
onMouseLeave,
startDrag,
form,
dragging,
isBlockOrderIcon,
isBlockIcon,
activated,
} = useNodeRender();
const ctx = useClientContext();
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={(e) => {
startDrag(e);
e.stopPropagation();
}}
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',
borderRadius: 8,
boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
padding: 12,
cursor: 'move',
opacity: dragging ? 0.3 : 1,
...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),
}}
>
{form?.render()}
{/* 删除按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
ctx.operation.deleteNode(node);
}}
style={{
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
border: 'none',
borderRadius: '50%',
background: '#fff',
color: '#666',
fontSize: 12,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
transition: 'all 0.2s',
}}
>
×
</button>
</div>
);
};
const useAddNode = () => {
const playground = usePlayground();
const flowOperationService = useService(FlowOperationService) as FlowOperationService;
const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const blocks = addProps.blocks ? addProps.blocks : undefined;
const entity = flowOperationService.addFromNode(dropNode, {
...addProps,
blocks,
});
setTimeout(() => {
playground.scrollToView({
bounds: entity.bounds,
scrollToCenter: true,
});
}, 10);
return entity;
};
const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const index = dropNode.index + 1;
const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {
index,
});
return entity;
};
return {
handleAdd,
handleAddBranch,
};
};
const Adder: FC<{
from: FlowNodeEntity;
to?: FlowNodeEntity;
hoverActivated: boolean;
}> = ({ from, hoverActivated }) => {
const playground = usePlayground();
const { handleAdd } = useAddNode();
if (playground.config.readonlyOrDisabled) return null;
return (
<div
style={{
width: hoverActivated ? 15 : 6,
height: hoverActivated ? 15 : 6,
backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',
color: '#fff',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
onClick={() => {
handleAdd({ type: 'custom', id: `custom_${Date.now()}` }, from);
}}
>
{hoverActivated ? (
<span
style={{
color: '#3370ff',
fontSize: 12,
}}
>
+
</span>
) : null}
</div>
);
};
const FlowGramApp = () => (
<FixedLayoutEditorProvider
nodeRegistries={[
{
type: 'custom',
},
]}
initialData={{
nodes: [
{
id: 'start_0',
type: 'start',
},
{
id: 'custom_1',
type: 'custom',
},
{
id: 'end_2',
type: 'end',
},
],
}}
onAllLayersRendered={(ctx) => {
setTimeout(() => {
ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
}, 10);
}}
materials={{
renderDefaultNode: NodeRender,
components: {
...defaultFixedSemiMaterials,
[FlowRendererKey.ADDER]: Adder,
},
}}
>
<EditorRenderer />
</FixedLayoutEditorProvider>
);
export default FlowGramApp;
@@ -0,0 +1,212 @@
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FC } from 'react';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
import {
FixedLayoutEditorProvider,
EditorRenderer,
FlowNodeEntity,
useNodeRender,
FlowNodeJSON,
FlowOperationService,
usePlayground,
useService,
FlowRendererKey,
useClientContext,
} from '@flowgram.ai/fixed-layout-editor';
export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {
const {
onMouseEnter,
onMouseLeave,
startDrag,
form,
dragging,
isBlockOrderIcon,
isBlockIcon,
activated,
} = useNodeRender();
const ctx = useClientContext();
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={(e) => {
startDrag(e);
e.stopPropagation();
}}
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',
borderRadius: 8,
boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
padding: 12,
cursor: 'move',
opacity: dragging ? 0.3 : 1,
...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),
}}
>
{form?.render()}
{/* 删除按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
ctx.operation.deleteNode(node);
}}
style={{
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
border: 'none',
borderRadius: '50%',
background: '#fff',
color: '#666',
fontSize: 12,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
transition: 'all 0.2s',
}}
>
×
</button>
</div>
);
};
const useAddNode = () => {
const playground = usePlayground();
const flowOperationService = useService(FlowOperationService) as FlowOperationService;
const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const blocks = addProps.blocks ? addProps.blocks : undefined;
const entity = flowOperationService.addFromNode(dropNode, {
...addProps,
blocks,
});
setTimeout(() => {
playground.scrollToView({
bounds: entity.bounds,
scrollToCenter: true,
});
}, 10);
return entity;
};
const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const index = dropNode.index + 1;
const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {
index,
});
return entity;
};
return {
handleAdd,
handleAddBranch,
};
};
const Adder: FC<{
from: FlowNodeEntity;
to?: FlowNodeEntity;
hoverActivated: boolean;
}> = ({ from, hoverActivated }) => {
const playground = usePlayground();
const { handleAdd } = useAddNode();
if (playground.config.readonlyOrDisabled) return null;
return (
<div
style={{
width: hoverActivated ? 15 : 6,
height: hoverActivated ? 15 : 6,
backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',
color: '#fff',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
onClick={() => {
handleAdd({ type: 'custom', id: `custom_${Date.now()}` }, from);
}}
>
{hoverActivated ? (
<span
style={{
color: '#3370ff',
fontSize: 12,
}}
>
+
</span>
) : null}
</div>
);
};
const FlowGramApp = () => (
<FixedLayoutEditorProvider
plugins={() => [
createMinimapPlugin({
enableDisplayAllNodes: true,
}),
]}
nodeRegistries={[
{
type: 'custom',
},
]}
initialData={{
nodes: [
{
id: 'start_0',
type: 'start',
},
{
id: 'custom_1',
type: 'custom',
},
{
id: 'end_2',
type: 'end',
},
],
}}
onAllLayersRendered={(ctx) => {
setTimeout(() => {
ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
}, 10);
}}
materials={{
renderDefaultNode: NodeRender,
components: {
...defaultFixedSemiMaterials,
[FlowRendererKey.ADDER]: Adder,
},
}}
>
<EditorRenderer />
</FixedLayoutEditorProvider>
);
export default FlowGramApp;
@@ -0,0 +1,91 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FC } from 'react';
import {
FlowNodeEntity,
FlowNodeJSON,
FlowOperationService,
usePlayground,
useService,
} from '@flowgram.ai/fixed-layout-editor';
const useAddNode = () => {
const playground = usePlayground();
const flowOperationService = useService(FlowOperationService) as FlowOperationService;
const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const blocks = addProps.blocks ? addProps.blocks : undefined;
const entity = flowOperationService.addFromNode(dropNode, {
...addProps,
blocks,
});
setTimeout(() => {
playground.scrollToView({
bounds: entity.bounds,
scrollToCenter: true,
});
}, 10);
return entity;
};
const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const index = dropNode.index + 1;
const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {
index,
});
return entity;
};
return {
handleAdd,
handleAddBranch,
};
};
export const Adder: FC<{
from: FlowNodeEntity;
to?: FlowNodeEntity;
hoverActivated: boolean;
}> = ({ from, hoverActivated }) => {
const playground = usePlayground();
const { handleAdd } = useAddNode();
if (playground.config.readonlyOrDisabled) return null;
return (
<div
style={{
width: hoverActivated ? 15 : 6,
height: hoverActivated ? 15 : 6,
backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',
color: '#fff',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
onClick={() => {
handleAdd({ type: 'custom', id: `custom_${Date.now()}` }, from);
}}
>
{hoverActivated ? (
<span
style={{
color: '#3370ff',
fontSize: 12,
}}
>
+
</span>
) : null}
</div>
);
};
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';
import { useEditorProps } from './use-editor-props';
const FlowGramApp = () => {
const editorProps = useEditorProps();
return (
<FixedLayoutEditorProvider {...editorProps}>
<EditorRenderer />
</FixedLayoutEditorProvider>
);
};
export default FlowGramApp;
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import type { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
export const initialData: FlowDocumentJSON = {
nodes: [
{
id: 'start_0',
type: 'start',
},
{
id: 'custom_1',
type: 'custom',
},
{
id: 'end_2',
type: 'end',
},
],
};
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
export const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [
{
type: 'custom',
},
];
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FlowNodeEntity, useNodeRender, useClientContext } from '@flowgram.ai/fixed-layout-editor';
export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {
const {
onMouseEnter,
onMouseLeave,
startDrag,
form,
dragging,
isBlockOrderIcon,
isBlockIcon,
activated,
} = useNodeRender();
const ctx = useClientContext();
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={(e) => {
startDrag(e);
e.stopPropagation();
}}
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',
borderRadius: 8,
boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
padding: 12,
cursor: 'move',
opacity: dragging ? 0.3 : 1,
...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),
}}
>
{form?.render()}
{/* 删除按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
ctx.operation.deleteNode(node);
}}
style={{
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
border: 'none',
borderRadius: '50%',
background: '#fff',
color: '#666',
fontSize: 12,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
transition: 'all 0.2s',
}}
>
×
</button>
</div>
);
};
@@ -0,0 +1,39 @@
import '@flowgram.ai/fixed-layout-editor/index.css';
import { useMemo } from 'react';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
import { FlowRendererKey, FixedLayoutProps } from '@flowgram.ai/fixed-layout-editor';
import { NodeRender } from './node-render';
import { nodeRegistries } from './node-registries';
import { initialData } from './initial-data';
import { Adder } from './adder';
export function useEditorProps(): FixedLayoutProps {
return useMemo<FixedLayoutProps>(
() => ({
plugins: () => [
createMinimapPlugin({
enableDisplayAllNodes: true,
}),
],
nodeRegistries,
initialData,
onAllLayersRendered: (ctx) => {
setTimeout(() => {
ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
}, 10);
},
materials: {
renderDefaultNode: NodeRender,
components: {
...defaultFixedSemiMaterials,
[FlowRendererKey.ADDER]: Adder,
},
},
}),
[]
);
}
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FC } from 'react';
import {
FlowNodeEntity,
FlowNodeJSON,
FlowOperationService,
usePlayground,
useService,
} from '@flowgram.ai/fixed-layout-editor';
const useAddNode = () => {
const playground = usePlayground();
const flowOperationService = useService(FlowOperationService) as FlowOperationService;
const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const blocks = addProps.blocks ? addProps.blocks : undefined;
const entity = flowOperationService.addFromNode(dropNode, {
...addProps,
blocks,
});
setTimeout(() => {
playground.scrollToView({
bounds: entity.bounds,
scrollToCenter: true,
});
}, 10);
return entity;
};
const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const index = dropNode.index + 1;
const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {
index,
});
return entity;
};
return {
handleAdd,
handleAddBranch,
};
};
export const Adder: FC<{
from: FlowNodeEntity;
to?: FlowNodeEntity;
hoverActivated: boolean;
}> = ({ from, hoverActivated }) => {
const playground = usePlayground();
const { handleAdd } = useAddNode();
if (playground.config.readonlyOrDisabled) return null;
return (
<div
style={{
width: hoverActivated ? 15 : 6,
height: hoverActivated ? 15 : 6,
backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',
color: '#fff',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
onClick={() => {
handleAdd(
{
type: 'custom',
id: `custom_${Date.now()}`,
data: {
title: 'New Custom Node',
content: 'Custom Node Content',
},
},
from
);
}}
>
{hoverActivated ? (
<span
style={{
color: '#3370ff',
fontSize: 12,
}}
>
+
</span>
) : null}
</div>
);
};
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';
import { useEditorProps } from './use-editor-props';
const FlowGramApp = () => {
const editorProps = useEditorProps();
return (
<FixedLayoutEditorProvider {...editorProps}>
<EditorRenderer />
</FixedLayoutEditorProvider>
);
};
export default FlowGramApp;
@@ -0,0 +1,81 @@
import type { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
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',
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',
},
},
],
};
@@ -0,0 +1,79 @@
/**
* Copyright (c) 202 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
const randomID = () => Math.random().toString(36).slice(2, 7);
export const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [
{
/**
* 自定义节点类型
*/
type: 'condition',
/**
* 自定义节点扩展:
* - loop: 扩展为循环节点
* - start: 扩展为开始节点
* - dynamicSplit: 扩展为分支节点
* - end: 扩展为结束节点
* - tryCatch: 扩展为 tryCatch 节点
* - break: 分支断开
* - default: 扩展为普通节点 (默认)
*/
extend: 'dynamicSplit',
/**
* 节点配置信息
*/
meta: {
// isStart: false, // 是否为开始节点
// isNodeEnd: false, // 是否为结束节点,结束节点后边无法再添加节点
// draggable: false, // 是否可拖拽,如开始节点和结束节点无法拖拽
// selectable: false, // 触发器等开始节点不能被框选
// deleteDisable: true, // 禁止删除
// copyDisable: true, // 禁止copy
// addDisable: true, // 禁止添加
},
onAdd() {
return {
id: `condition_${randomID()}`,
type: 'condition',
data: {
title: 'Condition',
},
blocks: [
{
id: randomID(),
type: 'block',
data: {
title: 'If_0',
},
},
{
id: randomID(),
type: 'block',
data: {
title: 'If_1',
},
},
],
};
},
},
{
type: 'custom',
meta: {},
onAdd() {
return {
id: `custom_${randomID()}`,
type: 'custom',
data: {
title: 'Custom',
content: 'this is custom content',
},
};
},
},
];
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FlowNodeEntity, useNodeRender, useClientContext } from '@flowgram.ai/fixed-layout-editor';
export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {
const {
onMouseEnter,
onMouseLeave,
startDrag,
form,
dragging,
isBlockOrderIcon,
isBlockIcon,
activated,
} = useNodeRender();
const ctx = useClientContext();
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={(e) => {
startDrag(e);
e.stopPropagation();
}}
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',
borderRadius: 8,
boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
padding: 12,
cursor: 'move',
opacity: dragging ? 0.3 : 1,
...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),
}}
>
{form?.render()}
{/* 删除按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
ctx.operation.deleteNode(node);
}}
style={{
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
border: 'none',
borderRadius: '50%',
background: '#fff',
color: '#666',
fontSize: 12,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
transition: 'all 0.2s',
}}
>
×
</button>
</div>
);
};
@@ -0,0 +1,81 @@
import '@flowgram.ai/fixed-layout-editor/index.css';
import { useMemo } from 'react';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
import { FlowRendererKey, FixedLayoutProps, Field } from '@flowgram.ai/fixed-layout-editor';
import { NodeRender } from './node-render';
import { nodeRegistries } from './node-registries';
import { initialData } from './initial-data';
import { Adder } from './adder';
export function useEditorProps(): FixedLayoutProps {
return useMemo<FixedLayoutProps>(
() => ({
plugins: () => [
createMinimapPlugin({
enableDisplayAllNodes: true,
}),
],
nodeRegistries,
initialData,
materials: {
renderDefaultNode: NodeRender,
components: {
...defaultFixedSemiMaterials,
[FlowRendererKey.ADDER]: Adder,
},
},
onAllLayersRendered: (ctx) => {
setTimeout(() => {
ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
}, 10);
},
/**
* Get the default node registry, which will be merged with the 'nodeRegistries'
* 提供默认的节点注册,这个会和 nodeRegistries 做合并
*/
getNodeDefaultRegistry(type) {
return {
type,
meta: {
defaultExpanded: true,
},
formMeta: {
/**
* Render form
*/
render: () => (
<>
<Field<string> name="title">{({ field }) => <div>{field.value}</div>}</Field>
<Field<string> name="content">
<input />
</Field>
</>
),
},
};
},
/**
* Redo/Undo enable
*/
history: {
enable: true,
enableChangeNode: true, // Listen Node engine data change
onApply: (ctx) => {
if (ctx.document.disposed) return;
// Listen change to trigger auto save
console.log('auto save: ', ctx.document.toJSON());
},
},
/**
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
*/ nodeEngine: {
enable: true,
},
}),
[]
);
}
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FC } from 'react';
import {
FlowNodeEntity,
FlowNodeJSON,
FlowOperationService,
usePlayground,
useService,
} from '@flowgram.ai/fixed-layout-editor';
const useAddNode = () => {
const playground = usePlayground();
const flowOperationService = useService(FlowOperationService) as FlowOperationService;
const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const blocks = addProps.blocks ? addProps.blocks : undefined;
const entity = flowOperationService.addFromNode(dropNode, {
...addProps,
blocks,
});
setTimeout(() => {
playground.scrollToView({
bounds: entity.bounds,
scrollToCenter: true,
});
}, 10);
return entity;
};
const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {
const index = dropNode.index + 1;
const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {
index,
});
return entity;
};
return {
handleAdd,
handleAddBranch,
};
};
export const Adder: FC<{
from: FlowNodeEntity;
to?: FlowNodeEntity;
hoverActivated: boolean;
}> = ({ from, hoverActivated }) => {
const playground = usePlayground();
const { handleAdd } = useAddNode();
if (playground.config.readonlyOrDisabled) return null;
return (
<div
style={{
width: hoverActivated ? 15 : 6,
height: hoverActivated ? 15 : 6,
backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',
color: '#fff',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
onClick={() => {
handleAdd(
{
type: 'custom',
id: `custom_${Date.now()}`,
data: {
title: 'New Custom Node',
content: 'Custom Node Content',
},
},
from
);
}}
>
{hoverActivated ? (
<span
style={{
color: '#3370ff',
fontSize: 12,
}}
>
+
</span>
) : null}
</div>
);
};
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';
import { useEditorProps } from './use-editor-props';
import { Tools } from './tools';
import { Minimap } from './minimap';
const FlowGramApp = () => {
const editorProps = useEditorProps();
return (
<FixedLayoutEditorProvider {...editorProps}>
<EditorRenderer />
<Tools />
<Minimap />
</FixedLayoutEditorProvider>
);
};
export default FlowGramApp;
@@ -0,0 +1,81 @@
import type { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
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',
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',
},
},
],
};
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { MinimapRender } from '@flowgram.ai/minimap-plugin';
export const Minimap = () => (
<div
style={{
position: 'absolute',
left: 16,
bottom: 72,
zIndex: 100,
width: 118,
}}
>
<MinimapRender
containerStyles={{
pointerEvents: 'auto',
position: 'relative',
top: 'unset',
right: 'unset',
bottom: 'unset',
left: 'unset',
}}
inactiveStyle={{
opacity: 1,
scale: 1,
translateX: 0,
translateY: 0,
}}
/>
</div>
);
@@ -0,0 +1,79 @@
/**
* Copyright (c) 202 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
const randomID = () => Math.random().toString(36).slice(2, 7);
export const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [
{
/**
* 自定义节点类型
*/
type: 'condition',
/**
* 自定义节点扩展:
* - loop: 扩展为循环节点
* - start: 扩展为开始节点
* - dynamicSplit: 扩展为分支节点
* - end: 扩展为结束节点
* - tryCatch: 扩展为 tryCatch 节点
* - break: 分支断开
* - default: 扩展为普通节点 (默认)
*/
extend: 'dynamicSplit',
/**
* 节点配置信息
*/
meta: {
// isStart: false, // 是否为开始节点
// isNodeEnd: false, // 是否为结束节点,结束节点后边无法再添加节点
// draggable: false, // 是否可拖拽,如开始节点和结束节点无法拖拽
// selectable: false, // 触发器等开始节点不能被框选
// deleteDisable: true, // 禁止删除
// copyDisable: true, // 禁止copy
// addDisable: true, // 禁止添加
},
onAdd() {
return {
id: `condition_${randomID()}`,
type: 'condition',
data: {
title: 'Condition',
},
blocks: [
{
id: randomID(),
type: 'block',
data: {
title: 'If_0',
},
},
{
id: randomID(),
type: 'block',
data: {
title: 'If_1',
},
},
],
};
},
},
{
type: 'custom',
meta: {},
onAdd() {
return {
id: `custom_${randomID()}`,
type: 'custom',
data: {
title: 'Custom',
content: 'this is custom content',
},
};
},
},
];
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { FlowNodeEntity, useNodeRender, useClientContext } from '@flowgram.ai/fixed-layout-editor';
export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {
const {
onMouseEnter,
onMouseLeave,
startDrag,
form,
dragging,
isBlockOrderIcon,
isBlockIcon,
activated,
} = useNodeRender();
const ctx = useClientContext();
return (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={(e) => {
startDrag(e);
e.stopPropagation();
}}
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',
borderRadius: 8,
boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
padding: 12,
cursor: 'move',
opacity: dragging ? 0.3 : 1,
...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),
}}
>
{form?.render()}
{/* 删除按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
ctx.operation.deleteNode(node);
}}
style={{
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
border: 'none',
borderRadius: '50%',
background: '#fff',
color: '#666',
fontSize: 12,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
transition: 'all 0.2s',
}}
>
×
</button>
</div>
);
};
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { CSSProperties, useEffect, useState } from 'react';
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';
export const Tools = () => {
const { history } = useClientContext();
const tools = usePlaygroundTools();
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const buttonStyle: CSSProperties = {
border: '1px solid #e0e0e0',
borderRadius: '4px',
cursor: 'pointer',
padding: '4px',
color: '#141414',
background: '#e1e3e4',
};
useEffect(() => {
const disposable = history.undoRedoService.onChange(() => {
setCanUndo(history.canUndo());
setCanRedo(history.canRedo());
});
return () => disposable.dispose();
}, [history]);
return (
<div
style={{ position: 'absolute', zIndex: 10, bottom: 34, left: 16, display: 'flex', gap: 8 }}
>
<button style={buttonStyle} onClick={() => tools.zoomin()}>
ZoomIn
</button>
<button style={buttonStyle} onClick={() => tools.zoomout()}>
ZoomOut
</button>
<span
style={{
...buttonStyle,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'default',
width: 40,
}}
>
{Math.floor(tools.zoom * 100)}%
</span>
<button style={buttonStyle} onClick={() => tools.fitView()}>
FitView
</button>
<button style={buttonStyle} onClick={() => tools.changeLayout()}>
ChangeLayout
</button>
<button
style={{
...buttonStyle,
cursor: canUndo ? 'pointer' : 'not-allowed',
color: canUndo ? '#141414' : '#b1b1b1',
}}
onClick={() => history.undo()}
disabled={!canUndo}
>
Undo
</button>
<button
style={{
...buttonStyle,
cursor: canRedo ? 'pointer' : 'not-allowed',
color: canRedo ? '#141414' : '#b1b1b1',
}}
onClick={() => history.redo()}
disabled={!canRedo}
>
Redo
</button>
</div>
);
};
@@ -0,0 +1,87 @@
import '@flowgram.ai/fixed-layout-editor/index.css';
import { useMemo } from 'react';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
import { FlowRendererKey, FixedLayoutProps, Field } from '@flowgram.ai/fixed-layout-editor';
import { NodeRender } from './node-render';
import { nodeRegistries } from './node-registries';
import { initialData } from './initial-data';
import { Adder } from './adder';
export function useEditorProps(): FixedLayoutProps {
return useMemo<FixedLayoutProps>(
() => ({
plugins: () => [
createMinimapPlugin({
disableLayer: true,
enableDisplayAllNodes: true,
canvasStyle: {
canvasWidth: 100,
canvasHeight: 50,
canvasPadding: 50,
},
}),
],
nodeRegistries,
initialData,
materials: {
renderDefaultNode: NodeRender,
components: {
...defaultFixedSemiMaterials,
[FlowRendererKey.ADDER]: Adder,
},
},
onAllLayersRendered: (ctx) => {
setTimeout(() => {
ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
}, 10);
},
/**
* Get the default node registry, which will be merged with the 'nodeRegistries'
* 提供默认的节点注册,这个会和 nodeRegistries 做合并
*/
getNodeDefaultRegistry(type) {
return {
type,
meta: {
defaultExpanded: true,
},
formMeta: {
/**
* Render form
*/
render: () => (
<>
<Field<string> name="title">{({ field }) => <div>{field.value}</div>}</Field>
<Field<string> name="content">
<input />
</Field>
</>
),
},
};
},
/**
* Redo/Undo enable
*/
history: {
enable: true,
enableChangeNode: true, // Listen Node engine data change
onApply: (ctx) => {
if (ctx.document.disposed) return;
// Listen change to trigger auto save
console.log('auto save: ', ctx.document.toJSON());
},
},
/**
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
*/ nodeEngine: {
enable: true,
},
}),
[]
);
}