新版本流程编辑器
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormRenderProps, FormMeta } from '@flowgram.ai/free-layout-editor';
|
||||
import { Avatar } from '@douyinfe/semi-ui';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import iconEnd from '../../assets/icon-end.jpg';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: 60,
|
||||
height: 60,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
shape="circle"
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
cursor: 'move',
|
||||
}}
|
||||
alt="Icon"
|
||||
src={iconEnd}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
render: renderForm,
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconStart from '../../assets/icon-start.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
export const BlockEndNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.BlockEnd,
|
||||
meta: {
|
||||
isNodeEnd: true,
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
sidebarDisabled: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
size: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
wrapperStyle: {
|
||||
minWidth: 'unset',
|
||||
width: '100%',
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
cursor: 'move',
|
||||
},
|
||||
},
|
||||
info: {
|
||||
icon: iconStart,
|
||||
description: 'The final node of the block.',
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
/**
|
||||
* Start Node cannot be added
|
||||
*/
|
||||
canAdd() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormRenderProps, FormMeta } from '@flowgram.ai/free-layout-editor';
|
||||
import { Avatar } from '@douyinfe/semi-ui';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import iconStart from '../../assets/icon-start.jpg';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: 60,
|
||||
height: 60,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
shape="circle"
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
cursor: 'move',
|
||||
}}
|
||||
alt="Icon"
|
||||
src={iconStart}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
render: renderForm,
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconStart from '../../assets/icon-start.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
export const BlockStartNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.BlockStart,
|
||||
meta: {
|
||||
isStart: true,
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
sidebarDisabled: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [{ type: 'output' }],
|
||||
size: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
wrapperStyle: {
|
||||
minWidth: 'unset',
|
||||
width: '100%',
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
cursor: 'move',
|
||||
},
|
||||
},
|
||||
info: {
|
||||
icon: iconStart,
|
||||
description: 'The starting node of the block.',
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
/**
|
||||
* Start Node cannot be added
|
||||
*/
|
||||
canAdd() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormMeta } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { useIsSidebar } from '../../hooks';
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
|
||||
export const renderForm = () => {
|
||||
const isSidebar = useIsSidebar();
|
||||
if (isSidebar) {
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const formMeta: FormMeta = {
|
||||
...defaultFormMeta,
|
||||
render: renderForm,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconBreak from '../../assets/icon-break.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
let index = 0;
|
||||
export const BreakNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Break,
|
||||
meta: {
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
sidebarDisabled: true,
|
||||
size: {
|
||||
width: 360,
|
||||
height: 54,
|
||||
},
|
||||
expandable: false,
|
||||
onlyInContainer: WorkflowNodeType.Loop,
|
||||
},
|
||||
info: {
|
||||
icon: iconBreak,
|
||||
description:
|
||||
'The final node of the workflow, used to return the result information after the workflow is run.',
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
onAdd() {
|
||||
return {
|
||||
id: `break_${nanoid(5)}`,
|
||||
type: 'break',
|
||||
data: {
|
||||
title: `Break_${++index}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { TypeScriptCodeEditor } from '@flowgram.ai/form-materials';
|
||||
import { Divider } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../../hooks';
|
||||
|
||||
export function Code() {
|
||||
const isSidebar = useIsSidebar();
|
||||
const { readonly } = useNodeRenderContext();
|
||||
|
||||
if (!isSidebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<Field<string> name="script.content">
|
||||
{({ field }) => (
|
||||
<TypeScriptCodeEditor
|
||||
value={field.value}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { DisplayInputsValues, IFlowValue, InputsValues } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../../hooks';
|
||||
import { FormItem } from '../../../form-components';
|
||||
|
||||
export function Inputs() {
|
||||
const isSidebar = useIsSidebar();
|
||||
|
||||
const { readonly } = useNodeRenderContext();
|
||||
|
||||
if (!isSidebar) {
|
||||
return (
|
||||
<Field<Record<string, IFlowValue | undefined> | undefined> name="inputsValues">
|
||||
{({ field }) => <DisplayInputsValues value={field.value} />}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem name="inputs" type="object" vertical>
|
||||
<Field<Record<string, IFlowValue | undefined> | undefined> name="inputsValues">
|
||||
{({ field }) => (
|
||||
<InputsValues
|
||||
value={field.value}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { DisplayOutputs, IJsonSchema, JsonSchemaEditor } from '@flowgram.ai/form-materials';
|
||||
import { Divider } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../../hooks';
|
||||
import { FormItem } from '../../../form-components';
|
||||
|
||||
export function Outputs() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
const isSidebar = useIsSidebar();
|
||||
|
||||
if (!isSidebar) {
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<Field<IJsonSchema> name="outputs">
|
||||
{({ field }) => <DisplayOutputs value={field.value} />}
|
||||
</Field>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<FormItem name="outputs" type="object" vertical>
|
||||
<Field<IJsonSchema> name="outputs">
|
||||
{({ field }) => (
|
||||
<JsonSchemaEditor
|
||||
readonly={readonly}
|
||||
value={field.value}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';
|
||||
import { createInferInputsPlugin } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
import { CodeNodeJSON } from './types';
|
||||
import { Outputs } from './components/outputs';
|
||||
import { Inputs } from './components/inputs';
|
||||
import { Code } from './components/code';
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
|
||||
export const FormRender = ({ form }: FormRenderProps<CodeNodeJSON>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<Inputs />
|
||||
<Code />
|
||||
<Outputs />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta = {
|
||||
render: (props) => <FormRender {...props} />,
|
||||
effect: defaultFormMeta.effect,
|
||||
validate: defaultFormMeta.validate,
|
||||
plugins: [createInferInputsPlugin({ sourceKey: 'inputsValues', targetKey: 'inputs' })],
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconCode from '../../assets/icon-script.png';
|
||||
import { formMeta } from './form-meta';
|
||||
|
||||
let index = 0;
|
||||
|
||||
const defaultCode = `// 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 CodeNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Code,
|
||||
info: {
|
||||
icon: iconCode,
|
||||
description: 'Run the Script',
|
||||
},
|
||||
meta: {
|
||||
size: {
|
||||
width: 360,
|
||||
height: 390,
|
||||
},
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
id: `code_${nanoid(5)}`,
|
||||
type: 'code',
|
||||
data: {
|
||||
title: `Code_${++index}`,
|
||||
inputsValues: {
|
||||
input: { type: 'constant', content: '' },
|
||||
},
|
||||
script: {
|
||||
language: 'javascript',
|
||||
content: defaultCode,
|
||||
},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key0: {
|
||||
type: 'string',
|
||||
},
|
||||
key1: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
key2: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key21: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
formMeta: formMeta,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeJSON } from '@flowgram.ai/free-layout-editor';
|
||||
import { IFlowValue, IJsonSchema } from '@flowgram.ai/form-materials';
|
||||
|
||||
export interface CodeNodeJSON extends FlowNodeJSON {
|
||||
data: {
|
||||
title: string;
|
||||
inputsValues: Record<string, IFlowValue>;
|
||||
inputs: IJsonSchema<'object'>;
|
||||
outputs: IJsonSchema<'object'>;
|
||||
script: {
|
||||
language: 'javascript';
|
||||
content: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
|
||||
export const CommentNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Comment,
|
||||
meta: {
|
||||
sidebarDisabled: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [],
|
||||
renderKey: WorkflowNodeType.Comment,
|
||||
size: {
|
||||
width: 240,
|
||||
height: 150,
|
||||
},
|
||||
},
|
||||
formMeta: {
|
||||
render: () => <></>,
|
||||
},
|
||||
getInputPoints: () => [], // Comment 节点没有输入
|
||||
getOutputPoints: () => [], // Comment 节点没有输出
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Field, FieldArray, I18n } from '@flowgram.ai/free-layout-editor';
|
||||
import { ConditionRow, ConditionRowValueType } from '@flowgram.ai/form-materials';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { IconPlus, IconCrossCircleStroked } from '@douyinfe/semi-icons';
|
||||
|
||||
import { useNodeRenderContext } from '../../../hooks';
|
||||
import { FormItem } from '../../../form-components';
|
||||
import { Feedback } from '../../../form-components';
|
||||
import { ConditionPort } from './styles';
|
||||
|
||||
interface ConditionValue {
|
||||
key: string;
|
||||
value?: ConditionRowValueType;
|
||||
}
|
||||
|
||||
export function ConditionInputs() {
|
||||
const { node, readonly } = useNodeRenderContext();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
node.ports.updateDynamicPorts();
|
||||
});
|
||||
}, [node]);
|
||||
|
||||
return (
|
||||
<FieldArray name="conditions">
|
||||
{({ field }) => (
|
||||
<>
|
||||
{field.map((child, index) => (
|
||||
<Field<ConditionValue> key={child.name} name={child.name}>
|
||||
{({ field: childField, fieldState: childState }) => (
|
||||
<FormItem name="if" type="boolean" required={true} labelWidth={50}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ConditionRow
|
||||
readonly={readonly}
|
||||
style={{ flexGrow: 1, overflow: 'hidden' }}
|
||||
value={childField.value.value}
|
||||
onChange={(v) => childField.onChange({ value: v, key: childField.value.key })}
|
||||
/>
|
||||
|
||||
{!readonly && (
|
||||
<Button
|
||||
theme="borderless"
|
||||
disabled={readonly}
|
||||
icon={<IconCrossCircleStroked />}
|
||||
onClick={() => field.delete(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Feedback errors={childState?.errors} invalid={childState?.invalid} />
|
||||
<ConditionPort data-port-id={childField.value.key} data-port-type="output" />
|
||||
</FormItem>
|
||||
)}
|
||||
</Field>
|
||||
))}
|
||||
<FormItem name="else" type="boolean" required={true} labelWidth={100}>
|
||||
<ConditionPort data-port-id="else" data-port-type="output" />
|
||||
</FormItem>
|
||||
{!readonly && (
|
||||
<div>
|
||||
<Button
|
||||
theme="borderless"
|
||||
icon={<IconPlus />}
|
||||
onClick={() =>
|
||||
field.append({
|
||||
key: `if_${nanoid(6)}`,
|
||||
value: { type: 'expression', content: '' },
|
||||
})
|
||||
}
|
||||
>
|
||||
{I18n.t('Add')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FieldArray>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ConditionPort = styled.div`
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
top: 50%;
|
||||
`;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
|
||||
import { autoRenameRefEffect } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
import { ConditionInputs } from './condition-inputs';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<ConditionInputs />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||
'conditions.*': ({ value }) => {
|
||||
if (!value?.value) return 'Condition is required';
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
effect: {
|
||||
conditions: autoRenameRefEffect,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconCondition from '../../assets/icon-condition.svg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
export const ConditionNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Condition,
|
||||
info: {
|
||||
icon: iconCondition,
|
||||
description:
|
||||
'Connect multiple downstream branches. Only the corresponding branch will be executed if the set conditions are met.',
|
||||
},
|
||||
meta: {
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
// Condition Outputs use dynamic port
|
||||
useDynamicPort: true,
|
||||
expandable: false, // disable expanded
|
||||
size: {
|
||||
width: 360,
|
||||
height: 210,
|
||||
},
|
||||
},
|
||||
formMeta,
|
||||
onAdd() {
|
||||
return {
|
||||
id: `condition_${nanoid(5)}`,
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: 'Condition',
|
||||
conditions: [
|
||||
{
|
||||
key: `if_${nanoid(5)}`,
|
||||
value: {},
|
||||
},
|
||||
{
|
||||
key: `if_${nanoid(5)}`,
|
||||
value: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export enum WorkflowNodeType {
|
||||
Start = 'start',
|
||||
End = 'end',
|
||||
LLM = 'llm',
|
||||
HTTP = 'http',
|
||||
Code = 'code',
|
||||
Variable = 'variable',
|
||||
Condition = 'condition',
|
||||
MultiCondition = 'multi-condition',
|
||||
Loop = 'loop',
|
||||
BlockStart = 'block-start',
|
||||
BlockEnd = 'block-end',
|
||||
Comment = 'comment',
|
||||
Continue = 'continue',
|
||||
Break = 'break',
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormMeta } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { useIsSidebar } from '../../hooks';
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
|
||||
export const renderForm = () => {
|
||||
const isSidebar = useIsSidebar();
|
||||
if (isSidebar) {
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const formMeta: FormMeta = {
|
||||
...defaultFormMeta,
|
||||
render: renderForm,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconContinue from '../../assets/icon-continue.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
let index = 0;
|
||||
export const ContinueNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Continue,
|
||||
meta: {
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
sidebarDisabled: true,
|
||||
size: {
|
||||
width: 360,
|
||||
height: 54,
|
||||
},
|
||||
expandable: false,
|
||||
onlyInContainer: WorkflowNodeType.Loop,
|
||||
},
|
||||
info: {
|
||||
icon: iconContinue,
|
||||
description:
|
||||
'The final node of the workflow, used to return the result information after the workflow is run.',
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
onAdd() {
|
||||
return {
|
||||
id: `continue_${nanoid(5)}`,
|
||||
type: 'continue',
|
||||
data: {
|
||||
title: `Continue_${++index}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
|
||||
import {
|
||||
autoRenameRefEffect,
|
||||
provideJsonSchemaOutputs,
|
||||
syncVariableTitle,
|
||||
DisplayOutputs,
|
||||
validateFlowValue,
|
||||
validateWhenVariableSync,
|
||||
listenRefSchemaChange,
|
||||
} from '@flowgram.ai/form-materials';
|
||||
import { Divider } from '@douyinfe/semi-ui';
|
||||
|
||||
import { FlowNodeJSON } from '../typings';
|
||||
import { FormHeader, FormContent, FormInputs } from '../form-components';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<FormInputs />
|
||||
<Divider />
|
||||
<DisplayOutputs displayFromScope />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const defaultFormMeta: FormMeta<FlowNodeJSON> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
/**
|
||||
* Supported writing as:
|
||||
* 1: validate as options: { title: () => {} , ... }
|
||||
* 2: validate as dynamic function: (values, ctx) => ({ title: () => {}, ... })
|
||||
*/
|
||||
validate: {
|
||||
title: ({ value }) => (value ? undefined : 'Title is required'),
|
||||
'inputsValues.*': ({ value, context, formValues, name }) => {
|
||||
const valuePropertyKey = name.replace(/^inputsValues\./, '');
|
||||
const required = formValues.inputs?.required || [];
|
||||
|
||||
return validateFlowValue(value, {
|
||||
node: context.node,
|
||||
required: required.includes(valuePropertyKey),
|
||||
errorMessages: {
|
||||
required: `${valuePropertyKey} is required`,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Initialize (fromJSON) data transformation
|
||||
* 初始化(fromJSON) 数据转换
|
||||
* @param value
|
||||
* @param ctx
|
||||
*/
|
||||
formatOnInit: (value, ctx) => value,
|
||||
/**
|
||||
* Save (toJSON) data transformation
|
||||
* 保存(toJSON) 数据转换
|
||||
* @param value
|
||||
* @param ctx
|
||||
*/
|
||||
formatOnSubmit: (value, ctx) => value,
|
||||
effect: {
|
||||
title: syncVariableTitle,
|
||||
outputs: provideJsonSchemaOutputs,
|
||||
inputsValues: [...autoRenameRefEffect, ...validateWhenVariableSync({ scope: 'public' })],
|
||||
'inputsValues.*': listenRefSchemaChange((params) => {
|
||||
console.log(`[${params.context.node.id}][${params.name}] Schema Of Ref Updated`);
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field, FormMeta } from '@flowgram.ai/free-layout-editor';
|
||||
import {
|
||||
createInferInputsPlugin,
|
||||
DisplayInputsValues,
|
||||
IFlowValue,
|
||||
InputsValues,
|
||||
} from '@flowgram.ai/form-materials';
|
||||
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { useIsSidebar } from '../../hooks';
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
|
||||
export const renderForm = () => {
|
||||
const isSidebar = useIsSidebar();
|
||||
if (isSidebar) {
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<Field<Record<string, IFlowValue | undefined> | undefined> name="inputsValues">
|
||||
{({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<InputsValues value={value} onChange={(_v) => onChange(_v)} />
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<Field<Record<string, IFlowValue | undefined> | undefined> name="inputsValues">
|
||||
{({ field: { value } }) => (
|
||||
<>
|
||||
<DisplayInputsValues value={value} />
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const formMeta: FormMeta = {
|
||||
...defaultFormMeta,
|
||||
render: renderForm,
|
||||
plugins: [
|
||||
createInferInputsPlugin({
|
||||
sourceKey: 'inputsValues',
|
||||
targetKey: 'inputs',
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconEnd from '../../assets/icon-end.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
export const EndNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.End,
|
||||
meta: {
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
size: {
|
||||
width: 360,
|
||||
height: 211,
|
||||
},
|
||||
},
|
||||
info: {
|
||||
icon: iconEnd,
|
||||
description:
|
||||
'The final node of the workflow, used to return the result information after the workflow is run.',
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
/**
|
||||
* End Node cannot be added
|
||||
*/
|
||||
canAdd() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
import {
|
||||
FlowNodeBaseType,
|
||||
WorkflowNodeEntity,
|
||||
PositionSchema,
|
||||
FlowNodeTransformData,
|
||||
nanoid,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
|
||||
let index = 0;
|
||||
export const GroupNodeRegistry: FlowNodeRegistry = {
|
||||
type: FlowNodeBaseType.GROUP,
|
||||
meta: {
|
||||
renderKey: FlowNodeBaseType.GROUP,
|
||||
defaultPorts: [],
|
||||
isContainer: true,
|
||||
disableSideBar: true,
|
||||
size: {
|
||||
width: 560,
|
||||
height: 400,
|
||||
},
|
||||
padding: () => ({
|
||||
top: 80,
|
||||
bottom: 40,
|
||||
left: 65,
|
||||
right: 65,
|
||||
}),
|
||||
selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {
|
||||
if (!mousePos) {
|
||||
return true;
|
||||
}
|
||||
const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||
return !transform.bounds.contains(mousePos.x, mousePos.y);
|
||||
},
|
||||
expandable: false,
|
||||
/**
|
||||
* It cannot be added through the panel
|
||||
* 不能通过面板添加
|
||||
*/
|
||||
nodePanelVisible: false,
|
||||
},
|
||||
formMeta: {
|
||||
render: () => <></>,
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
type: FlowNodeBaseType.GROUP,
|
||||
id: `group_${nanoid(5)}`,
|
||||
meta: {
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
color: 'Green',
|
||||
title: `Group_${++index}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { IFlowTemplateValue, PromptEditorWithVariables } from '@flowgram.ai/form-materials';
|
||||
import { Select } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useNodeRenderContext } from '../../../hooks';
|
||||
import { FormItem } from '../../../form-components';
|
||||
|
||||
export function Api() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormItem name="API" required vertical type="string">
|
||||
<div style={{ display: 'flex', gap: 5 }}>
|
||||
<Field<string> name="api.method" defaultValue="GET">
|
||||
{({ field }) => (
|
||||
<Select
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value as string);
|
||||
}}
|
||||
style={{ width: 85, maxWidth: 85, minWidth: 85 }}
|
||||
size="small"
|
||||
disabled={readonly}
|
||||
optionList={[
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
{ label: 'PATCH', value: 'PATCH' },
|
||||
{ label: 'HEAD', value: 'HEAD' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field<IFlowTemplateValue> name="api.url">
|
||||
{({ field }) => (
|
||||
<PromptEditorWithVariables
|
||||
disableMarkdownHighlight
|
||||
readonly={readonly}
|
||||
style={{ flexGrow: 1 }}
|
||||
placeholder="Input URL, use var by '{'"
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value!);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import {
|
||||
IFlowTemplateValue,
|
||||
JsonEditorWithVariables,
|
||||
PromptEditorWithVariables,
|
||||
} from '@flowgram.ai/form-materials';
|
||||
import { Select } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useNodeRenderContext } from '../../../hooks';
|
||||
import { FormItem } from '../../../form-components';
|
||||
|
||||
const BODY_TYPE_OPTIONS = [
|
||||
{
|
||||
label: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
label: 'JSON',
|
||||
value: 'JSON',
|
||||
},
|
||||
{
|
||||
label: 'Raw Text',
|
||||
value: 'raw-text',
|
||||
},
|
||||
];
|
||||
|
||||
export function Body() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
|
||||
const renderBodyEditor = (bodyType: string) => {
|
||||
switch (bodyType) {
|
||||
case 'JSON':
|
||||
return (
|
||||
<Field<IFlowTemplateValue> name="body.json">
|
||||
{({ field }) => (
|
||||
<JsonEditorWithVariables
|
||||
value={field.value?.content}
|
||||
readonly={readonly}
|
||||
activeLinePlaceholder="use var by '@'"
|
||||
onChange={(value) => {
|
||||
field.onChange({ type: 'template', content: value });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
case 'raw-text':
|
||||
return (
|
||||
<Field<IFlowTemplateValue> name="body.rawText">
|
||||
{({ field }) => (
|
||||
<PromptEditorWithVariables
|
||||
disableMarkdownHighlight
|
||||
readonly={readonly}
|
||||
style={{ flexGrow: 1 }}
|
||||
placeholder="Input raw text, use var by '{'"
|
||||
onChange={(value) => {
|
||||
field.onChange(value!);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Field<string> name="body.bodyType" defaultValue="JSON">
|
||||
{({ field }) => (
|
||||
<div style={{ marginTop: 5 }}>
|
||||
<FormItem name="Body" vertical type="object">
|
||||
<Select
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value as string);
|
||||
}}
|
||||
style={{ width: '100%', marginBottom: 10 }}
|
||||
disabled={readonly}
|
||||
size="small"
|
||||
optionList={BODY_TYPE_OPTIONS}
|
||||
/>
|
||||
{renderBodyEditor(field.value)}
|
||||
</FormItem>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { DisplayInputsValues, IFlowValue, InputsValues } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../../hooks';
|
||||
import { FormItem } from '../../../form-components';
|
||||
|
||||
export function Headers() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
const isSidebar = useIsSidebar();
|
||||
|
||||
if (!isSidebar) {
|
||||
return (
|
||||
<FormItem name="headers" type="object" vertical>
|
||||
<Field<Record<string, IFlowValue | undefined> | undefined> name="headersValues">
|
||||
{({ field }) => <DisplayInputsValues value={field.value} />}
|
||||
</Field>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem name="headers" type="object" vertical>
|
||||
<Field<Record<string, IFlowValue | undefined> | undefined> name="headersValues">
|
||||
{({ field }) => (
|
||||
<InputsValues
|
||||
value={field.value}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { DisplayInputsValues, IFlowValue, InputsValues } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../../hooks';
|
||||
import { FormItem } from '../../../form-components';
|
||||
|
||||
export function Params() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
const isSidebar = useIsSidebar();
|
||||
|
||||
if (!isSidebar) {
|
||||
return (
|
||||
<FormItem name="params" type="object" vertical>
|
||||
<Field<Record<string, IFlowValue | undefined> | undefined> name="paramsValues">
|
||||
{({ field }) => <DisplayInputsValues value={field.value} />}
|
||||
</Field>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem name="params" type="object" vertical>
|
||||
<Field<Record<string, IFlowValue | undefined> | undefined> name="paramsValues">
|
||||
{({ field }) => (
|
||||
<InputsValues
|
||||
value={field.value}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Field } from '@flowgram.ai/free-layout-editor';
|
||||
import { InputNumber } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useNodeRenderContext } from '../../../hooks';
|
||||
import { FormItem } from '../../../form-components';
|
||||
|
||||
export function Timeout() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormItem name="Timeout(ms)" required style={{ flex: 1 }} type="number">
|
||||
<Field<number> name="timeout.timeout" defaultValue={10000}>
|
||||
{({ field }) => (
|
||||
<InputNumber
|
||||
size="small"
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value as number);
|
||||
}}
|
||||
disabled={readonly}
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
<FormItem name="Retry Times" required type="number">
|
||||
<Field<number> name="timeout.retryTimes" defaultValue={1}>
|
||||
{({ field }) => (
|
||||
<InputNumber
|
||||
size="small"
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value as number);
|
||||
}}
|
||||
disabled={readonly}
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';
|
||||
import { createInferInputsPlugin, DisplayOutputs } from '@flowgram.ai/form-materials';
|
||||
import { Divider } from '@douyinfe/semi-ui';
|
||||
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
import { HTTPNodeJSON } from './types';
|
||||
import { Timeout } from './components/timeout';
|
||||
import { Params } from './components/params';
|
||||
import { Headers } from './components/headers';
|
||||
import { Body } from './components/body';
|
||||
import { Api } from './components/api';
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
|
||||
export const FormRender = ({ form }: FormRenderProps<HTTPNodeJSON>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<Api />
|
||||
<Divider />
|
||||
<Headers />
|
||||
<Divider />
|
||||
<Params />
|
||||
<Divider />
|
||||
<Body />
|
||||
<Divider />
|
||||
<Timeout />
|
||||
<Divider />
|
||||
<DisplayOutputs displayFromScope />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta = {
|
||||
render: (props) => <FormRender {...props} />,
|
||||
effect: defaultFormMeta.effect,
|
||||
plugins: [
|
||||
createInferInputsPlugin({ sourceKey: 'headersValues', targetKey: 'headers' }),
|
||||
createInferInputsPlugin({ sourceKey: 'paramsValues', targetKey: 'params' }),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconHTTP from '../../assets/icon-http.svg';
|
||||
import { formMeta } from './form-meta';
|
||||
|
||||
let index = 0;
|
||||
|
||||
export const HTTPNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.HTTP,
|
||||
info: {
|
||||
icon: iconHTTP,
|
||||
description: 'Call the HTTP API',
|
||||
},
|
||||
meta: {
|
||||
size: {
|
||||
width: 360,
|
||||
height: 390,
|
||||
},
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
id: `http_${nanoid(5)}`,
|
||||
type: 'http',
|
||||
data: {
|
||||
title: `HTTP_${++index}`,
|
||||
api: {
|
||||
method: 'GET',
|
||||
},
|
||||
body: {
|
||||
bodyType: 'JSON',
|
||||
},
|
||||
headers: {},
|
||||
params: {},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
body: { type: 'string' },
|
||||
headers: { type: 'object' },
|
||||
statusCode: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
formMeta: formMeta,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { IFlowConstantRefValue } from '@flowgram.ai/runtime-interface';
|
||||
import { FlowNodeJSON } from '@flowgram.ai/free-layout-editor';
|
||||
import { IFlowTemplateValue, IJsonSchema } from '@flowgram.ai/form-materials';
|
||||
|
||||
export interface HTTPNodeJSON extends FlowNodeJSON {
|
||||
data: {
|
||||
title: string;
|
||||
outputs: IJsonSchema<'object'>;
|
||||
api: {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
|
||||
url: IFlowTemplateValue;
|
||||
};
|
||||
headers: IJsonSchema<'object'>;
|
||||
headersValues: Record<string, IFlowConstantRefValue>;
|
||||
params: IJsonSchema<'object'>;
|
||||
paramsValues: Record<string, IFlowConstantRefValue>;
|
||||
body: {
|
||||
bodyType: 'none' | 'form-data' | 'x-www-form-urlencoded' | 'raw-text' | 'JSON';
|
||||
json?: IFlowTemplateValue;
|
||||
formData?: IJsonSchema<'object'>;
|
||||
formDataValues?: Record<string, IFlowConstantRefValue>;
|
||||
rawText?: IFlowTemplateValue;
|
||||
xWwwFormUrlencoded?: IJsonSchema<'object'>;
|
||||
xWwwFormUrlencodedValues?: Record<string, IFlowConstantRefValue>;
|
||||
};
|
||||
timeout: {
|
||||
retryTimes: number;
|
||||
timeout: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeRegistry } from '../typings';
|
||||
import { VariableNodeRegistry } from './variable';
|
||||
import { StartNodeRegistry } from './start';
|
||||
import { LoopNodeRegistry } from './loop';
|
||||
import { LLMNodeRegistry } from './llm';
|
||||
import { HTTPNodeRegistry } from './http';
|
||||
import { GroupNodeRegistry } from './group';
|
||||
import { EndNodeRegistry } from './end';
|
||||
import { ContinueNodeRegistry } from './continue';
|
||||
import { ConditionNodeRegistry } from './condition';
|
||||
import { CommentNodeRegistry } from './comment';
|
||||
import { CodeNodeRegistry } from './code';
|
||||
import { BreakNodeRegistry } from './break';
|
||||
import { BlockStartNodeRegistry } from './block-start';
|
||||
import { BlockEndNodeRegistry } from './block-end';
|
||||
import { MultiConditionNodeRegistry } from "./multi-condition";
|
||||
export { WorkflowNodeType } from './constants';
|
||||
|
||||
export const nodeRegistries: FlowNodeRegistry[] = [
|
||||
ConditionNodeRegistry,
|
||||
StartNodeRegistry,
|
||||
EndNodeRegistry,
|
||||
LLMNodeRegistry,
|
||||
LoopNodeRegistry,
|
||||
CommentNodeRegistry,
|
||||
BlockStartNodeRegistry,
|
||||
BlockEndNodeRegistry,
|
||||
HTTPNodeRegistry,
|
||||
CodeNodeRegistry,
|
||||
ContinueNodeRegistry,
|
||||
BreakNodeRegistry,
|
||||
VariableNodeRegistry,
|
||||
GroupNodeRegistry,
|
||||
MultiConditionNodeRegistry,
|
||||
];
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconLLM from '../../assets/icon-llm.jpg';
|
||||
|
||||
let index = 0;
|
||||
export const LLMNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.LLM,
|
||||
info: {
|
||||
icon: iconLLM,
|
||||
description:
|
||||
'Call the large language model and use variables and prompt words to generate responses.',
|
||||
},
|
||||
meta: {
|
||||
size: {
|
||||
width: 360,
|
||||
height: 390,
|
||||
},
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
id: `llm_${nanoid(5)}`,
|
||||
type: 'llm',
|
||||
data: {
|
||||
title: `LLM_${++index}`,
|
||||
inputsValues: {
|
||||
modelName: {
|
||||
type: 'constant',
|
||||
content: 'gpt-3.5-turbo',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'constant',
|
||||
content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'constant',
|
||||
content: 'https://mock-ai-url/api/v3',
|
||||
},
|
||||
temperature: {
|
||||
type: 'constant',
|
||||
content: 0.5,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'template',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'template',
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],
|
||||
properties: {
|
||||
modelName: {
|
||||
type: 'string',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
},
|
||||
apiHost: {
|
||||
type: 'string',
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: {
|
||||
formComponent: 'prompt-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';
|
||||
import { SubCanvasRender } from '@flowgram.ai/free-container-plugin';
|
||||
import {
|
||||
BatchOutputs,
|
||||
BatchVariableSelector,
|
||||
createBatchOutputsFormPlugin,
|
||||
DisplayOutputs,
|
||||
IFlowRefValue,
|
||||
provideBatchInputEffect,
|
||||
} from '@flowgram.ai/form-materials';
|
||||
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../hooks';
|
||||
import { FormHeader, FormContent, FormItem, Feedback } from '../../form-components';
|
||||
|
||||
interface LoopNodeJSON extends FlowNodeJSON {
|
||||
data: {
|
||||
loopFor: IFlowRefValue;
|
||||
};
|
||||
}
|
||||
|
||||
export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
|
||||
const isSidebar = useIsSidebar();
|
||||
const { readonly } = useNodeRenderContext();
|
||||
const formHeight = 115;
|
||||
|
||||
const loopFor = (
|
||||
<Field<IFlowRefValue> name={`loopFor`}>
|
||||
{({ field, fieldState }) => (
|
||||
<FormItem name={'loopFor'} type={'array'} required>
|
||||
<BatchVariableSelector
|
||||
style={{ width: '100%' }}
|
||||
value={field.value?.content}
|
||||
onChange={(val) => field.onChange({ type: 'ref', content: val })}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
/>
|
||||
<Feedback errors={fieldState?.errors} />
|
||||
</FormItem>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
const loopOutputs = (
|
||||
<Field<Record<string, IFlowRefValue | undefined> | undefined> name={`loopOutputs`}>
|
||||
{({ field, fieldState }) => (
|
||||
<FormItem name="loopOutputs" type="object" vertical>
|
||||
<BatchOutputs
|
||||
style={{ width: '100%' }}
|
||||
value={field.value}
|
||||
onChange={(val) => field.onChange(val)}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
/>
|
||||
<Feedback errors={fieldState?.errors} />
|
||||
</FormItem>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
if (isSidebar) {
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
{loopFor}
|
||||
{loopOutputs}
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
{loopFor}
|
||||
<SubCanvasRender offsetY={-formHeight} />
|
||||
<DisplayOutputs displayFromScope />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const formMeta: FormMeta = {
|
||||
...defaultFormMeta,
|
||||
render: LoopFormRender,
|
||||
effect: {
|
||||
loopFor: provideBatchInputEffect,
|
||||
},
|
||||
plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' })],
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
WorkflowNodeEntity,
|
||||
PositionSchema,
|
||||
FlowNodeTransformData,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconLoop from '../../assets/icon-loop.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
let index = 0;
|
||||
export const LoopNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Loop,
|
||||
info: {
|
||||
icon: iconLoop,
|
||||
description:
|
||||
'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',
|
||||
},
|
||||
meta: {
|
||||
/**
|
||||
* Mark as subcanvas
|
||||
* 子画布标记
|
||||
*/
|
||||
isContainer: true,
|
||||
/**
|
||||
* The subcanvas default size setting
|
||||
* 子画布默认大小设置
|
||||
*/
|
||||
size: {
|
||||
width: 424,
|
||||
height: 244,
|
||||
},
|
||||
// autoResizeDisable: true,
|
||||
/**
|
||||
* The subcanvas padding setting
|
||||
* 子画布 padding 设置
|
||||
*/
|
||||
padding: (transform) => {
|
||||
if (!transform.isContainer) {
|
||||
return {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
top: 120,
|
||||
bottom: 80,
|
||||
left: 80,
|
||||
right: 80,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Controls the node selection status within the subcanvas
|
||||
* 控制子画布内的节点选中状态
|
||||
*/
|
||||
selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {
|
||||
if (!mousePos) {
|
||||
return true;
|
||||
}
|
||||
const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||
// 鼠标开始时所在位置不包括当前节点时才可选中
|
||||
return !transform.bounds.contains(mousePos.x, mousePos.y);
|
||||
},
|
||||
// expandable: false, // disable expanded
|
||||
wrapperStyle: {
|
||||
minWidth: 'unset',
|
||||
width: '100%',
|
||||
},
|
||||
// defaultPorts: [{ type: 'output', location: 'right' }, { type: 'input', location: 'left'}, { type: 'output', location: 'bottom', portID: 'bottom' }, { type: 'input', location: 'top', portID: 'top'}]
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
id: `loop_${nanoid(5)}`,
|
||||
type: WorkflowNodeType.Loop,
|
||||
data: {
|
||||
title: `Loop_${++index}`,
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: `block_start_${nanoid(5)}`,
|
||||
type: WorkflowNodeType.BlockStart,
|
||||
meta: {
|
||||
position: {
|
||||
x: 32,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: `block_end_${nanoid(5)}`,
|
||||
type: WorkflowNodeType.BlockEnd,
|
||||
meta: {
|
||||
position: {
|
||||
x: 192,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
formMeta,
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Field, FieldArray, I18n, WorkflowNodePortsData } from '@flowgram.ai/free-layout-editor';
|
||||
import { ConditionRow, ConditionRowValueType } from '@flowgram.ai/form-materials';
|
||||
import { Button, Select, Space } from '@douyinfe/semi-ui';
|
||||
import { IconCrossCircleStroked, IconDelete, IconPlus } from '@douyinfe/semi-icons';
|
||||
|
||||
import { useNodeRenderContext, useIsSidebar } from '../../../hooks';
|
||||
import { Feedback, FormItem } from '../../../form-components';
|
||||
import { ConditionBranch, ConditionBranchLogic, ConditionPort } from './styles';
|
||||
|
||||
interface ConditionValue {
|
||||
key: string;
|
||||
value?: ConditionRowValueType;
|
||||
}
|
||||
|
||||
interface BranchItem {
|
||||
logic: string; // 'and' | 'or'
|
||||
conditions: ConditionValue[];
|
||||
}
|
||||
|
||||
export function ConditionInputs() {
|
||||
const { node, readonly } = useNodeRenderContext();
|
||||
const isSidebar = useIsSidebar();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
node.getData<WorkflowNodePortsData>(WorkflowNodePortsData).updateDynamicPorts();
|
||||
});
|
||||
}, [node]);
|
||||
|
||||
return (
|
||||
<FieldArray name="branch">
|
||||
{({ field: conditions }) => (
|
||||
<>
|
||||
{conditions.map((branch, index) => (
|
||||
<Field<BranchItem> name={branch.name} key={branch.name}>
|
||||
{({ field, fieldState }) => (
|
||||
<FormItem
|
||||
type="boolean"
|
||||
labelWidth={100}
|
||||
name={index === 0 ? I18n.t('IF') : I18n.t('ELSE-IF')}
|
||||
vertical
|
||||
required={index === 0}
|
||||
>
|
||||
<ConditionBranch>
|
||||
{field.value.conditions.length > 1 && (
|
||||
<ConditionBranchLogic>
|
||||
<Select
|
||||
size="small"
|
||||
value={field.value.logic}
|
||||
style={{ backgroundColor: 'var(--semi-color-bg-0)' }}
|
||||
onChange={(v) =>
|
||||
field.onChange({
|
||||
...field.value,
|
||||
logic: (v as string) ?? 'and',
|
||||
})
|
||||
}
|
||||
>
|
||||
<Select.Option value="and">{I18n.t('AND')}</Select.Option>
|
||||
<Select.Option value="or">{I18n.t('OR')}</Select.Option>
|
||||
</Select>
|
||||
</ConditionBranchLogic>
|
||||
)}
|
||||
<div style={{ flex: 1 }}>
|
||||
{field.value.conditions.map((condition, childIndex) => (
|
||||
<Field<ConditionValue>
|
||||
name={`${field.name}.conditions.${childIndex}`}
|
||||
key={condition.key}
|
||||
>
|
||||
{({ field: conditionField }) => (
|
||||
<Space align="center" style={{ padding: '6px 0', width: '100%' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ConditionRow
|
||||
readonly={readonly}
|
||||
value={conditionField.value.value}
|
||||
onChange={(v) => {
|
||||
conditionField.onChange({
|
||||
value: v,
|
||||
key: conditionField.value.key,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/*remove current branch condition*/}
|
||||
{isSidebar && !readonly && (
|
||||
<Button
|
||||
theme="borderless"
|
||||
disabled={field.value?.conditions.length === 1}
|
||||
icon={<IconCrossCircleStroked />}
|
||||
onClick={() =>
|
||||
field.onChange({
|
||||
...field.value,
|
||||
conditions: field.value.conditions.filter(
|
||||
(i: ConditionValue) => i.key !== condition.key
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Field>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ConditionPort data-port-id={`${branch.name}`} data-port-type="output" />
|
||||
</ConditionBranch>
|
||||
|
||||
{/* remove current branch and add new condition*/}
|
||||
{isSidebar && !readonly && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
size="small"
|
||||
theme="borderless"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => {
|
||||
field.onChange({
|
||||
...field.value,
|
||||
conditions: [
|
||||
...field.value.conditions,
|
||||
{
|
||||
key: `condition_${nanoid(6)}`,
|
||||
value: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
>
|
||||
{I18n.t('Add condition')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={conditions.value?.length === 1}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
icon={<IconDelete />}
|
||||
onClick={() => conditions.remove(index)}
|
||||
>
|
||||
{I18n.t('Remove branch')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Feedback errors={fieldState?.errors} invalid={fieldState?.invalid} />
|
||||
</FormItem>
|
||||
)}
|
||||
</Field>
|
||||
))}
|
||||
|
||||
{/* else */}
|
||||
<FormItem name={I18n.t('ELSE')} type="boolean" required={true} labelWidth={100}>
|
||||
<ConditionPort data-port-id="else" data-port-type="output" />
|
||||
</FormItem>
|
||||
|
||||
{!readonly && (
|
||||
<div>
|
||||
<Button
|
||||
theme="borderless"
|
||||
icon={<IconPlus />}
|
||||
onClick={() =>
|
||||
conditions.append({
|
||||
logic: 'and',
|
||||
conditions: [
|
||||
{
|
||||
key: `condition_${nanoid(6)}`,
|
||||
value: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
{I18n.t('Add branch')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FieldArray>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import styled from "styled-components";
|
||||
|
||||
export const ConditionPort = styled.div`
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
top: 50%;
|
||||
`;
|
||||
|
||||
export const ConditionBranch = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ConditionBranchLogic = styled.div`
|
||||
position: relative;
|
||||
width: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
border: 1px solid var(--semi-color-tertiary-light-active);
|
||||
border-radius: 6px 0 0 6px;
|
||||
border-right: none;
|
||||
left: 50%;
|
||||
top: 32px;
|
||||
bottom: 32px;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
|
||||
import { autoRenameRefEffect } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
|
||||
import { ConditionInputs } from './condition-inputs';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<ConditionInputs />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||
'branch.*': ({ value }) => {
|
||||
const haveEmptyCondition =
|
||||
value.conditions.filter((item: any) => {
|
||||
return Object.keys(item.value).length === 0;
|
||||
}).length > 0;
|
||||
if (haveEmptyCondition) return 'Condition is required';
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
effect: {
|
||||
conditions: autoRenameRefEffect,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import iconCondition from '../../assets/icon-condition.svg';
|
||||
|
||||
import { formMeta } from './form-meta';
|
||||
|
||||
let index = 0;
|
||||
export const MultiConditionNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.MultiCondition,
|
||||
info: {
|
||||
icon: iconCondition,
|
||||
description:
|
||||
'Connect multiple downstream branches. Only the corresponding branch will be executed if the set conditions are met.',
|
||||
},
|
||||
meta: {
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
// Condition Outputs use dynamic port
|
||||
useDynamicPort: true,
|
||||
expandable: false, // disable expanded
|
||||
size: {
|
||||
width: 360,
|
||||
height: 210,
|
||||
},
|
||||
},
|
||||
formMeta,
|
||||
onAdd() {
|
||||
return {
|
||||
id: `multi_condition_${nanoid(5)}`,
|
||||
type: 'condition',
|
||||
data: {
|
||||
title: `multi_condition_${++index}`,
|
||||
branch: [
|
||||
{
|
||||
logic: 'and',
|
||||
conditions: [
|
||||
{
|
||||
key: `condition_${nanoid(5)}`,
|
||||
value: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldRenderProps,
|
||||
FormRenderProps,
|
||||
FormMeta,
|
||||
ValidateTrigger,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import {
|
||||
DisplayOutputs,
|
||||
JsonSchemaEditor,
|
||||
provideJsonSchemaOutputs,
|
||||
syncVariableTitle,
|
||||
} from '@flowgram.ai/form-materials';
|
||||
|
||||
import { FlowNodeJSON, JsonSchema } from '../../typings';
|
||||
import { useIsSidebar } from '../../hooks';
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => {
|
||||
const isSidebar = useIsSidebar();
|
||||
if (isSidebar) {
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<Field
|
||||
name="outputs"
|
||||
render={({ field: { value, onChange } }: FieldRenderProps<JsonSchema>) => (
|
||||
<>
|
||||
<JsonSchemaEditor
|
||||
value={value}
|
||||
onChange={(value) => onChange(value as JsonSchema)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<DisplayOutputs displayFromScope />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
|
||||
},
|
||||
effect: {
|
||||
title: syncVariableTitle,
|
||||
outputs: provideJsonSchemaOutputs,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconStart from '../../assets/icon-start.jpg';
|
||||
import { formMeta } from './form-meta';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
|
||||
export const StartNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Start,
|
||||
meta: {
|
||||
isStart: true,
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
nodePanelVisible: false,
|
||||
defaultPorts: [{ type: 'output' }],
|
||||
size: {
|
||||
width: 360,
|
||||
height: 211,
|
||||
},
|
||||
},
|
||||
info: {
|
||||
icon: iconStart,
|
||||
description:
|
||||
'The starting node of the workflow, used to set the information needed to initiate the workflow.',
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
/**
|
||||
* Start Node cannot be added
|
||||
*/
|
||||
canAdd() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';
|
||||
import { AssignRows, createInferAssignPlugin, DisplayOutputs } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { FormHeader, FormContent } from '../../form-components';
|
||||
import { VariableNodeJSON } from './types';
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { useIsSidebar } from '../../hooks';
|
||||
|
||||
export const FormRender = ({ form }: FormRenderProps<VariableNodeJSON>) => {
|
||||
const isSidebar = useIsSidebar();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
{isSidebar ? <AssignRows name="assign" /> : <DisplayOutputs displayFromScope />}
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const formMeta: FormMeta = {
|
||||
render: (props) => <FormRender {...props} />,
|
||||
effect: defaultFormMeta.effect,
|
||||
plugins: [
|
||||
createInferAssignPlugin({
|
||||
assignKey: 'assign',
|
||||
outputKey: 'outputs',
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconVariable from '../../assets/icon-variable.png';
|
||||
import { formMeta } from './form-meta';
|
||||
|
||||
let index = 0;
|
||||
|
||||
export const VariableNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.Variable,
|
||||
info: {
|
||||
icon: iconVariable,
|
||||
description: 'Variable Assign and Declaration',
|
||||
},
|
||||
meta: {
|
||||
size: {
|
||||
width: 360,
|
||||
height: 390,
|
||||
},
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
id: `variable_${nanoid(5)}`,
|
||||
type: 'variable',
|
||||
data: {
|
||||
title: `Variable_${++index}`,
|
||||
assign: [
|
||||
{
|
||||
operator: 'declare',
|
||||
left: 'sum',
|
||||
right: {
|
||||
type: 'constant',
|
||||
content: 0,
|
||||
schema: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
formMeta: formMeta,
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeJSON } from '@flowgram.ai/free-layout-editor';
|
||||
import { AssignValueType, IJsonSchema } from '@flowgram.ai/form-materials';
|
||||
|
||||
export interface VariableNodeJSON extends FlowNodeJSON {
|
||||
data: {
|
||||
title: string;
|
||||
assign: AssignValueType[];
|
||||
outputs: IJsonSchema<'object'>;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user