feat(free-demo): create loop node

This commit is contained in:
liuyangxing 2025-03-17 19:19:36 +08:00
parent c83d06b593
commit e7656d4cec
18 changed files with 172 additions and 39 deletions

View File

@ -35,6 +35,7 @@
"@flowgram.ai/free-lines-plugin": "workspace:*", "@flowgram.ai/free-lines-plugin": "workspace:*",
"@flowgram.ai/free-node-panel-plugin": "workspace:*", "@flowgram.ai/free-node-panel-plugin": "workspace:*",
"@flowgram.ai/minimap-plugin": "workspace:*", "@flowgram.ai/minimap-plugin": "workspace:*",
"@flowgram.ai/free-container-plugin": "workspace:*",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"react": "^18", "react": "^18",

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -38,9 +38,7 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
outline: form?.state.invalid ? '1px solid red' : 'none', outline: form?.state.invalid ? '1px solid red' : 'none',
}} }}
> >
<NodeRenderContext.Provider value={nodeRender}> <NodeRenderContext.Provider value={{}}>{form?.render()}</NodeRenderContext.Provider>
{form?.render()}
</NodeRenderContext.Provider>
</BaseNodeStyle> </BaseNodeStyle>
</WorkflowNodeRenderer> </WorkflowNodeRenderer>
</ConfigProvider> </ConfigProvider>

View File

