mirror of
https://github.com/certimate-go/certimate.git
synced 2026-06-19 21:03:27 +08:00
feat(ui): add onDocumentChange handler in WorkflowDesigner
This commit is contained in:
parent
f440103fce
commit
c7b88cbcfb
@ -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<boolean>;
|
||||
}
|
||||
|
||||
const Designer = forwardRef<DesignerInstance, DesignerProps>(({ className, style, children, initialData, readonly, onNodeClick }, ref) => {
|
||||
const { token: themeToken } = theme.useToken();
|
||||
const Designer = forwardRef<DesignerInstance, DesignerProps>(
|
||||
({ className, style, children, initialData, readonly, onDocumentChange, onNodeChange, onNodeClick }, ref) => {
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const flowgramEditorRef = useRef<FixedLayoutPluginContext>(null);
|
||||
const flowgramEditorProps = useMemo<FixedLayoutProps>(
|
||||
() => ({
|
||||
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<FixedLayoutPluginContext>(null);
|
||||
const flowgramEditorProps = useMemo<FixedLayoutProps>(
|
||||
() => ({
|
||||
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: () => <BranchNode description={type} />,
|
||||
},
|
||||
};
|
||||
},
|
||||
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: () => <BranchNode description={type} />,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
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 (
|
||||
<FixedLayoutEditorProvider ref={flowgramEditorRef} {...flowgramEditorProps}>
|
||||
<DegisnerContextProvider value={{ onNodeClick: (node) => onNodeClick?.(flowgramEditorRef.current!, node) }}>
|
||||
<EditorRenderer className={className} style={style} />
|
||||
{children}
|
||||
</DegisnerContextProvider>
|
||||
</FixedLayoutEditorProvider>
|
||||
);
|
||||
});
|
||||
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 (
|
||||
<FixedLayoutEditorProvider ref={flowgramEditorRef} {...flowgramEditorProps}>
|
||||
<DegisnerContextProvider
|
||||
value={{
|
||||
onDocumentChange: () => onDocumentChange?.(flowgramEditorRef.current!),
|
||||
onNodeChange: (node) => onNodeChange?.(flowgramEditorRef.current!, node),
|
||||
onNodeClick: (node) => onNodeClick?.(flowgramEditorRef.current!, node),
|
||||
}}
|
||||
>
|
||||
<EditorRenderer className={className} style={style} />
|
||||
{children}
|
||||
</DegisnerContextProvider>
|
||||
</FixedLayoutEditorProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Designer;
|
||||
|
||||
@ -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<DesignerContextType>({
|
||||
onDocumentChange: () => {},
|
||||
onNodeChange: () => {},
|
||||
onNodeClick: () => {},
|
||||
});
|
||||
|
||||
|
||||
@ -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<NodeRegistry>().meta?.clickable) {
|
||||
designer.onNodeClick?.(node);
|
||||
fireOnNodeClick(node);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ const Toolbar = ({ className, style }: ToolbarProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const d = playground.config.onReadonlyOrDisabledChange(() => refresh());
|
||||
|
||||
return () => d.dispose();
|
||||
}, [playground]);
|
||||
|
||||
|
||||
@ -150,6 +150,7 @@ const InternalNodeMenuButton = ({
|
||||
};
|
||||
const d1 = node.onEntityChange(callback);
|
||||
const d2 = node.parent?.onEntityChange?.(callback);
|
||||
|
||||
return () => {
|
||||
d1?.dispose();
|
||||
d2?.dispose();
|
||||
|
||||
@ -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 = () => {
|
||||
<WorkflowDesigner
|
||||
ref={designerRef}
|
||||
initialData={degisnerData}
|
||||
onDocumentChange={onDesignerDocumentChange}
|
||||
onNodeClick={(_, node) => {
|
||||
setDrawerNode(node);
|
||||
setNodeDrawerOpen(true);
|
||||
nodeDrawer.open(node);
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-8 z-10 w-full px-4">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user