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,61 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FC } from 'react';
import { useDark } from '@rspress/core/runtime';
import { Sandpack } from '@codesandbox/sandpack-react';
interface CodePreviewProps {
files: Record<string, string>;
activeFile?: string;
}
export const CodePreview: FC<CodePreviewProps> = ({ files, activeFile }) => {
const dark = useDark();
return (
<Sandpack
files={files}
theme={dark ? 'dark' : 'light'}
template="react-ts"
customSetup={{
dependencies: {
'@flowgram.ai/free-layout-editor': '0.5.5',
'@flowgram.ai/free-snap-plugin': '0.5.5',
'@flowgram.ai/minimap-plugin': '0.5.5',
'styled-components': '5.3.11',
},
}}
options={{
editorHeight: 350,
activeFile,
}}
/>
);
};
export const FixedLayoutCodePreview: FC<CodePreviewProps> = ({ files, activeFile }) => {
const dark = useDark();
return (
<Sandpack
files={files}
theme={dark ? 'dark' : 'light'}
template="react-ts"
customSetup={{
dependencies: {
'@flowgram.ai/fixed-layout-editor': '0.1.0-alpha.19',
// 为了解决semi无法在sandpack使用的问题,单独发了包,将semi打包进@flowgram.ai/fixed-semi-materials中
'@flowgram.ai/fixed-semi-materials': '0.1.0-alpha.19',
'@flowgram.ai/minimap-plugin': '0.1.0-alpha.19',
'styled-components': '5.3.11',
},
}}
options={{
editorHeight: 350,
activeFile,
}}
/>
);
};
@@ -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,
},
}),
[]
);
}
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.doc-feature-overview {
position: relative;
z-index: 1;
.gedit-playground {
position: relative;
width: 100%;
height: 600px;
}
.fixed-demo-tools {
position: relative;
bottom: 40px;
color: black;
}
}
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import './index.less';
// https://github.com/web-infra-dev/rspress/issues/553
const FixedFeatureOverview = React.lazy(() =>
import('@flowgram.ai/demo-fixed-layout').then((module) => ({
default: module.DemoFixedLayout,
}))
);
export { FixedFeatureOverview };
@@ -0,0 +1,184 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
/* eslint-disable import/no-unresolved */
import tryCatch from '@flowgram.ai/demo-fixed-layout-simple/src/data/tryCatch.ts?raw';
import slot from '@flowgram.ai/demo-fixed-layout-simple/src/data/slot.ts?raw';
import multiOutputs from '@flowgram.ai/demo-fixed-layout-simple/src/data/multiOutputs.ts?raw';
import multiInputs from '@flowgram.ai/demo-fixed-layout-simple/src/data/multiInputs.ts?raw';
import loop from '@flowgram.ai/demo-fixed-layout-simple/src/data/loop.ts?raw';
import dynamicSplit from '@flowgram.ai/demo-fixed-layout-simple/src/data/dynamicSplit.ts?raw';
import { PreviewEditor } from '../preview-editor.tsx';
import { FixedLayoutSimple } from './fixed-layout-simple.tsx';
export function CompositeNodesPreview(props: { cellHeight?: number }) {
const previewWidth = '50%';
const editorWidth = '50%';
const cellHeight = props.cellHeight || 300;
return (
<table
className=""
style={{
width: '100%',
border: '1px solid var(--rp-c-divider-light)',
zIndex: 1,
position: 'relative',
}}
>
<tr>
<td style={{ textAlign: 'center' }}>dynamicSplit</td>
<td>
<PreviewEditor
codeInRight
files={{
'index.tsx': {
code: dynamicSplit,
active: true,
},
}}
previewStyle={{
width: previewWidth,
height: cellHeight,
position: 'relative',
}}
editorStyle={{
width: editorWidth,
height: cellHeight,
}}
>
<FixedLayoutSimple hideTools demo="dynamicSplit" />
</PreviewEditor>
</td>
</tr>
<tr>
<td style={{ textAlign: 'center' }}>loop</td>
<td>
<PreviewEditor
codeInRight
files={{
'index.tsx': {
code: loop,
active: true,
},
}}
previewStyle={{
width: previewWidth,
height: cellHeight,
position: 'relative',
}}
editorStyle={{
height: cellHeight,
width: editorWidth,
}}
>
<FixedLayoutSimple hideTools demo="loop" />
</PreviewEditor>
</td>
</tr>
<tr>
<td style={{ textAlign: 'center' }}>tryCatch</td>
<td>
<PreviewEditor
codeInRight
files={{
'index.tsx': {
code: tryCatch,
active: true,
},
}}
previewStyle={{
width: previewWidth,
height: cellHeight,
position: 'relative',
}}
editorStyle={{
height: cellHeight,
width: editorWidth,
}}
>
<FixedLayoutSimple hideTools demo="tryCatch" />
</PreviewEditor>
</td>
</tr>
<tr>
<td style={{ textAlign: 'center' }}>multiInputs</td>
<td>
<PreviewEditor
codeInRight
files={{
'index.tsx': {
code: multiInputs,
active: true,
},
}}
previewStyle={{
width: previewWidth,
height: cellHeight,
position: 'relative',
}}
editorStyle={{
height: cellHeight,
width: editorWidth,
}}
>
<FixedLayoutSimple hideTools demo="multiInputs" />
</PreviewEditor>
</td>
</tr>
<tr>
<td style={{ textAlign: 'center' }}>multiOutputs</td>
<td>
<PreviewEditor
codeInRight
files={{
'index.tsx': {
code: multiOutputs,
active: true,
},
}}
previewStyle={{
width: previewWidth,
height: cellHeight,
position: 'relative',
}}
editorStyle={{
height: cellHeight,
width: editorWidth,
}}
>
<FixedLayoutSimple hideTools demo="multiOutputs" />
</PreviewEditor>
</td>
</tr>
<tr>
<td style={{ textAlign: 'center' }}>slot</td>
<td>
<PreviewEditor
codeInRight
files={{
'index.tsx': {
code: slot,
active: true,
},
}}
previewStyle={{
width: previewWidth,
height: cellHeight,
position: 'relative',
}}
editorStyle={{
height: cellHeight,
width: editorWidth,
}}
>
<FixedLayoutSimple hideTools demo="slot" />
</PreviewEditor>
</td>
</tr>
</table>
);
}
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
// https://github.com/web-infra-dev/rspress/issues/553
const FixedLayoutSimple = React.lazy(() =>
import('@flowgram.ai/demo-fixed-layout-simple').then((module) => ({
default: module.DemoFixedLayout,
}))
);
export { FixedLayoutSimple };
@@ -0,0 +1,7 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export { FixedLayoutSimple } from './fixed-layout-simple.tsx';
export { CompositeNodesPreview } from './composite-nodes-preview.tsx';
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
/* eslint-disable import/no-unresolved */
import nodeRegistriesCode from '@flowgram.ai/demo-fixed-layout-simple/src/node-registries.ts?raw';
import initialDataCode from '@flowgram.ai/demo-fixed-layout-simple/src/initial-data.ts?raw';
import indexCssCode from '@flowgram.ai/demo-fixed-layout-simple/src/index.css?raw';
import useEditorPropsCode from '@flowgram.ai/demo-fixed-layout-simple/src/hooks/use-editor-props.tsx?raw';
import editorCode from '@flowgram.ai/demo-fixed-layout-simple/src/editor.tsx?raw';
import toolsCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/tools.tsx?raw';
import nodeAdderCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/node-adder.tsx?raw';
import miniMapCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/minimap.tsx?raw';
import branchAdderCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/branch-adder.tsx?raw';
import baseNodeCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/base-node.tsx?raw';
import { FixedLayoutSimple } from './index';
import { PreviewEditor } from '../preview-editor';
const indexCode = {
code: editorCode,
active: true,
};
export const FixedLayoutSimplePreview = () => (
<div
style={{
zIndex: 1,
position: 'relative',
}}
>
<PreviewEditor
files={{
'editor.tsx': indexCode,
'index.css': indexCssCode,
'initial-data.ts': initialDataCode,
'node-registries.ts': nodeRegistriesCode,
'use-editor-props.tsx': useEditorPropsCode,
'base-node.tsx': baseNodeCode,
'branch-adder.tsx': branchAdderCode,
'minimap.tsx': miniMapCode,
'node-adder.tsx': nodeAdderCode,
'tools.tsx': toolsCode,
}}
previewStyle={{
height: 500,
}}
editorStyle={{
height: 500,
}}
>
<FixedLayoutSimple />
</PreviewEditor>
</div>
);
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { createDisableDeclarationPlugin } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const VariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.VariableSelector,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
plugins={() => [createDisableDeclarationPlugin()]}
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string[] | undefined> name="variable_selector">
{({ field }) => (
<VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.json-schema-color-picker-container {
.semi-colorPicker-popover-defaultChildren {
width: 100%;
}
}
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import {
ConditionRow,
ConditionRowValueType,
createTypePresetPlugin,
DynamicValueInput,
IFlowConstantRefValue,
type IJsonSchema,
} from '@flowgram.ai/form-materials';
import { ColorPicker } from '@douyinfe/semi-ui';
import { IconColorPalette } from '@douyinfe/semi-icons';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
import './json-schema-preset.css';
const TypeSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.TypeSelector,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
transformInitialNode={{
start_0: (node) => {
node.data.outputs = {
type: 'object',
properties: {
color_output: {
type: 'color',
},
},
};
return node;
},
}}
plugins={() => [
createTypePresetPlugin({
types: [
{
type: 'color',
icon: <IconColorPalette />,
label: 'Color',
ConstantRenderer: ({ value, onChange }) => (
<div className="json-schema-color-picker-container ">
<ColorPicker
alpha={true}
usePopover={true}
value={value ? ColorPicker.colorStringToValue(value) : undefined}
onChange={(_value) => onChange?.(_value.hex)}
/>
</div>
),
conditionRule: {
eq: { type: 'color' },
},
},
],
}),
]}
formMeta={{
render: () => (
<>
<FormHeader />
<b>Type Selector: </b>
<Field<Partial<IJsonSchema> | undefined>
name="type_selector"
defaultValue={{ type: 'color' }}
>
{({ field }) => (
<TypeSelector value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<b>DynamicValueInput: </b>
<Field<IFlowConstantRefValue | undefined>
name="dynamic_value_input"
defaultValue={{ type: 'constant', schema: { type: 'color' }, content: '#b5ed0c' }}
>
{({ field }) => (
<DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<b>Condition: </b>
<Field<ConditionRowValueType | undefined>
name="condition_row"
defaultValue={{
left: { type: 'ref', content: ['start_0', 'color_output'] },
operator: 'eq',
right: { type: 'ref', content: ['start_0', 'color_output'] },
}}
>
{({ field }) => (
<ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
</>
),
}}
/>
);
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { AssignValueType } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const AssignRow = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.AssignRow,
}))
);
export const AssignModeStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<AssignValueType | undefined>
defaultValue={{
operator: 'assign',
left: { type: 'ref', content: ['start_0', 'str'] },
right: { type: 'constant', content: 'Hello World', schema: { type: 'string' } },
}}
name="assign_row"
>
{({ field }) => (
<AssignRow value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
export const DeclareModeStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<AssignValueType | undefined> name="assign_row">
{({ field }) => (
<AssignRow
value={{
operator: 'declare',
left: 'newVariable',
right: {
type: 'constant',
content: 'Hello World',
schema: { type: 'string' },
},
}}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const AssignRows = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.AssignRows,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<AssignRows name="assign_rows" />
</>
),
}}
/>
);
@@ -0,0 +1,171 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field, FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
import { SubCanvasRender, createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
import {
provideBatchInputEffect,
createBatchOutputsFormPlugin,
type IFlowRefValue,
DisplayOutputs,
} from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const BatchVariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.BatchVariableSelector,
}))
);
const BatchOutputs = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.BatchOutputs,
}))
);
type BatchOutputsValueType = Record<string, IFlowRefValue | undefined>;
const createLoopRegistry = (): FlowNodeRegistry => ({
type: 'custom',
meta: {
isContainer: true,
size: {
width: 500,
height: 260,
},
padding: () => ({
top: 160,
bottom: 40,
left: 50,
right: 50,
}),
},
formMeta: {
render: () => (
<>
<FormHeader />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Loop input (select array variable):
</div>
<Field<IFlowRefValue | undefined>
name="loopFor"
defaultValue={{ type: 'ref', content: ['start_0', 'arr', 'arr_obj'] }}
>
{({ field }) => (
<BatchVariableSelector
style={{ width: '100%' }}
value={field.value?.content}
onChange={(val) => field.onChange({ type: 'ref', content: val })}
/>
)}
</Field>
</div>
<SubCanvasRender offsetY={-100} />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Loop outputs (key-value pairs collected into arrays):
</div>
<Field<BatchOutputsValueType | undefined>
name="loopOutputs"
defaultValue={{
result: { type: 'ref', content: ['variable_0', 'result'] },
count: { type: 'ref', content: ['variable_0', 'count'] },
}}
>
{({ field }) => (
<BatchOutputs
style={{ width: '100%' }}
value={field.value}
onChange={(val) => field.onChange(val)}
/>
)}
</Field>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Generated output variables:
</div>
<DisplayOutputs displayFromScope />
</div>
</div>
</>
),
effect: {
loopFor: provideBatchInputEffect,
},
plugins: [
createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' }),
],
},
});
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
height={550}
initialData={{
nodes: [
{
id: 'custom_0',
type: 'custom',
data: {
title: 'Loop',
loopFor: { type: 'ref', content: ['start_0', 'arr', 'arr_obj'] },
},
blocks: [
{
id: 'block_start_0',
type: 'block-start',
data: { title: 'Start' },
meta: { position: { x: 20, y: 0 } },
},
{
id: 'variable_0',
type: 'variable',
data: {
title: 'Variable',
assign: [
{
operator: 'declare',
left: 'result',
right: { type: 'ref', content: ['custom_0_locals', 'item', 'str'] },
},
{
operator: 'declare',
left: 'count',
right: { type: 'ref', content: ['custom_0_locals', 'index'] },
},
],
},
meta: { position: { x: 100, y: 0 } },
},
{
id: 'block_end_0',
type: 'block-end',
data: { title: 'End' },
meta: { position: { x: 360, y: 0 } },
},
],
edges: [
{ sourceNodeID: 'block_start_0', targetNodeID: 'variable_0' },
{ sourceNodeID: 'variable_0', targetNodeID: 'block_end_0' },
],
},
],
edges: [{ sourceNodeID: 'start_0', targetNodeID: 'custom_0' }],
}}
transformRegistry={(props) => ({
...props,
nodeRegistries: [
...(props.nodeRegistries || []).filter((r) => r.type !== 'custom'),
createLoopRegistry(),
],
})}
plugins={(ctx) => [createContainerNodePlugin({})]}
/>
);
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const BatchVariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.BatchVariableSelector,
}))
);
const VariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.VariableSelector,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
BatchVariableSelector (Array variables only):
</div>
<Field<string[] | undefined> name="batch_variable">
{({ field }) => (
<BatchVariableSelector
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
VariableSelector (All variables):
</div>
<Field<string[] | undefined> name="normal_variable">
{({ field }) => (
<VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</div>
</>
),
}}
/>
);
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const BlurInput = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.BlurInput,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
filterStartNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string> name="blur_input" defaultValue="Initial text">
{({ field }) => (
<>
<BlurInput
value={field.value}
onChange={(value) => field.onChange(value)}
placeholder="Please enter text"
/>
<p className="mt-2">Current value: {field.value}</p>
<p className="text-sm text-gray-500">
Note: Value updates after clicking outside the input
</p>
</>
)}
</Field>
</>
),
}}
/>
);
export const PlaceholderStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
filterStartNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string> name="blur_input_placeholder" defaultValue="">
{({ field }) => (
<>
<BlurInput
value={field.value}
onChange={(value) => field.onChange(value)}
placeholder="This is an input field with placeholder"
/>
<p className="mt-2">Current value: {field.value || 'Empty'}</p>
</>
)}
</Field>
</>
),
}}
/>
);
export const DisabledStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
filterStartNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string> name="blur_input_disabled" defaultValue="Disabled state text">
{({ field }) => (
<BlurInput value={field.value} onChange={(value) => field.onChange(value)} disabled />
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const CodeEditor = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.CodeEditor,
}))
);
const defaultTsCode = `// Here, you can retrieve input variables from the node using 'params' and output results using 'ret'.
// 'params' has been correctly injected into the environment.
// Here's an example of getting the value of the parameter named 'input' from the node input:
// const input = params.input;
// Here's an example of outputting a 'ret' object containing multiple data types:
// const ret = { "name": 'Xiaoming', "hobbies": ["Reading", "Traveling"] };
async function main({ params }) {
// Build the output object
const ret = {
key0: params.input + params.input, // Concatenate the input parameter 'input' twice
key1: ["hello", "world"], // Output an array
key2: { // Output an Object
key21: "hi"
},
};
return ret;
}`;
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterStartNode
filterEndNode
height={600}
formMeta={{
render: () => (
<div style={{ width: 500 }}>
<FormHeader />
<Field<string | undefined> name="code_editor" defaultValue={defaultTsCode}>
{({ field }) => (
<CodeEditor
value={field.value}
onChange={(value) => field.onChange(value)}
languageId="typescript"
/>
)}
</Field>
</div>
),
}}
/>
);
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import type { ConditionOpConfigs, IConditionRule } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const ConditionRow = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.ConditionRow,
}))
);
const DBConditionRow = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DBConditionRow,
}))
);
const ConditionProvider = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.ConditionProvider,
}))
);
const OPS: ConditionOpConfigs = {
cop: {
abbreviation: 'C',
label: 'Custom Operator',
},
};
const RULES: Record<string, IConditionRule> = {
string: {
cop: { type: 'string' },
},
};
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<ConditionProvider ops={OPS} rules={RULES}>
<Field<any | undefined> name="condition_row">
{({ field }) => (
<ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<Field<any | undefined> name="db_condition_row">
{({ field }) => (
<DBConditionRow
options={[
{
label: 'UserName',
value: 'username',
schema: { type: 'string' },
},
]}
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</ConditionProvider>
</>
),
}}
/>
);
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const ConditionRow = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.ConditionRow,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<any | undefined>
name="condition_row"
defaultValue={{
left: {
type: 'ref',
content: ['start_0', 'str'],
},
operator: 'eq',
right: {
type: 'constant',
content: 'Hello World!',
schema: {
type: 'string',
},
},
}}
>
{({ field }) => (
<ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,163 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const ConstantInput = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.ConstantInput,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterStartNode
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<b>String</b>
<Field<string> name="constant_string" defaultValue="Hello World">
{({ field }) => (
<ConstantInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'string' }}
/>
)}
</Field>
<b>Enum</b>
<Field<string> name="constant_enum" defaultValue="morning">
{({ field }) => (
<ConstantInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'string', enum: ['morning', 'afternoon'] }}
/>
)}
</Field>
<b>Number</b>
<Field<number> name="constant_number" defaultValue={42}>
{({ field }) => (
<ConstantInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'number' }}
/>
)}
</Field>
<b>Boolean</b>
<Field<boolean> name="constant_boolean" defaultValue={true}>
{({ field }) => (
<ConstantInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'boolean' }}
/>
)}
</Field>
<b>Object</b>
<Field<string>
name="constant_object"
defaultValue={JSON.stringify({ key: 'value', nested: { data: 'test' } })}
>
{({ field }) => (
<ConstantInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'object' }}
/>
)}
</Field>
<b>Array</b>
<Field<string>
name="constant_array"
defaultValue={JSON.stringify([1, 2, 3, 'four', true])}
>
{({ field }) => (
<ConstantInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'array' }}
/>
)}
</Field>
</div>
</>
),
}}
/>
);
export const FallbackRendererStory = () => (
<FreeFormMetaStoryBuilder
filterStartNode
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<any>
name="constant_fallback"
defaultValue={{ custom: 'data', type: 'unsupported' }}
>
{({ field }) => (
<ConstantInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'custom-unsupported-type' }}
fallbackRenderer={({ value, onChange, readonly }) => (
<div style={{ padding: '8px', background: '#f0f0f0', border: '1px dashed #ccc' }}>
<p>Fallback renderer for unsupported type</p>
</div>
)}
/>
)}
</Field>
</>
),
}}
/>
);
export const CustomStrategyStory = () => (
<FreeFormMetaStoryBuilder
filterStartNode
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string> name="constant_custom" defaultValue="Custom Value">
{({ field }) => (
<ConstantInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'object' }}
strategies={[
{
hit: (schema) => schema.type === 'object',
Renderer: ({ value, onChange, readonly }) => <p>Object is not supported now</p>,
},
]}
/>
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const DBConditionRow = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DBConditionRow,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<any | undefined>
name="db_condition_row"
defaultValue={{
left: 'amount',
operator: 'gt',
right: {
type: 'constant',
content: 1000,
schema: {
type: 'number',
},
},
}}
>
{({ field }) => (
<DBConditionRow
options={[
{
label: 'TransactionID',
value: 'transaction_id',
schema: { type: 'integer' },
},
{
label: 'Amount',
value: 'amount',
schema: { type: 'number' },
},
{
label: 'Description',
value: 'description',
schema: { type: 'string' },
},
{
label: 'Archived',
value: 'archived',
schema: { type: 'boolean' },
},
{
label: 'CreateTime',
value: 'create_time',
schema: { type: 'date-time' },
},
]}
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const DynamicValueInput = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DynamicValueInput,
}))
);
const DisplayFlowValue = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DisplayFlowValue,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode={true}
transformInitialNode={{
custom_0: (node) => {
node.data.dynamic_value_input = {
type: 'constant',
content: 'Hello World',
};
return node;
},
}}
formMeta={{
render: () => (
<>
<FormHeader />
<Field<any> name="dynamic_value_input">
{({ field }) => (
<DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<div>
<Field<any> name="dynamic_value_input">
{({ field }) => {
console.log('debugger field value', field);
return <DisplayFlowValue value={field.value} title="Display Flow Value" />;
}}
</Field>
</div>
</>
),
}}
/>
);
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const InputsValuesTree = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.InputsValuesTree,
}))
);
const DisplayInputsValues = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DisplayInputsValues,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode={true}
transformInitialNode={{
custom_0: (node) => {
node.data.inputs_values = {
a: {
b: {
type: 'ref',
content: ['start_0', 'str'],
},
c: {
type: 'constant',
content: 'hello',
},
},
d: {
type: 'ref',
content: ['start_0', 'arr', 'arr_str'],
},
e: {
type: 'ref',
content: ['start_0', 'obj'],
},
};
return node;
},
}}
formMeta={{
render: () => (
<>
<FormHeader />
<Field<Record<string, any> | undefined> name="inputs_values">
{({ field }) => (
<InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<Field<Record<string, any> | undefined> name="inputs_values">
{({ field }) => <DisplayInputsValues value={field.value} />}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { provideJsonSchemaOutputs, type IJsonSchema } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const DisplayOutputs = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DisplayOutputs,
}))
);
const JsonSchemaEditor = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.JsonSchemaEditor,
}))
);
export const DisplayFromScopeStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
filterStartNode
transformInitialNode={{
custom_0: (node) => {
node.data.outputs = {
type: 'object',
properties: {
result: { type: 'string' },
status: { type: 'number' },
},
};
return node;
},
}}
formMeta={{
render: () => (
<>
<FormHeader />
<Field<IJsonSchema | undefined> name="outputs">
{({ field }) => (
<JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<b>Display Outputs by Scope:</b>
<DisplayOutputs displayFromScope />
</>
),
effect: {
outputs: provideJsonSchemaOutputs,
},
}}
/>
);
export const DisplayFromFieldStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
filterStartNode
transformInitialNode={{
custom_0: (node) => {
node.data.outputs = {
type: 'object',
properties: {
result: { type: 'string' },
status: { type: 'number' },
},
};
return node;
},
}}
formMeta={{
render: () => (
<>
<FormHeader />
<Field<IJsonSchema | undefined> name="outputs">
{({ field }) => (
<JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<b>Display Outputs By Schema</b>
<Field<IJsonSchema | undefined> name="outputs">
{({ field }) => <DisplayOutputs value={field.value} />}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const DisplaySchemaTag = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DisplaySchemaTag,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
filterStartNode
formMeta={{
render: () => (
<>
<FormHeader />
<div>
<DisplaySchemaTag
title="Transaction"
value={{
type: 'object',
properties: {
transaction_id: { type: 'integer' },
amount: { type: 'number' },
description: { type: 'string' },
archived: { type: 'boolean' },
owner: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
friends: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
},
},
},
},
},
},
}}
/>
</div>
</>
),
}}
/>
);
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const DisplaySchemaTree = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DisplaySchemaTree,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
filterStartNode
formMeta={{
render: () => (
<>
<FormHeader />
<DisplaySchemaTree
value={{
type: 'object',
properties: {
transaction_id: { type: 'integer' },
amount: { type: 'number' },
description: { type: 'string' },
archived: { type: 'boolean' },
owner: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
friends: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
},
},
},
},
},
},
}}
/>
</>
),
}}
/>
);
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const DynamicValueInput = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DynamicValueInput,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<any> name="dynamic_value_input">
{({ field }) => (
<DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
export const WithSchemaStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<any> name="dynamic_value_input">
{({ field }) => (
<DynamicValueInput
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'string' }}
/>
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const InputsValuesTree = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.InputsValuesTree,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<Record<string, any> | undefined>
name="inputs_values"
defaultValue={{
a: {
b: {
type: 'ref',
content: ['start_0', 'str'],
},
c: {
type: 'constant',
content: 'hello',
},
},
d: {
type: 'constant',
content: '{ "a": "b"}',
schema: { type: 'object' },
},
}}
>
{({ field }) => (
<InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,74 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const InputsValues = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.InputsValues,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<Record<string, any> | undefined>
name="inputs_values"
defaultValue={{
a: {
type: 'ref',
content: ['start_0', 'str'],
},
}}
>
{({ field }) => (
<InputsValues value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
export const WithSchemaStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<Record<string, any> | undefined>
name="inputs_values"
defaultValue={{
a: {
type: 'ref',
content: ['start_0', 'str'],
},
}}
>
{({ field }) => (
<InputsValues
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{
type: 'string',
}}
/>
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const JsonEditorWithVariables = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.JsonEditorWithVariables,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<div style={{ width: 400 }}>
<FormHeader />
<Field<any> name="json_editor_with_variables" defaultValue={`{ "a": {{start_0.str}}}`}>
{({ field }) => (
<JsonEditorWithVariables
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</div>
),
}}
/>
);
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import type { IJsonSchema } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const JsonSchemaCreator = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.JsonSchemaCreator,
}))
);
const JsonSchemaEditor = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.JsonSchemaEditor,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<IJsonSchema | undefined> name="json_schema">
{({ field }) => (
<div>
<JsonSchemaCreator onSchemaCreate={(schema) => field.onChange(schema)} />
<div style={{ marginTop: 16 }}>
<JsonSchemaEditor
value={field.value}
onChange={(value) => field.onChange(value)}
/>
</div>
</div>
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import type { IJsonSchema } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const JsonSchemaEditor = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.JsonSchemaEditor,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterStartNode
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<IJsonSchema | undefined>
name="json_schema_editor"
defaultValue={{
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
}}
>
{({ field }) => (
<JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React, { useState } from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { IFlowTemplateValue, IInputsValues, InputsValuesTree } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const PromptEditorWithInputs = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.PromptEditorWithInputs,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<div style={{ width: 400 }}>
<FormHeader />
<Field<IInputsValues | undefined>
name="inputsValues"
defaultValue={{
a: { type: 'constant', content: '123' },
b: { type: 'ref', content: ['start_0', 'obj'] },
}}
>
{({ field }) => (
<InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<Field<IInputsValues | undefined> name="inputsValues">
{({ field: inputsField }) => (
<Field<IFlowTemplateValue | undefined>
name="prompt_editor_with_inputs"
defaultValue={{
type: 'template',
content: '# Query \n {{b.obj2.num}}',
}}
>
{({ field }) => (
<PromptEditorWithInputs
value={field.value}
onChange={(value) => field.onChange(value)}
inputsValues={inputsField.value || {}}
/>
)}
</Field>
)}
</Field>
</div>
),
}}
/>
);
export const WithoutCanvas = () => {
const [value, setValue] = useState<IFlowTemplateValue | undefined>({
type: 'template',
content: '# Role \nYou are a helpful assistant. \n\n# Query \n{{b.obj2.num}} \n\n',
});
return (
<div>
<PromptEditorWithInputs
value={value}
onChange={(value) => setValue(value)}
inputsValues={{
a: { type: 'constant', content: '123' },
b: {
c: {
d: { type: 'constant', content: 456 },
},
e: { type: 'constant', content: 789 },
},
}}
/>
</div>
);
};
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const PromptEditorWithVariables = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.PromptEditorWithVariables,
}))
);
const VariableSelectorProvider = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.VariableSelectorProvider,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<div style={{ width: 400 }}>
<FormHeader />
<Field<any | undefined>
name="prompt_editor"
defaultValue={{
type: 'template',
content: `# Role
You are a helpful assistant
# Query
{{start_0.str}}`,
}}
>
{({ field }) => (
<PromptEditorWithVariables
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</div>
),
}}
/>
);
const STRING_ONLY_SCHEMA = { type: 'string' };
export const StringOnlyStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<div style={{ width: 400 }}>
<FormHeader />
<VariableSelectorProvider includeSchema={STRING_ONLY_SCHEMA}>
<Field<any | undefined>
name="prompt_editor"
defaultValue={{
type: 'template',
content: `# Role
You are a helpful assistant`,
}}
>
{({ field }) => (
<PromptEditorWithVariables
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</VariableSelectorProvider>
</div>
),
}}
/>
);
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const PromptEditor = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.PromptEditor,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<any | undefined>
name="prompt_editor"
defaultValue={{
type: 'template',
content: '# Role \n You are a helpful assistant',
}}
>
{({ field }) => (
<PromptEditor value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const SQLEditorWithVariables = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.SQLEditorWithVariables,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<div style={{ width: 400 }}>
<FormHeader />
<Field<string | undefined>
name="sql_editor_with_variables"
defaultValue={'SELECT * FROM users \n WHERE user_id = {{start_0.str}}'}
>
{({ field }) => (
<SQLEditorWithVariables
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</div>
),
}}
/>
);
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import type { IJsonSchema } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const TypeSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.TypeSelector,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterStartNode
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<Partial<IJsonSchema> | undefined>
name="type_selector"
defaultValue={{ type: 'string' }}
>
{({ field }) => (
<TypeSelector value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { useVariableTree } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const VariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.VariableSelector,
}))
);
const VariableSelectorProvider = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.VariableSelectorProvider,
}))
);
const Tree = React.lazy(() =>
import('@douyinfe/semi-ui').then((module) => ({
default: module.Tree,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string[] | undefined> name="variable_selector">
{({ field }) => (
<VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
export const FilterSchemaStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string[] | undefined> name="variable_selector">
{({ field }) => (
<VariableSelector
value={field.value}
onChange={(value) => field.onChange(value)}
includeSchema={{ type: 'string' }}
/>
)}
</Field>
</>
),
}}
/>
);
export const CustomFilterStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<VariableSelectorProvider skipVariable={(variable) => variable?.key === 'str'}>
<FormHeader />
<Field<string[] | undefined> name="variable_selector">
{({ field }) => (
<VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</VariableSelectorProvider>
),
}}
/>
);
export const CustomVariableTreeStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => {
const treeData = useVariableTree({});
return (
<VariableSelectorProvider skipVariable={(variable) => variable?.key === 'str'}>
<FormHeader />
<Tree treeData={treeData} defaultExpandAll />
</VariableSelectorProvider>
);
},
}}
/>
);
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { autoRenameRefEffect } from '@flowgram.ai/form-materials';
import { Field } from '@flowgram.ai/fixed-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const InputsValues = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.InputsValues,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
effect: {
inputsValues: autoRenameRefEffect,
},
render: () => (
<>
<FormHeader />
<Field<Record<string, any> | undefined>
name="inputsValues"
defaultValue={{
a: {
type: 'ref',
content: ['start_0', 'str'],
},
}}
>
{({ field }) => (
<InputsValues value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { listenRefSchemaChange } from '@flowgram.ai/form-materials';
import { Field } from '@flowgram.ai/fixed-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const InputsValues = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.InputsValues,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
effect: {
'inputsValues.*': listenRefSchemaChange(({ name, schema, form, formValues }) => {
form.setValueIn(
`log`,
`${form.getValueIn(`log`) || ''}${name}: ${JSON.stringify(schema)}\n`
);
}),
},
render: () => (
<>
<FormHeader />
<Field<Record<string, any> | undefined>
name="inputsValues"
defaultValue={{
a: {
type: 'ref',
content: ['start_0', 'str'],
},
}}
>
{({ field }) => (
<InputsValues value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<Field<any> name="log" defaultValue={'When schema updated, log changes:\n'}>
{({ field }) => (
<pre
style={{
width: 500,
padding: 4,
background: '#f5f5f5',
fontSize: 12,
whiteSpace: 'pre-wrap',
}}
>
{field.value}
</pre>
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { listenRefValueChange } from '@flowgram.ai/form-materials';
import { Field } from '@flowgram.ai/fixed-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const InputsValues = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.InputsValues,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
effect: {
'inputsValues.*': listenRefValueChange(({ name, variable, form }) => {
form.setValueIn(
`log`,
`${form.getValueIn(`log`) || ''}${name}: ${JSON.stringify(variable?.toJSON() || {})} \n`
);
}),
},
render: () => (
<>
<FormHeader />
<Field<Record<string, any> | undefined>
name="inputsValues"
defaultValue={{
a: {
type: 'ref',
content: ['start_0', 'str'],
},
}}
>
{({ field }) => (
<InputsValues value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
<br />
<Field<any> name="log" defaultValue={'When variable value updated, log changes:\n'}>
{({ field }) => (
<pre
style={{
width: 500,
padding: 4,
background: '#f5f5f5',
fontSize: 12,
whiteSpace: 'pre-wrap',
}}
>
{field.value}
</pre>
)}
</Field>
</>
),
}}
/>
);
@@ -0,0 +1,176 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field, FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
import { SubCanvasRender, createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
import {
provideBatchInputEffect,
createBatchOutputsFormPlugin,
type IFlowRefValue,
DisplayOutputs,
} from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const BatchVariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.BatchVariableSelector,
}))
);
const BatchOutputs = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.BatchOutputs,
}))
);
const VariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.VariableSelector,
}))
);
type BatchOutputsValueType = Record<string, IFlowRefValue | undefined>;
const createLoopRegistry = (): FlowNodeRegistry => ({
type: 'custom',
meta: {
isContainer: true,
size: {
width: 500,
height: 260,
},
padding: () => ({
top: 180,
bottom: 40,
left: 50,
right: 50,
}),
},
formMeta: {
render: () => (
<>
<FormHeader />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Loop input (select array variable):
</div>
<Field<IFlowRefValue | undefined>
name="loopFor"
defaultValue={{ type: 'ref', content: ['start_0', 'arr', 'arr_obj'] }}
>
{({ field }) => (
<BatchVariableSelector
style={{ width: '100%' }}
value={field.value?.content}
onChange={(val) => field.onChange({ type: 'ref', content: val })}
/>
)}
</Field>
</div>
<SubCanvasRender offsetY={-120} />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Available local variables (item & index generated by provideBatchInputEffect):
</div>
<Field<string[] | undefined> name="localVariable">
{({ field }) => (
<VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</div>
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Loop outputs (collected into arrays):
</div>
<Field<BatchOutputsValueType | undefined>
name="loopOutputs"
defaultValue={{
names: { type: 'ref', content: ['variable_0', 'name'] },
}}
>
{({ field }) => (
<BatchOutputs
style={{ width: '100%' }}
value={field.value}
onChange={(val) => field.onChange(val)}
/>
)}
</Field>
</div>
<DisplayOutputs displayFromScope />
</>
),
effect: {
loopFor: provideBatchInputEffect,
},
plugins: [
createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' }),
],
},
});
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
height={550}
initialData={{
nodes: [
{
id: 'custom_0',
type: 'custom',
data: {
title: 'Loop',
loopFor: { type: 'ref', content: ['start_0', 'arr', 'arr_obj'] },
},
blocks: [
{
id: 'block_start_0',
type: 'block-start',
data: { title: 'Start' },
meta: { position: { x: 20, y: 0 } },
},
{
id: 'variable_0',
type: 'variable',
data: {
title: 'Variable',
assign: [
{
operator: 'declare',
left: 'name',
right: { type: 'ref', content: ['custom_0_locals', 'item', 'str'] },
},
],
},
meta: { position: { x: 100, y: 0 } },
},
{
id: 'block_end_0',
type: 'block-end',
data: { title: 'End' },
meta: { position: { x: 360, y: 0 } },
},
],
edges: [
{ sourceNodeID: 'block_start_0', targetNodeID: 'variable_0' },
{ sourceNodeID: 'variable_0', targetNodeID: 'block_end_0' },
],
},
],
edges: [{ sourceNodeID: 'start_0', targetNodeID: 'custom_0' }],
}}
transformRegistry={(props) => ({
...props,
nodeRegistries: [
...(props.nodeRegistries || []).filter((r) => r.type !== 'custom'),
createLoopRegistry(),
],
})}
plugins={(ctx) => [createContainerNodePlugin({})]}
/>
);
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import {
provideJsonSchemaOutputs,
syncVariableTitle,
type IJsonSchema,
} from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const JsonSchemaEditor = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.JsonSchemaEditor,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterStartNode
transformInitialNode={{
end_0: (node) => {
node.data.inputsValues = {
success: { type: 'constant', content: true, schema: { type: 'boolean' } },
message: { type: 'ref', content: ['custom_0', 'name'] },
};
return node;
},
}}
formMeta={{
render: () => (
<>
<FormHeader />
<Field<IJsonSchema | undefined>
name="outputs"
defaultValue={{
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
}}
>
{({ field }) => (
<JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
effect: {
// Sync the title to variables
title: syncVariableTitle,
outputs: provideJsonSchemaOutputs,
},
}}
/>
);
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { provideJsonSchemaOutputs, syncVariableTitle } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const Input = React.lazy(() =>
import('@douyinfe/semi-ui').then((module) => ({
default: module.Input,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterStartNode
transformInitialNode={{
end_0: (node) => {
node.data.inputsValues = {
success: { type: 'constant', content: true, schema: { type: 'boolean' } },
message: { type: 'ref', content: ['custom_0', 'name'] },
};
return node;
},
custom_0: (node) => {
node.data.outputs = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
};
return node;
},
}}
formMeta={{
render: () => (
<>
<FormHeader />
<p>Please Edit Title below to sync to variables:</p>
<Field<string | undefined> name="title">
{({ field }) => (
<Input value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</>
),
effect: {
// Sync the title to variables
title: syncVariableTitle,
outputs: provideJsonSchemaOutputs,
},
}}
/>
);
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React, { useEffect } from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { validateFlowValue, validateWhenVariableSync } from '@flowgram.ai/form-materials';
import { Button } from '@douyinfe/semi-ui';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const DynamicValueInput = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DynamicValueInput,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
transformInitialNode={{
custom_0: (node) => {
node.data.value = {
type: 'ref',
content: ['start_0', 'query'],
};
return node;
},
}}
formMeta={{
effect: {
value: validateWhenVariableSync(),
},
validate: {
value: ({ value, context }) =>
validateFlowValue(value, {
node: context.node,
errorMessages: {
unknownVariable: 'Unknown Variable',
},
}),
},
render: ({ form }) => {
useEffect(() => {
form.validate();
}, []);
return (
<>
<FormHeader />
<b>{"Add 'query' in Start"}</b>
<Field<any> name="value">
{({ field, fieldState }) => (
<>
<DynamicValueInput
value={field.value}
onChange={(value) => field.onChange(value)}
/>
<span style={{ color: 'red' }}>
{fieldState.errors?.map((e) => e.message).join('\n')}
</span>
</>
)}
</Field>
<br />
<Button onClick={() => form.validate()}>Trigger Validate</Button>
</>
);
},
}}
/>
);
@@ -0,0 +1,303 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field, FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
import { SubCanvasRender, createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
import {
provideBatchInputEffect,
createBatchOutputsFormPlugin,
type IFlowRefValue,
DisplayOutputs,
} from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const BatchVariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.BatchVariableSelector,
}))
);
const BatchOutputs = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.BatchOutputs,
}))
);
type BatchOutputsValueType = Record<string, IFlowRefValue | undefined>;
const createLoopRegistry = (): FlowNodeRegistry => ({
type: 'custom',
meta: {
isContainer: true,
size: {
width: 500,
height: 260,
},
padding: () => ({
top: 160,
bottom: 40,
left: 50,
right: 50,
}),
},
formMeta: {
render: () => (
<>
<FormHeader />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Loop input (select array variable):
</div>
<Field<IFlowRefValue | undefined>
name="loopFor"
defaultValue={{ type: 'ref', content: ['start_0', 'arr', 'arr_obj'] }}
>
{({ field }) => (
<BatchVariableSelector
style={{ width: '100%' }}
value={field.value?.content}
onChange={(val) => field.onChange({ type: 'ref', content: val })}
/>
)}
</Field>
</div>
<SubCanvasRender offsetY={-100} />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Loop outputs (collected into arrays):
</div>
<Field<BatchOutputsValueType | undefined>
name="loopOutputs"
defaultValue={{
names: { type: 'ref', content: ['variable_0', 'name'] },
}}
>
{({ field }) => (
<BatchOutputs
style={{ width: '100%' }}
value={field.value}
onChange={(val) => field.onChange(val)}
/>
)}
</Field>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Generated output variables:
</div>
<DisplayOutputs displayFromScope />
</div>
</>
),
effect: {
loopFor: provideBatchInputEffect,
},
plugins: [
createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' }),
],
},
});
const createLoopRegistryWithInfer = (): FlowNodeRegistry => ({
type: 'custom',
meta: {
isContainer: true,
size: {
width: 500,
height: 260,
},
padding: () => ({
top: 160,
bottom: 40,
left: 50,
right: 50,
}),
},
formMeta: {
render: () => (
<>
<FormHeader />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>Loop input:</div>
<Field<IFlowRefValue | undefined>
name="loopFor"
defaultValue={{ type: 'ref', content: ['start_0', 'arr', 'arr_obj'] }}
>
{({ field }) => (
<BatchVariableSelector
style={{ width: '100%' }}
value={field.value?.content}
onChange={(val) => field.onChange({ type: 'ref', content: val })}
/>
)}
</Field>
</div>
<SubCanvasRender offsetY={-100} />
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Loop outputs (with schema inference):
</div>
<Field<BatchOutputsValueType | undefined>
name="loopOutputs"
defaultValue={{
items: { type: 'ref', content: ['variable_0', 'item_name'] },
values: { type: 'ref', content: ['variable_0', 'item_index'] },
}}
>
{({ field }) => (
<BatchOutputs
style={{ width: '100%' }}
value={field.value}
onChange={(val) => field.onChange(val)}
/>
)}
</Field>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
Output variables (check Debug panel for inferred schema):
</div>
<DisplayOutputs displayFromScope />
</div>
</>
),
effect: {
loopFor: provideBatchInputEffect,
},
plugins: [
createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' }),
],
},
});
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
height={550}
initialData={{
nodes: [
{
id: 'custom_0',
type: 'custom',
data: {
title: 'Loop',
loopFor: { type: 'ref', content: ['start_0', 'arr', 'arr_obj'] },
},
blocks: [
{
id: 'block_start_0',
type: 'block-start',
data: { title: 'Start' },
meta: { position: { x: 20, y: 0 } },
},
{
id: 'variable_0',
type: 'variable',
data: {
title: 'Variable',
assign: [
{
operator: 'declare',
left: 'name',
right: { type: 'ref', content: ['custom_0_locals', 'item', 'str'] },
},
],
},
meta: { position: { x: 100, y: 0 } },
},
{
id: 'block_end_0',
type: 'block-end',
data: { title: 'End' },
meta: { position: { x: 360, y: 0 } },
},
],
edges: [
{ sourceNodeID: 'block_start_0', targetNodeID: 'variable_0' },
{ sourceNodeID: 'variable_0', targetNodeID: 'block_end_0' },
],
},
],
edges: [{ sourceNodeID: 'start_0', targetNodeID: 'custom_0' }],
}}
transformRegistry={(props) => ({
...props,
nodeRegistries: [
...(props.nodeRegistries || []).filter((r) => r.type !== 'custom'),
createLoopRegistry(),
],
})}
plugins={(ctx) => [createContainerNodePlugin({})]}
/>
);
export const WithInferSchemaStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
height={550}
initialData={{
nodes: [
{
id: 'custom_0',
type: 'custom',
data: {
title: 'Loop',
loopFor: { type: 'ref', content: ['start_0', 'arr', 'arr_obj'] },
},
blocks: [
{
id: 'block_start_0',
type: 'block-start',
data: { title: 'Start' },
meta: { position: { x: 20, y: 0 } },
},
{
id: 'variable_0',
type: 'variable',
data: {
title: 'Variable',
assign: [
{
operator: 'declare',
left: 'item_name',
right: { type: 'ref', content: ['custom_0_locals', 'item', 'str'] },
},
{
operator: 'declare',
left: 'item_index',
right: { type: 'ref', content: ['custom_0_locals', 'index'] },
},
],
},
meta: { position: { x: 100, y: 0 } },
},
{
id: 'block_end_0',
type: 'block-end',
data: { title: 'End' },
meta: { position: { x: 360, y: 0 } },
},
],
edges: [
{ sourceNodeID: 'block_start_0', targetNodeID: 'variable_0' },
{ sourceNodeID: 'variable_0', targetNodeID: 'block_end_0' },
],
},
],
edges: [{ sourceNodeID: 'start_0', targetNodeID: 'custom_0' }],
}}
transformRegistry={(props) => ({
...props,
nodeRegistries: [
...(props.nodeRegistries || []).filter((r) => r.type !== 'custom'),
createLoopRegistryWithInfer(),
],
})}
plugins={(ctx) => [createContainerNodePlugin({})]}
/>
);
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { createInferAssignPlugin } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const AssignRows = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.AssignRows,
}))
);
const DisplayOutputs = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DisplayOutputs,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
transformInitialNode={{
end_0: (node) => {
node.data.inputsValues = {
info: {
type: 'ref',
content: ['custom_0', 'userInfo'],
},
};
return node;
},
}}
formMeta={{
render: () => (
<>
<FormHeader />
<AssignRows
name="assign"
defaultValue={[
// 从常量声明变量
{
operator: 'declare',
left: 'userName',
right: {
type: 'constant',
content: 'John Doe',
schema: { type: 'string' },
},
},
// 从变量声明变量
{
operator: 'declare',
left: 'userInfo',
right: {
type: 'ref',
content: ['start_0', 'obj'],
},
},
// 赋值现有变量
{
operator: 'assign',
left: {
type: 'ref',
content: ['start_0', 'str'],
},
right: {
type: 'constant',
content: 'Hello Flowgram',
schema: { type: 'string' },
},
},
]}
/>
<DisplayOutputs displayFromScope style={{ marginTop: 10 }} />
</>
),
plugins: [
createInferAssignPlugin({
assignKey: 'assign',
outputKey: 'outputs',
}),
],
}}
/>
);
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { createInferInputsPlugin } from '@flowgram.ai/form-materials';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const InputsValues = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.InputsValues,
}))
);
const InputsValuesTree = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.InputsValuesTree,
}))
);
/**
* Basic usage story - demonstrates automatic schema inference from InputsValues
*/
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
height={500}
formMeta={{
render: () => (
<>
<FormHeader />
<div>
<div>
<h4>Headers</h4>
<Field<Record<string, any> | undefined>
name="headersValues"
defaultValue={{
'Content-Type': {
type: 'constant',
content: 'application/json',
schema: { type: 'string' },
},
Authorization: {
type: 'ref',
content: ['start_0', 'str'],
},
}}
>
{({ field }) => (
<InputsValues value={field.value} onChange={(value) => field.onChange(value)} />
)}
</Field>
</div>
<div>
<h4>Body</h4>
<Field<Record<string, any> | undefined>
name="bodyValues"
defaultValue={{
page: {
index: {
type: 'ref',
content: ['start_0', 'obj', 'obj2', 'num'],
},
size: {
type: 'constant',
content: 10,
schema: { type: 'number' },
},
},
query: {
type: 'ref',
content: ['start_0', 'obj'],
},
}}
>
{({ field }) => (
<InputsValuesTree
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</div>
</div>
</>
),
plugins: [
createInferInputsPlugin({
sourceKey: 'headersValues',
targetKey: 'headersSchema',
}),
createInferInputsPlugin({
sourceKey: 'bodyValues',
targetKey: 'bodySchema',
}),
],
}}
/>
);
@@ -0,0 +1,135 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { validateFlowValue } from '@flowgram.ai/form-materials';
import { Button } from '@douyinfe/semi-ui';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const DynamicValueInput = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.DynamicValueInput,
}))
);
const PromptEditorWithVariables = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.PromptEditorWithVariables,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
transformInitialNode={{
custom_0: (node) => {
node.data.dynamic_value_input = {
type: 'ref',
content: ['start_0', 'unknown_key'],
};
node.data.required_dynamic_value_input = {
type: 'constant',
content: '',
};
node.data.prompt_editor = {
type: 'template',
content: 'Hello {{start_0.unknown_key}}',
};
return node;
},
}}
formMeta={{
validate: {
dynamic_value_input: ({ value, context }) =>
validateFlowValue(value, {
node: context.node,
errorMessages: {
required: 'Value is required',
unknownVariable: 'Unknown Variable',
},
}),
required_dynamic_value_input: ({ value, context }) =>
validateFlowValue(value, {
node: context.node,
required: true,
errorMessages: {
required: 'Value is required',
unknownVariable: 'Unknown Variable',
},
}),
prompt_editor: ({ value, context }) =>
validateFlowValue(value, {
node: context.node,
required: true,
errorMessages: {
required: 'Prompt is required',
unknownVariable: 'Unknown Variable In Template',
},
}),
},
render: ({ form }) => (
<>
<FormHeader />
<b>Validate variable valid</b>
<Field<any> name="dynamic_value_input">
{({ field, fieldState }) => (
<>
<DynamicValueInput
value={field.value}
onChange={(value) => field.onChange(value)}
/>
<span style={{ color: 'red' }}>
{fieldState.errors?.map((e) => e.message).join('\n')}
</span>
</>
)}
</Field>
<br />
<b>Validate required value</b>
<Field<any> name="required_dynamic_value_input">
{({ field, fieldState }) => (
<>
<DynamicValueInput
value={field.value}
onChange={(value) => field.onChange(value)}
/>
<span style={{ color: 'red' }}>
{fieldState.errors?.map((e) => e.message).join('\n')}
</span>
</>
)}
</Field>
<br />
<b>Validate required and variables valid in prompt</b>
<Field<any> name="prompt_editor">
{({ field, fieldState }) => (
<>
<PromptEditorWithVariables
value={field.value}
onChange={(value) => field.onChange(value)}
/>
<span style={{ color: 'red' }}>
{fieldState.errors?.map((e) => e.message).join('\n')}
</span>
</>
)}
</Field>
<br />
<Button onClick={() => form.validate()}>Trigger Validate</Button>
</>
),
}}
/>
);
@@ -0,0 +1,10 @@
import '@flowgram.ai/free-layout-editor/index.css';
import { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';
const FlowGramApp = () => (
<FreeLayoutEditorProvider>
<EditorRenderer />
</FreeLayoutEditorProvider>
);
export default FlowGramApp;
@@ -0,0 +1,65 @@
import '@flowgram.ai/free-layout-editor/index.css';
import {
FreeLayoutEditorProvider,
EditorRenderer,
useNodeRender,
WorkflowNodeProps,
WorkflowNodeRenderer,
} from '@flowgram.ai/free-layout-editor';
const NodeRender = (props: WorkflowNodeProps) => {
const { form, selected } = useNodeRender();
return (
<WorkflowNodeRenderer
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: selected ? '#4e40e5' : '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',
}}
node={props.node}
>
{form?.render()}
</WorkflowNodeRenderer>
);
};
const FlowGramApp = () => (
<FreeLayoutEditorProvider
materials={{
renderDefaultNode: NodeRender,
}}
nodeRegistries={[
{
type: 'custom',
},
]}
initialData={{
nodes: [
{
id: '1',
type: 'custom',
meta: {
position: { x: 250, y: 100 },
},
},
],
edges: [],
}}
>
<EditorRenderer />
</FreeLayoutEditorProvider>
);
export default FlowGramApp;
@@ -0,0 +1,82 @@
import '@flowgram.ai/free-layout-editor/index.css';
import {
FreeLayoutEditorProvider,
EditorRenderer,
useNodeRender,
WorkflowNodeProps,
WorkflowNodeRenderer,
} from '@flowgram.ai/free-layout-editor';
const NodeRender = (props: WorkflowNodeProps) => {
const { form, selected } = useNodeRender();
return (
<WorkflowNodeRenderer
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: selected ? '#4e40e5' : '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',
}}
node={props.node}
>
{form?.render()}
</WorkflowNodeRenderer>
);
};
const FlowGramApp = () => (
<FreeLayoutEditorProvider
onAllLayersRendered={(ctx) => {
ctx.tools.fitView(false);
}}
materials={{
renderDefaultNode: NodeRender,
}}
nodeRegistries={[
{
type: 'custom',
},
]}
canDeleteNode={() => true}
canDeleteLine={() => true}
initialData={{
nodes: [
{
id: '1',
type: 'custom',
meta: {
position: { x: 0, y: 0 },
},
},
{
id: '2',
type: 'custom',
meta: {
position: { x: 400, y: 0 },
},
},
],
edges: [
{
sourceNodeID: '1',
targetNodeID: '2',
},
],
}}
>
<EditorRenderer />
</FreeLayoutEditorProvider>
);
export default FlowGramApp;
@@ -0,0 +1,126 @@
import '@flowgram.ai/free-layout-editor/index.css';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';
import {
FreeLayoutEditorProvider,
EditorRenderer,
useNodeRender,
WorkflowNodeProps,
WorkflowNodeRenderer,
} from '@flowgram.ai/free-layout-editor';
const NodeRender = (props: WorkflowNodeProps) => {
const { form, selected } = useNodeRender();
return (
<WorkflowNodeRenderer
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: selected ? '#4e40e5' : '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',
}}
node={props.node}
>
{form?.render()}
</WorkflowNodeRenderer>
);
};
const FlowGramApp = () => (
<FreeLayoutEditorProvider
plugins={() => [createMinimapPlugin({}), createFreeSnapPlugin({})]}
onAllLayersRendered={(ctx) => {
ctx.tools.fitView(false);
}}
materials={{
renderDefaultNode: NodeRender,
}}
nodeRegistries={[
{
type: 'custom',
},
]}
canDeleteNode={() => true}
canDeleteLine={() => true}
initialData={{
nodes: [
{
id: '1',
type: 'custom',
meta: {
position: { x: 0, y: 0 },
},
},
{
id: '2',
type: 'custom',
meta: {
position: { x: 400, y: -200 },
},
},
{
id: '3',
type: 'custom',
meta: {
position: { x: 400, y: 0 },
},
},
{
id: '4',
type: 'custom',
meta: {
position: { x: 400, y: 200 },
},
},
{
id: '5',
type: 'custom',
meta: {
position: { x: 800, y: 0 },
},
},
],
edges: [
{
sourceNodeID: '1',
targetNodeID: '2',
},
{
sourceNodeID: '1',
targetNodeID: '3',
},
{
sourceNodeID: '1',
targetNodeID: '4',
},
{
sourceNodeID: '2',
targetNodeID: '5',
},
{
sourceNodeID: '3',
targetNodeID: '5',
},
{
sourceNodeID: '4',
targetNodeID: '5',
},
],
}}
>
<EditorRenderer />
</FreeLayoutEditorProvider>
);
export default FlowGramApp;
@@ -0,0 +1,16 @@
import '@flowgram.ai/free-layout-editor/index.css';
import { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';
import { useEditorProps } from './use-editor-props';
const FlowGramApp = () => {
const editorProps = useEditorProps();
return (
<FreeLayoutEditorProvider {...editorProps}>
<EditorRenderer />
</FreeLayoutEditorProvider>
);
};
export default FlowGramApp;
@@ -0,0 +1,67 @@
import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
export const initialData: WorkflowJSON = {
nodes: [
{
id: '1',
type: 'custom',
meta: {
position: { x: 0, y: 0 },
},
},
{
id: '2',
type: 'custom',
meta: {
position: { x: 400, y: -200 },
},
},
{
id: '3',
type: 'custom',
meta: {
position: { x: 400, y: 0 },
},
},
{
id: '4',
type: 'custom',
meta: {
position: { x: 400, y: 200 },
},
},
{
id: '5',
type: 'custom',
meta: {
position: { x: 800, y: 0 },
},
},
],
edges: [
{
sourceNodeID: '1',
targetNodeID: '2',
},
{
sourceNodeID: '1',
targetNodeID: '3',
},
{
sourceNodeID: '1',
targetNodeID: '4',
},
{
sourceNodeID: '2',
targetNodeID: '5',
},
{
sourceNodeID: '3',
targetNodeID: '5',
},
{
sourceNodeID: '4',
targetNodeID: '5',
},
],
};
@@ -0,0 +1,11 @@
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
/**
* You can customize your own node registry
* 你可以自定义节点的注册器
*/
export const nodeRegistries: WorkflowNodeRegistry[] = [
{
type: 'custom',
},
];
@@ -0,0 +1,34 @@
import '@flowgram.ai/free-layout-editor/index.css';
import {
useNodeRender,
WorkflowNodeProps,
WorkflowNodeRenderer,
} from '@flowgram.ai/free-layout-editor';
export const NodeRender = (props: WorkflowNodeProps) => {
const { form, selected } = useNodeRender();
return (
<WorkflowNodeRenderer
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: selected ? '#4e40e5' : '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',
}}
node={props.node}
>
{form?.render()}
</WorkflowNodeRenderer>
);
};
@@ -0,0 +1,27 @@
import { useMemo } from 'react';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';
import { FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
import { NodeRender } from './node-render';
import { nodeRegistries } from './node-registries';
import { initialData } from './initial-data';
export const useEditorProps = () =>
useMemo<FreeLayoutProps>(
() => ({
plugins: () => [createMinimapPlugin({}), createFreeSnapPlugin({})],
onAllLayersRendered: (ctx) => {
ctx.tools.fitView(false);
},
materials: {
renderDefaultNode: NodeRender,
},
nodeRegistries,
canDeleteNode: () => true,
canDeleteLine: () => true,
initialData,
}),
[]
);
@@ -0,0 +1,16 @@
import '@flowgram.ai/free-layout-editor/index.css';
import { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';
import { useEditorProps } from './use-editor-props';
const FlowGramApp = () => {
const editorProps = useEditorProps();
return (
<FreeLayoutEditorProvider {...editorProps}>
<EditorRenderer />
</FreeLayoutEditorProvider>
);
};
export default FlowGramApp;
@@ -0,0 +1,82 @@
import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
export const initialData: WorkflowJSON = {
nodes: [
{
id: '1',
type: 'start',
meta: {
position: { x: 0, y: 0 },
},
data: {
title: 'Start Node',
},
},
{
id: '2',
type: 'custom',
meta: {
position: { x: 400, y: -200 },
},
data: {
title: 'Custom Node A',
},
},
{
id: '3',
type: 'custom',
meta: {
position: { x: 400, y: 0 },
},
data: {
title: 'Custom Node B',
},
},
{
id: '4',
type: 'custom',
meta: {
position: { x: 400, y: 200 },
},
data: {
title: 'Custom Node C',
},
},
{
id: '5',
type: 'end',
meta: {
position: { x: 800, y: 0 },
},
data: {
title: 'End Node',
},
},
],
edges: [
{
sourceNodeID: '1',
targetNodeID: '2',
},
{
sourceNodeID: '1',
targetNodeID: '3',
},
{
sourceNodeID: '1',
targetNodeID: '4',
},
{
sourceNodeID: '2',
targetNodeID: '5',
},
{
sourceNodeID: '3',
targetNodeID: '5',
},
{
sourceNodeID: '4',
targetNodeID: '5',
},
],
};
@@ -0,0 +1,30 @@
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
/**
* You can customize your own node registry
* 你可以自定义节点的注册器
*/
export const nodeRegistries: WorkflowNodeRegistry[] = [
{
type: 'start',
meta: {
isStart: true, // Mark as start
deleteDisable: true, // The start node cannot be deleted
copyDisable: true, // The start node cannot be copied
defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
},
},
{
type: 'end',
meta: {
deleteDisable: true,
copyDisable: true,
defaultPorts: [{ type: 'input' }],
},
},
{
type: 'custom',
meta: {},
defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports
},
];
@@ -0,0 +1,34 @@
import '@flowgram.ai/free-layout-editor/index.css';
import {
useNodeRender,
WorkflowNodeProps,
WorkflowNodeRenderer,
} from '@flowgram.ai/free-layout-editor';
export const NodeRender = (props: WorkflowNodeProps) => {
const { form, selected } = useNodeRender();
return (
<WorkflowNodeRenderer
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: selected ? '#4e40e5' : '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',
}}
node={props.node}
>
{form?.render()}
</WorkflowNodeRenderer>
);
};
@@ -0,0 +1,58 @@
import { useMemo } from 'react';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';
import { Field, FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
import { NodeRender } from './node-render';
import { nodeRegistries } from './node-registries';
import { initialData } from './initial-data';
export const useEditorProps = () =>
useMemo<FreeLayoutProps>(
() => ({
plugins: () => [createMinimapPlugin({}), createFreeSnapPlugin({})],
onAllLayersRendered: (ctx) => {
ctx.tools.fitView(false);
},
materials: {
renderDefaultNode: NodeRender,
},
nodeRegistries,
canDeleteNode: () => true,
canDeleteLine: () => true,
initialData,
/**
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
*/
nodeEngine: {
enable: true,
},
/**
* Redo/Undo enable
*/
history: {
enable: true,
enableChangeNode: true, // Listen Node engine data change
},
getNodeDefaultRegistry(type) {
return {
type,
meta: {
defaultExpanded: true,
},
formMeta: {
/**
* Render form
*/
render: () => (
<>
<Field<string> name="title">{({ field }) => <div>{field.value}</div>}</Field>
</>
),
},
};
},
}),
[]
);
@@ -0,0 +1,43 @@
import {
useService,
WorkflowDocument,
WorkflowNodeEntity,
WorkflowSelectService,
} from '@flowgram.ai/free-layout-editor';
export const AddNode = () => {
const workflowDocument = useService(WorkflowDocument);
const selectService = useService(WorkflowSelectService);
return (
<div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 8, display: 'flex', gap: 8 }}>
<button
style={{
border: '1px solid #e0e0e0',
borderRadius: '50%',
cursor: 'pointer',
padding: '4px',
color: '#ffffff',
background: '#7e72e8',
width: 70,
height: 70,
fontSize: 14,
}}
onClick={() => {
const node: WorkflowNodeEntity = workflowDocument.createWorkflowNodeByType(
'custom',
undefined, // position undefined means create node in center of canvas - position undefined 可以在画布中间创建节点
{
data: {
title: 'New Node',
},
}
);
selectService.selectNode(node);
}}
>
+ Node
</button>
</div>
);
};
@@ -0,0 +1,22 @@
import '@flowgram.ai/free-layout-editor/index.css';
import { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';
import { useEditorProps } from './use-editor-props';
import { Tools } from './tools';
import { Minimap } from './minimap';
import { AddNode } from './add-node';
const FlowGramApp = () => {
const editorProps = useEditorProps();
return (
<FreeLayoutEditorProvider {...editorProps}>
<EditorRenderer />
<Tools />
<Minimap />
<AddNode />
</FreeLayoutEditorProvider>
);
};
export default FlowGramApp;
@@ -0,0 +1,82 @@
import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
export const initialData: WorkflowJSON = {
nodes: [
{
id: '1',
type: 'start',
meta: {
position: { x: 0, y: 0 },
},
data: {
title: 'Start Node',
},
},
{
id: '2',
type: 'custom',
meta: {
position: { x: 400, y: -200 },
},
data: {
title: 'Custom Node A',
},
},
{
id: '3',
type: 'custom',
meta: {
position: { x: 400, y: 0 },
},
data: {
title: 'Custom Node B',
},
},
{
id: '4',
type: 'custom',
meta: {
position: { x: 400, y: 200 },
},
data: {
title: 'Custom Node C',
},
},
{
id: '5',
type: 'end',
meta: {
position: { x: 800, y: 0 },
},
data: {
title: 'End Node',
},
},
],
edges: [
{
sourceNodeID: '1',
targetNodeID: '2',
},
{
sourceNodeID: '1',
targetNodeID: '3',
},
{
sourceNodeID: '1',
targetNodeID: '4',
},
{
sourceNodeID: '2',
targetNodeID: '5',
},
{
sourceNodeID: '3',
targetNodeID: '5',
},
{
sourceNodeID: '4',
targetNodeID: '5',
},
],
};
@@ -0,0 +1,30 @@
import { MinimapRender } from '@flowgram.ai/minimap-plugin';
export const Minimap = () => (
<div
style={{
position: 'absolute',
right: 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,30 @@
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
/**
* You can customize your own node registry
* 你可以自定义节点的注册器
*/
export const nodeRegistries: WorkflowNodeRegistry[] = [
{
type: 'start',
meta: {
isStart: true, // Mark as start
deleteDisable: true, // The start node cannot be deleted
copyDisable: true, // The start node cannot be copied
defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
},
},
{
type: 'end',
meta: {
deleteDisable: true,
copyDisable: true,
defaultPorts: [{ type: 'input' }],
},
},
{
type: 'custom',
meta: {},
defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports
},
];
@@ -0,0 +1,34 @@
import '@flowgram.ai/free-layout-editor/index.css';
import {
useNodeRender,
WorkflowNodeProps,
WorkflowNodeRenderer,
} from '@flowgram.ai/free-layout-editor';
export const NodeRender = (props: WorkflowNodeProps) => {
const { form, selected } = useNodeRender();
return (
<WorkflowNodeRenderer
style={{
width: 280,
minHeight: 88,
height: 'auto',
background: '#fff',
border: '1px solid rgba(6, 7, 9, 0.15)',
borderColor: selected ? '#4e40e5' : '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',
}}
node={props.node}
>
{form?.render()}
</WorkflowNodeRenderer>
);
};
@@ -0,0 +1,90 @@
import { CSSProperties, useEffect, useState } from 'react';
import { usePlaygroundTools, useClientContext, LineType } from '@flowgram.ai/free-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, right: 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.autoLayout()}>
AutoLayout
</button>
<button
style={buttonStyle}
onClick={() =>
tools.switchLineType(
tools.lineType === LineType.BEZIER ? LineType.LINE_CHART : LineType.BEZIER
)
}
>
{tools.lineType === LineType.BEZIER ? 'Bezier' : 'Fold'}
</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,68 @@
import { useMemo } from 'react';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';
import { Field, FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
import { NodeRender } from './node-render';
import { nodeRegistries } from './node-registries';
import { initialData } from './initial-data';
export const useEditorProps = () =>
useMemo<FreeLayoutProps>(
() => ({
plugins: () => [
createMinimapPlugin({
disableLayer: true,
canvasStyle: {
canvasWidth: 100,
canvasHeight: 50,
canvasPadding: 50,
},
}),
createFreeSnapPlugin({}),
],
onAllLayersRendered: (ctx) => {
ctx.tools.fitView(false);
},
materials: {
renderDefaultNode: NodeRender,
},
nodeRegistries,
canDeleteNode: () => true,
canDeleteLine: () => true,
initialData,
/**
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
*/
nodeEngine: {
enable: true,
},
/**
* Redo/Undo enable
*/
history: {
enable: true,
enableChangeNode: true, // Listen Node engine data change
},
getNodeDefaultRegistry(type) {
return {
type,
meta: {
defaultExpanded: true,
},
formMeta: {
/**
* Render form
*/
render: () => (
<>
<Field<string> name="title">{({ field }) => <div>{field.value}</div>}</Field>
</>
),
},
};
},
}),
[]
);
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.doc-free-feature-overview {
position: relative;
z-index: 1;
.demo-container {
position: relative;
width: 100%;
height: 600px;
}
.demo-free-layout-tools {
position: relative;
bottom: 40px;
color: black;
}
}
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import './index.less';
// https://github.com/web-infra-dev/rspress/issues/553
const FreeFeatureOverview = React.lazy(() =>
import('@flowgram.ai/demo-free-layout').then((module) => ({
default: module.DemoFreeLayout,
}))
);
export { FreeFeatureOverview };
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
const OriginFreeFormMetaStoryBuilder = React.lazy(() =>
import('@flowgram.ai/demo-materials').then((module) => ({
default: module.FreeFormMetaStoryBuilder,
}))
);
const FormHeader = React.lazy(() =>
import('@flowgram.ai/demo-materials').then((module) => ({
default: module.FormHeader,
}))
);
const FreeFormMetaStoryBuilder = (
props: React.ComponentPropsWithoutRef<typeof OriginFreeFormMetaStoryBuilder> & {
height?: number | string;
}
) => (
<div style={{ position: 'relative', height: props.height || 400 }}>
<OriginFreeFormMetaStoryBuilder {...props} />
</div>
);
export { FreeFormMetaStoryBuilder, FormHeader };
@@ -0,0 +1,9 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.demo-free-node {
min-height: unset !important;
min-width: unset !important;
}
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import './index.less';
// https://github.com/web-infra-dev/rspress/issues/553
const FreeLayoutSimple = React.lazy(() =>
import('@flowgram.ai/demo-free-layout-simple').then((module) => ({
default: module.DemoFreeLayout,
}))
);
export { FreeLayoutSimple };
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
/* eslint-disable import/no-unresolved */
import nodesCode from '@flowgram.ai/demo-free-layout-simple/src/nodes/index.ts?raw';
import dataCode from '@flowgram.ai/demo-free-layout-simple/src/initial-data.ts?raw';
import useEditorPropsCode from '@flowgram.ai/demo-free-layout-simple/src/hooks/use-editor-props.tsx?raw';
import editorCode from '@flowgram.ai/demo-free-layout-simple/src/editor.tsx?raw';
import toolsCode from '@flowgram.ai/demo-free-layout-simple/src/components/tools.tsx?raw';
import nodeAddPanelCode from '@flowgram.ai/demo-free-layout-simple/src/components/node-add-panel.tsx?raw';
import minimapCode from '@flowgram.ai/demo-free-layout-simple/src/components/minimap.tsx?raw';
import { PreviewEditor } from '../preview-editor';
import { FreeLayoutSimple } from '.';
export const FreeLayoutSimplePreview = () => {
const files = {
'editor.tsx': {
active: true,
code: editorCode,
},
'use-editor-props.tsx': useEditorPropsCode,
'initial-data.ts': dataCode,
'nodes/index.ts': nodesCode,
'node-add-panel.tsx': nodeAddPanelCode,
'tools.tsx': toolsCode,
'minimap.tsx': minimapCode,
};
return (
<div
style={{
zIndex: 1,
position: 'relative',
}}
>
<PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}>
<FreeLayoutSimple />
</PreviewEditor>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More