@ -6,14 +6,12 @@ export const BaseNodeStyle = styled.div`
background-color: #fff; background-color: #fff;
border: 1px solid rgba(6, 7, 9, 0.15); border: 1px solid rgba(6, 7, 9, 0.15);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
position: relative; position: relative;
width: 360px; width: 360px;
transition: all 0.3s ease;
&.selected { &.selected {
border: 1px solid var(--coz-stroke-hglt, #4e40e5); border: 1px solid var(--coz-stroke-hglt, #4e40e5);
} }

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { type NodeRenderReturnType } from '@flowgram.ai/free-layout-editor'; interface INodeRenderContext {}
export const NodeRenderContext = React.createContext<NodeRenderReturnType>({} as any); /** 业务自定义节点上下文 */
export const NodeRenderContext = React.createContext<INodeRenderContext>({});

View File

@ -1,8 +1,7 @@
import React, { useContext } from 'react'; import React from 'react';
import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor'; import { FlowNodeRegistry, useNodeRender } from '@flowgram.ai/free-layout-editor';
import { NodeRenderContext } from '../../context';
import { FormTitleDescription, FormWrapper } from './styles'; import { FormTitleDescription, FormWrapper } from './styles';
/** /**
@ -10,7 +9,7 @@ import { FormTitleDescription, FormWrapper } from './styles';
* @constructor * @constructor
*/ */
export function FormContent(props: { children?: React.ReactNode }) { export function FormContent(props: { children?: React.ReactNode }) {
const { expanded, node } = useContext(NodeRenderContext); const { expanded, node } = useNodeRender();
const registry = node.getNodeRegistry<FlowNodeRegistry>(); const registry = node.getNodeRegistry<FlowNodeRegistry>();
return ( return (
<FormWrapper> <FormWrapper>

View File

@ -1,25 +1,22 @@
import { useContext } from 'react';
import { import {
Command, Command,
Field, Field,
FieldRenderProps, FieldRenderProps,
useClientContext, useClientContext,
useNodeRender,
} from '@flowgram.ai/free-layout-editor'; } from '@flowgram.ai/free-layout-editor';
import { IconButton, Dropdown, Typography, Button } from '@douyinfe/semi-ui'; import { IconButton, Dropdown, Typography } from '@douyinfe/semi-ui';
import { IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';
import { IconMore } from '@douyinfe/semi-icons'; import { IconMore } from '@douyinfe/semi-icons';
import { Feedback } from '../feedback'; import { Feedback } from '../feedback';
import { FlowNodeRegistry } from '../../typings'; import { FlowNodeRegistry } from '../../typings';
import { NodeRenderContext } from '../../context';
import { getIcon } from './utils'; import { getIcon } from './utils';
import { Header, Operators, Title } from './styles'; import { Header, Operators, Title } from './styles';
const { Text } = Typography; const { Text } = Typography;
function DropdownContent() { function DropdownContent() {
const { node, deleteNode } = useContext(NodeRenderContext); const { node, deleteNode } = useNodeRender();
const clientContext = useClientContext(); const clientContext = useClientContext();
const registry = node.getNodeRegistry<FlowNodeRegistry>(); const registry = node.getNodeRegistry<FlowNodeRegistry>();
const handleCopy = () => { const handleCopy = () => {
@ -41,7 +38,7 @@ function DropdownContent() {
} }
export function FormHeader() { export function FormHeader() {
const { node, expanded, toggleExpand, readonly } = useContext(NodeRenderContext); const { node, expanded, toggleExpand, readonly } = useNodeRender();
return ( return (
<Header> <Header>
@ -56,13 +53,6 @@ export function FormHeader() {
)} )}
</Field> </Field>
</Title> </Title>
<Button
type="primary"
icon={expanded ? <IconSmallTriangleDown /> : <IconSmallTriangleLeft />}
size="small"
theme="borderless"
onClick={toggleExpand}
/>
{readonly ? undefined : ( {readonly ? undefined : (
<Operators> <Operators>
<Dropdown trigger="hover" position="bottomRight" render={<DropdownContent />}> <Dropdown trigger="hover" position="bottomRight" render={<DropdownContent />}>

View File

@ -7,7 +7,8 @@ export const Header = styled.div`
align-items: center; align-items: center;
width: 100%; width: 100%;
column-gap: 8px; column-gap: 8px;
border-radius: 8px; border-radius: 8px 8px 0 0;
cursor: move;
background: linear-gradient(#f2f2ff 0%, rgba(0, 0, 0, 0.02) 100%); background: linear-gradient(#f2f2ff 0%, rgba(0, 0, 0, 0.02) 100%);
overflow: hidden; overflow: hidden;

View File

@ -1,15 +1,12 @@
import { useContext } from 'react'; import { Field, useNodeRender } from '@flowgram.ai/free-layout-editor';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FxExpression } from '../fx-expression'; import { FxExpression } from '../fx-expression';
import { FormItem } from '../form-item'; import { FormItem } from '../form-item';
import { Feedback } from '../feedback'; import { Feedback } from '../feedback';
import { JsonSchema } from '../../typings'; import { JsonSchema } from '../../typings';
import { NodeRenderContext } from '../../context';
export function FormInputs() { export function FormInputs() {
const { readonly } = useContext(NodeRenderContext); const { readonly } = useNodeRender();
return ( return (
<Field<JsonSchema> name="inputs"> <Field<JsonSchema> name="inputs">
{({ field: inputsField }) => { {({ field: inputsField }) => {

View File

@ -1,10 +1,10 @@
import React, { useContext, useState } from 'react'; import React, { useState } from 'react';
import { useNodeRender } from '@flowgram.ai/free-layout-editor';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconPlus } from '@douyinfe/semi-icons'; import { IconPlus } from '@douyinfe/semi-icons';
import { JsonSchema } from '../../typings'; import { JsonSchema } from '../../typings';
import { NodeRenderContext } from '../../context';
import { PropertyEdit } from './property-edit'; import { PropertyEdit } from './property-edit';
export interface PropertiesEditProps { export interface PropertiesEditProps {
@ -15,7 +15,7 @@ export interface PropertiesEditProps {
export const PropertiesEdit: React.FC<PropertiesEditProps> = (props) => { export const PropertiesEdit: React.FC<PropertiesEditProps> = (props) => {
const value = (props.value || {}) as Record<string, JsonSchema>; const value = (props.value || {}) as Record<string, JsonSchema>;
const { readonly } = useContext(NodeRenderContext); const { readonly } = useNodeRender();
const [newProperty, updateNewPropertyFromCache] = useState<{ key: string; value: JsonSchema }>({ const [newProperty, updateNewPropertyFromCache] = useState<{ key: string; value: JsonSchema }>({
key: '', key: '',
value: { type: 'string' }, value: { type: 'string' },

View File

@ -9,6 +9,7 @@ import {
} from '@flowgram.ai/free-node-panel-plugin'; } from '@flowgram.ai/free-node-panel-plugin';
import { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin'; import { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';
import { FreeLayoutProps } from '@flowgram.ai/free-layout-editor'; import { FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
import { FlowNodeRegistry, FlowDocumentJSON } from '../typings'; import { FlowNodeRegistry, FlowDocumentJSON } from '../typings';
import { shortcuts } from '../shortcuts'; import { shortcuts } from '../shortcuts';
@ -199,6 +200,7 @@ export function useEditorProps(
createFreeNodePanelPlugin({ createFreeNodePanelPlugin({
renderer: NodePanel, renderer: NodePanel,
}), }),
createContainerNodePlugin({}),
], ],
}), }),
[] []

View File

@ -1,7 +1,5 @@
import { useContext } from 'react';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { Field, FieldArray } from '@flowgram.ai/free-layout-editor'; import { Field, FieldArray, useNodeRender } from '@flowgram.ai/free-layout-editor';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconPlus, IconCrossCircleStroked } from '@douyinfe/semi-icons'; import { IconPlus, IconCrossCircleStroked } from '@douyinfe/semi-icons';
@ -9,7 +7,6 @@ import { FlowLiteralValueSchema, FlowRefValueSchema } from '../../../typings';
import { FxExpression } from '../../../form-components/fx-expression'; import { FxExpression } from '../../../form-components/fx-expression';
import { FormItem } from '../../../form-components'; import { FormItem } from '../../../form-components';
import { Feedback } from '../../../form-components'; import { Feedback } from '../../../form-components';
import { NodeRenderContext } from '../../../context';
import { ConditionPort } from './styles'; import { ConditionPort } from './styles';
interface ConditionValue { interface ConditionValue {
@ -18,7 +15,7 @@ interface ConditionValue {
} }
export function ConditionInputs() { export function ConditionInputs() {
const { readonly } = useContext(NodeRenderContext); const { readonly } = useNodeRender();
return ( return (
<FieldArray name="inputsValues.conditions"> <FieldArray name="inputsValues.conditions">
{({ field }) => ( {({ field }) => (

View File

@ -1,5 +1,6 @@
import { FlowNodeRegistry } from '../typings'; import { FlowNodeRegistry } from '../typings';
import { StartNodeRegistry } from './start'; import { StartNodeRegistry } from './start';
import { LoopNodeRegistry } from './loop';
import { LLMNodeRegistry } from './llm'; import { LLMNodeRegistry } from './llm';
import { EndNodeRegistry } from './end'; import { EndNodeRegistry } from './end';
import { ConditionNodeRegistry } from './condition'; import { ConditionNodeRegistry } from './condition';
@ -9,4 +10,5 @@ export const nodeRegistries: FlowNodeRegistry[] = [
StartNodeRegistry, StartNodeRegistry,
EndNodeRegistry, EndNodeRegistry,
LLMNodeRegistry, LLMNodeRegistry,
LoopNodeRegistry,
]; ];

View File

@ -0,0 +1,54 @@
import { nanoid } from 'nanoid';
import { ContainerNodeRenderKey } from '@flowgram.ai/free-container-plugin';
import { FlowNodeRegistry } from '../../typings';
import iconLoop from '../../assets/icon-loop.jpg';
let index = 0;
export const LoopNodeRegistry: FlowNodeRegistry = {
type: 'loop',
info: {
icon: iconLoop,
description:
'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',
},
meta: {
renderKey: ContainerNodeRenderKey,
isContainer: true,
size: {
width: 560,
height: 400,
},
padding: () => ({
top: 205,
bottom: 50,
left: 100,
right: 100,
}),
},
onAdd() {
return {
id: `loop_${nanoid(5)}`,
type: 'loop',
data: {
title: `Loop_${++index}`,
inputsValues: {},
inputs: {
type: 'object',
required: ['loopTimes'],
properties: {
loopTimes: {
type: 'number',
},
},
},
outputs: {
type: 'object',
properties: {
result: { type: 'string' },
},
},
},
};
},
};

View File

@ -29,6 +29,7 @@
"@flowgram.ai/free-auto-layout-plugin": "workspace:*", "@flowgram.ai/free-auto-layout-plugin": "workspace:*",
"@flowgram.ai/minimap-plugin": "workspace:*", "@flowgram.ai/minimap-plugin": "workspace:*",
"@flowgram.ai/free-stack-plugin": "workspace:*", "@flowgram.ai/free-stack-plugin": "workspace:*",
"@flowgram.ai/free-container-plugin": "workspace:*",
"@flowgram.ai/free-snap-plugin": "workspace:*", "@flowgram.ai/free-snap-plugin": "workspace:*",
"@flowgram.ai/free-node-panel-plugin": "workspace:*", "@flowgram.ai/free-node-panel-plugin": "workspace:*",
"@flowgram.ai/free-lines-plugin": "workspace:*", "@flowgram.ai/free-lines-plugin": "workspace:*",

View File

@ -193,6 +193,9 @@ importers:
'@douyinfe/semi-ui': '@douyinfe/semi-ui':
specifier: ^2.72.3 specifier: ^2.72.3
version: 2.72.3(acorn@8.14.0)(react-dom@18.3.1)(react@18.3.1) version: 2.72.3(acorn@8.14.0)(react-dom@18.3.1)(react@18.3.1)
'@flowgram.ai/free-container-plugin':
specifier: workspace:*
version: link:../../packages/plugins/free-container-plugin
'@flowgram.ai/free-layout-editor': '@flowgram.ai/free-layout-editor':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/client/free-layout-editor version: link:../../packages/client/free-layout-editor
@ -409,6 +412,9 @@ importers:
'@flowgram.ai/free-auto-layout-plugin': '@flowgram.ai/free-auto-layout-plugin':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/plugins/free-auto-layout-plugin version: link:../../packages/plugins/free-auto-layout-plugin
'@flowgram.ai/free-container-plugin':
specifier: workspace:*
version: link:../../packages/plugins/free-container-plugin
'@flowgram.ai/free-layout-editor': '@flowgram.ai/free-layout-editor':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/client/free-layout-editor version: link:../../packages/client/free-layout-editor
@ -2083,6 +2089,85 @@ importers:
specifier: ^0.34.6 specifier: ^0.34.6
version: 0.34.6(jsdom@22.1.0) version: 0.34.6(jsdom@22.1.0)
../../packages/plugins/free-container-plugin:
dependencies:
'@flowgram.ai/core':
specifier: workspace:*
version: link:../../canvas-engine/core
'@flowgram.ai/document':
specifier: workspace:*
version: link:../../canvas-engine/document
'@flowgram.ai/free-history-plugin':
specifier: workspace:*
version: link:../free-history-plugin
'@flowgram.ai/free-layout-core':
specifier: workspace:*
version: link:../../canvas-engine/free-layout-core
'@flowgram.ai/free-lines-plugin':
specifier: workspace:*
version: link:../free-lines-plugin
'@flowgram.ai/renderer':
specifier: workspace:*
version: link:../../canvas-engine/renderer
'@flowgram.ai/utils':
specifier: workspace:*
version: link:../../common/utils
inversify:
specifier: ^6.0.1
version: 6.2.0(reflect-metadata@0.2.2)
lodash:
specifier: ^4.17.21
version: 4.17.21
reflect-metadata:
specifier: ~0.2.2
version: 0.2.2
devDependencies:
'@flowgram.ai/eslint-config':
specifier: workspace:*
version: link:../../../config/eslint-config
'@flowgram.ai/ts-config':
specifier: workspace:*
version: link:../../../config/ts-config
'@types/bezier-js':
specifier: 4.1.3
version: 4.1.3
'@types/lodash':
specifier: ^4.14.137
version: 4.17.13
'@types/react':
specifier: ^18
version: 18.3.16
'@types/react-dom':
specifier: ^18
version: 18.3.5(@types/react@18.3.16)
'@types/styled-components':
specifier: ^5
version: 5.1.34
'@vitest/coverage-v8':
specifier: ^0.32.0
version: 0.32.4(vitest@0.34.6)
eslint:
specifier: ^8.54.0
version: 8.57.1
react:
specifier: ^18
version: 18.3.1
react-dom:
specifier: ^18
version: 18.3.1(react@18.3.1)
styled-components:
specifier: ^5
version: 5.3.11(@babel/core@7.26.0)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)
tsup:
specifier: ^8.0.1
version: 8.3.5(typescript@5.0.4)
typescript:
specifier: ^5.0.4
version: 5.0.4
vitest:
specifier: ^0.34.6
version: 0.34.6(jsdom@22.1.0)
../../packages/plugins/free-history-plugin: ../../packages/plugins/free-history-plugin:
dependencies: dependencies:
'@flowgram.ai/core': '@flowgram.ai/core':

View File

@ -3,4 +3,5 @@ import styled from 'styled-components';
export const ContainerNodeFormStyle = styled.div` export const ContainerNodeFormStyle = styled.div`
background-color: #fff; background-color: #fff;
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
width: 100%;
`; `;

View File

@ -607,6 +607,12 @@
"versionPolicyName": "publishPolicy", "versionPolicyName": "publishPolicy",
"tags": ["level-1","team-flow"] "tags": ["level-1","team-flow"]
}, },
{
"packageName": "@flowgram.ai/free-container-plugin",
"projectFolder": "packages/plugins/free-container-plugin",
"versionPolicyName": "publishPolicy",
"tags": ["level-1","team-flow"]
},
{ {
"packageName": "@flowgram.ai/group-plugin", "packageName": "@flowgram.ai/group-plugin",
"projectFolder": "packages/plugins/group-plugin", "projectFolder": "packages/plugins/group-plugin",