From c7b88cbcfb2a73cd3a4fb1bd9d3c7b3bfce0e5fa Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 13 Aug 2025 20:59:21 +0800 Subject: [PATCH] feat(ui): add onDocumentChange handler in WorkflowDesigner --- .../components/workflow/designer/Designer.tsx | 295 ++++++++++-------- .../workflow/designer/DesignerContext.ts | 4 + .../workflow/designer/NodeRender.tsx | 17 +- .../components/workflow/designer/Toolbar.tsx | 1 + .../workflow/designer/nodes/_shared.tsx | 1 + .../pages/workflows/WorkflowDetailDesign.tsx | 11 +- 6 files changed, 186 insertions(+), 143 deletions(-) diff --git a/ui/src/components/workflow/designer/Designer.tsx b/ui/src/components/workflow/designer/Designer.tsx index bee18f0a..308cf3a7 100644 --- a/ui/src/components/workflow/designer/Designer.tsx +++ b/ui/src/components/workflow/designer/Designer.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useImperativeHandle, useMemo, useRef } from "react"; +import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from "react"; import { ConstantKeys, EditorRenderer, @@ -27,6 +27,8 @@ export interface DesignerProps { children?: React.ReactNode; initialData?: FlowDocumentJSON; readonly?: boolean; + onDocumentChange?: (ctx: FixedLayoutPluginContext) => void; + onNodeChange?: (ctx: FixedLayoutPluginContext, node: FlowNodeEntity) => void; onNodeClick?: (ctx: FixedLayoutPluginContext, node: FlowNodeEntity) => void; } @@ -35,149 +37,180 @@ export interface DesignerInstance extends FixedLayoutPluginContext { validateAllNodes(): Promise; } -const Designer = forwardRef(({ className, style, children, initialData, readonly, onNodeClick }, ref) => { - const { token: themeToken } = theme.useToken(); +const Designer = forwardRef( + ({ className, style, children, initialData, readonly, onDocumentChange, onNodeChange, onNodeClick }, ref) => { + const { token: themeToken } = theme.useToken(); - const flowgramEditorRef = useRef(null); - const flowgramEditorProps = useMemo( - () => ({ - initialData: initialData, + const rendered = useRef(false); - constants: { - [ConstantKeys.BASE_COLOR]: themeToken.colorBorder, - [ConstantKeys.BASE_ACTIVATED_COLOR]: themeToken.colorPrimary, - [ConstantKeys.NODE_SPACING]: 48, - [ConstantKeys.BRANCH_SPACING]: 48, - }, + const flowgramEditorRef = useRef(null); + const flowgramEditorProps = useMemo( + () => ({ + initialData: initialData, - background: { - backgroundColor: themeToken.colorBgContainer, - dotSize: 0, - }, - - playground: { - autoFocus: true, - autoResize: true, - preventGlobalGesture: true, - }, - - selectBox: { - enable: false, - }, - - scroll: { - enableScrollLimit: true, - }, - - readonly: readonly, - - nodeEngine: { - enable: true, - }, - - variableEngine: { - enable: true, - }, - - materials: { - components: getAllElements(), - renderTexts: { - [FlowTextKey.TRY_START_TEXT]: "Try", - [FlowTextKey.TRY_END_TEXT]: "Then", - [FlowTextKey.CATCH_TEXT]: "Catch", + constants: { + [ConstantKeys.BASE_COLOR]: themeToken.colorBorder, + [ConstantKeys.BASE_ACTIVATED_COLOR]: themeToken.colorPrimary, + [ConstantKeys.NODE_SPACING]: 48, + [ConstantKeys.BRANCH_SPACING]: 48, }, - renderDefaultNode: NodeRender, - }, - nodeRegistries: getAllNodeRegistries(), + background: { + backgroundColor: themeToken.colorBgContainer, + dotSize: 0, + }, - getNodeDefaultRegistry(type) { - return { - type, - meta: { - defaultExpanded: true, + playground: { + autoFocus: true, + autoResize: true, + preventGlobalGesture: true, + }, + + selectBox: { + enable: false, + }, + + scroll: { + enableScrollLimit: true, + }, + + readonly: readonly, + + nodeEngine: { + enable: true, + }, + + variableEngine: { + enable: true, + }, + + materials: { + components: getAllElements(), + renderTexts: { + [FlowTextKey.TRY_START_TEXT]: "Try", + [FlowTextKey.TRY_END_TEXT]: "Then", + [FlowTextKey.CATCH_TEXT]: "Catch", }, - formMeta: { - render: () => , - }, - }; - }, + renderDefaultNode: NodeRender, + }, - plugins: () => [ - createMinimapPlugin({ - disableLayer: true, - enableDisplayAllNodes: true, - canvasStyle: { - canvasWidth: 160, - canvasHeight: 160, - }, - }), - ], + nodeRegistries: getAllNodeRegistries(), - onAllLayersRendered: (ctx) => { - // 画布初始化后向下滚动一点,露出可能被 Alert 遮挡的部分 - setTimeout(() => { - ctx.playground.config.scroll({ scrollY: -80 }); - }, 1); - }, - }), - [themeToken, initialData, readonly] - ); + getNodeDefaultRegistry(type) { + return { + type, + meta: { + defaultExpanded: true, + }, + formMeta: { + render: () => , + }, + }; + }, - useImperativeHandle(ref, () => { - return { - get container() { - return flowgramEditorRef.current!.container; - }, - get document() { - return flowgramEditorRef.current!.document; - }, - get playground() { - return flowgramEditorRef.current!.playground; - }, - get operation() { - return flowgramEditorRef.current!.operation; - }, - get clipboard() { - return flowgramEditorRef.current!.clipboard; - }, - get selection() { - return flowgramEditorRef.current!.selection; - }, - get history() { - return flowgramEditorRef.current!.history; - }, + plugins: () => [ + createMinimapPlugin({ + disableLayer: true, + enableDisplayAllNodes: true, + canvasStyle: { + canvasWidth: 160, + canvasHeight: 160, + }, + }), + ], - get(identifier) { - return flowgramEditorRef.current!.get(identifier); - }, - getAll(identifier) { - return flowgramEditorRef.current!.getAll(identifier); - }, - validateNode(node) { - if (typeof node === "string") { - node = flowgramEditorRef.current!.document.getNode(node)!; + onAllLayersRendered: (ctx) => { + rendered.current = true; + + // 画布初始化后向下滚动一点,露出可能被 Alert 遮挡的部分 + setTimeout(() => { + ctx.playground.config.scroll({ scrollY: -80 }); + }, 1); + }, + }), + [themeToken, initialData, readonly] + ); + + useEffect(() => { + const callback = () => { + if (rendered.current) { + onDocumentChange?.(flowgramEditorRef.current!); } + }; + const d1 = flowgramEditorRef.current?.document?.onNodeCreate(callback); + const d2 = flowgramEditorRef.current?.document?.onNodeUpdate(callback); + const d3 = flowgramEditorRef.current?.document?.onNodeDispose(callback); + const d4 = flowgramEditorRef.current?.document?.renderTree?.onTreeChange(callback); - const form = getNodeForm(node); - return form ? form.validate().then((res) => res && !form.state.invalid) : Promise.resolve(true); - }, - validateAllNodes() { - const nodes = flowgramEditorRef.current!.document.getAllNodes(); - const forms = nodes.map((node) => getNodeForm(node)).filter((form) => form != null); - return Promise.allSettled(forms.map((form) => form.validate())).then((res) => forms.every((form, index) => res[index] && !form.state.invalid)); - }, - }; - }); + return () => { + d1?.dispose(); + d2?.dispose(); + d3?.dispose(); + d4?.dispose(); + }; + }, [onDocumentChange]); - return ( - - onNodeClick?.(flowgramEditorRef.current!, node) }}> - - {children} - - - ); -}); + useImperativeHandle(ref, () => { + return { + get container() { + return flowgramEditorRef.current!.container; + }, + get document() { + return flowgramEditorRef.current!.document; + }, + get playground() { + return flowgramEditorRef.current!.playground; + }, + get operation() { + return flowgramEditorRef.current!.operation; + }, + get clipboard() { + return flowgramEditorRef.current!.clipboard; + }, + get selection() { + return flowgramEditorRef.current!.selection; + }, + get history() { + return flowgramEditorRef.current!.history; + }, + + get(identifier) { + return flowgramEditorRef.current!.get(identifier); + }, + getAll(identifier) { + return flowgramEditorRef.current!.getAll(identifier); + }, + validateNode(node) { + if (typeof node === "string") { + node = flowgramEditorRef.current!.document.getNode(node)!; + } + + const form = getNodeForm(node); + return form ? form.validate().then((res) => res && !form.state.invalid) : Promise.resolve(true); + }, + validateAllNodes() { + const nodes = flowgramEditorRef.current!.document.getAllNodes(); + const forms = nodes.map((node) => getNodeForm(node)).filter((form) => form != null); + return Promise.allSettled(forms.map((form) => form.validate())).then((res) => forms.every((form, index) => res[index] && !form.state.invalid)); + }, + }; + }); + + return ( + + onDocumentChange?.(flowgramEditorRef.current!), + onNodeChange: (node) => onNodeChange?.(flowgramEditorRef.current!, node), + onNodeClick: (node) => onNodeClick?.(flowgramEditorRef.current!, node), + }} + > + + {children} + + + ); + } +); export default Designer; diff --git a/ui/src/components/workflow/designer/DesignerContext.ts b/ui/src/components/workflow/designer/DesignerContext.ts index 4db5975e..b0579a18 100644 --- a/ui/src/components/workflow/designer/DesignerContext.ts +++ b/ui/src/components/workflow/designer/DesignerContext.ts @@ -2,10 +2,14 @@ import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; export type DesignerContextType = { + onDocumentChange: () => void; + onNodeChange: (node: FlowNodeEntity) => void; onNodeClick: (node: FlowNodeEntity) => void; }; export const DesignerContext = createContext({ + onDocumentChange: () => {}, + onNodeChange: () => {}, onNodeClick: () => {}, }); diff --git a/ui/src/components/workflow/designer/NodeRender.tsx b/ui/src/components/workflow/designer/NodeRender.tsx index e1553678..9969aa03 100644 --- a/ui/src/components/workflow/designer/NodeRender.tsx +++ b/ui/src/components/workflow/designer/NodeRender.tsx @@ -14,16 +14,25 @@ const Node = (_: NodeProps) => { const nodeRender = useNodeRender(); - const designer = useDesignerContext(); + const { onDocumentChange: fireOnDocumentChange, onNodeChange: fireOnNodeChange, onNodeClick: fireOnNodeClick } = useDesignerContext(); useEffect(() => { const d = ctx.document.originTree.onTreeChange(() => refresh()); + return () => d.dispose(); }, []); useEffect(() => { - const d1 = nodeRender.form?.onFormValuesChange?.(() => refresh()); - const d2 = nodeRender.form?.onValidate?.(() => refresh()); + const d1 = nodeRender.form?.onFormValuesChange?.(() => { + refresh(); + + fireOnNodeChange(nodeRender.node); + fireOnDocumentChange(); + }); + const d2 = nodeRender.form?.onValidate?.(() => { + refresh(); + }); + return () => { d1?.dispose(); d2?.dispose(); @@ -33,7 +42,7 @@ const Node = (_: NodeProps) => { const handleNodeClick = () => { const node = nodeRender.node; if (node.getNodeRegistry().meta?.clickable) { - designer.onNodeClick?.(node); + fireOnNodeClick(node); } }; diff --git a/ui/src/components/workflow/designer/Toolbar.tsx b/ui/src/components/workflow/designer/Toolbar.tsx index a7518a5d..666feeb0 100644 --- a/ui/src/components/workflow/designer/Toolbar.tsx +++ b/ui/src/components/workflow/designer/Toolbar.tsx @@ -25,6 +25,7 @@ const Toolbar = ({ className, style }: ToolbarProps) => { useEffect(() => { const d = playground.config.onReadonlyOrDisabledChange(() => refresh()); + return () => d.dispose(); }, [playground]); diff --git a/ui/src/components/workflow/designer/nodes/_shared.tsx b/ui/src/components/workflow/designer/nodes/_shared.tsx index ac195cb9..d95c0d08 100644 --- a/ui/src/components/workflow/designer/nodes/_shared.tsx +++ b/ui/src/components/workflow/designer/nodes/_shared.tsx @@ -150,6 +150,7 @@ const InternalNodeMenuButton = ({ }; const d1 = node.onEntityChange(callback); const d2 = node.parent?.onEntityChange?.(callback); + return () => { d1?.dispose(); d2?.dispose(); diff --git a/ui/src/pages/workflows/WorkflowDetailDesign.tsx b/ui/src/pages/workflows/WorkflowDetailDesign.tsx index b913e28f..d0371fb8 100644 --- a/ui/src/pages/workflows/WorkflowDetailDesign.tsx +++ b/ui/src/pages/workflows/WorkflowDetailDesign.tsx @@ -55,12 +55,7 @@ const WorkflowDetailDesign = () => { console.log("document changed", designerRef.current!.document.toJSON()); }); - useEffect(() => { - const disposable = designerRef.current?.document?.originTree?.onTreeChange(onDesignerDocumentChange); - return () => disposable?.dispose(); - }, []); - - const { setNode: setDrawerNode, setOpen: setNodeDrawerOpen, ...nodeDrawerProps } = WorkflowNodeDrawer.useProps(); + const { drawerProps: nodeDrawerProps, ...nodeDrawer } = WorkflowNodeDrawer.useDrawer(); const handleRollbackClick = () => { modal.confirm({ @@ -116,9 +111,9 @@ const WorkflowDetailDesign = () => { { - setDrawerNode(node); - setNodeDrawerOpen(true); + nodeDrawer.open(node); }} >