This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Local
|
||||
.DS_Store
|
||||
*.local
|
||||
*.log*
|
||||
|
||||
# Dist
|
||||
node_modules
|
||||
dist/
|
||||
doc_build/
|
||||
|
||||
# IDE
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
|
||||
# 自动构建生成的 API
|
||||
auto-docs
|
||||
@@ -0,0 +1,30 @@
|
||||
# FlowGram.AI WebSite
|
||||
|
||||
## Setup
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
rush update
|
||||
```
|
||||
|
||||
## Get Started
|
||||
|
||||
Start the dev server:
|
||||
|
||||
```bash
|
||||
cd apps/docs
|
||||
rushx dev
|
||||
```
|
||||
|
||||
Build the website for production:
|
||||
|
||||
```bash
|
||||
rushx build
|
||||
```
|
||||
|
||||
Preview the production build locally:
|
||||
|
||||
```bash
|
||||
rushx preview
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user