Merge pull request #942 from SuanmoSuanyangTechnology/feature/history_zy
feat(web): workflow support undo/redo
This commit is contained in:
@@ -2515,6 +2515,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
arrange: 'Arrange',
|
arrange: 'Arrange',
|
||||||
redo: 'Redo',
|
redo: 'Redo',
|
||||||
undo: 'Undo',
|
undo: 'Undo',
|
||||||
|
fit: 'Fit View',
|
||||||
|
|
||||||
input: 'Input',
|
input: 'Input',
|
||||||
output: 'Output',
|
output: 'Output',
|
||||||
|
|||||||
@@ -2479,6 +2479,7 @@ export const zh = {
|
|||||||
arrange: '整理',
|
arrange: '整理',
|
||||||
redo: '重做',
|
redo: '重做',
|
||||||
undo: '撤销',
|
undo: '撤销',
|
||||||
|
fit: '自适应',
|
||||||
|
|
||||||
input: '输入',
|
input: '输入',
|
||||||
output: '输出',
|
output: '输出',
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Select, Divider } from 'antd';
|
import { Select, Divider, Tooltip } from 'antd';
|
||||||
import { PlusOutlined, MinusOutlined, FileAddOutlined } from '@ant-design/icons'
|
import { PlusOutlined, MinusOutlined, FileAddOutlined, UndoOutlined, RedoOutlined } from '@ant-design/icons'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Node } from '@antv/x6';
|
import { Node } from '@antv/x6';
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import type { GraphRef } from '../types'
|
import type { GraphRef } from '../types'
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ interface CanvasToolbarProps {
|
|||||||
setIsHandMode: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsHandMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
addNotes: () => void;
|
addNotes: () => void;
|
||||||
|
canUndo: boolean;
|
||||||
|
canRedo: boolean;
|
||||||
|
onUndo: () => void;
|
||||||
|
onRedo: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
||||||
@@ -22,12 +27,13 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
|||||||
miniMapRef,
|
miniMapRef,
|
||||||
graphRef,
|
graphRef,
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
// canUndo,
|
canUndo,
|
||||||
// canRedo,
|
canRedo,
|
||||||
// onUndo,
|
onUndo,
|
||||||
// onRedo,
|
onRedo,
|
||||||
addNotes,
|
addNotes,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 小地图 */}
|
{/* 小地图 */}
|
||||||
@@ -63,13 +69,16 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
|||||||
{ label: '125%', value: 125 },
|
{ label: '125%', value: 125 },
|
||||||
{ label: '150%', value: 150 },
|
{ label: '150%', value: 150 },
|
||||||
{ label: '200%', value: 200 },
|
{ label: '200%', value: 200 },
|
||||||
{ label: '自适应', value: 'fit' },
|
{ label: t('workflow.fit'), value: 'fit' },
|
||||||
]}
|
]}
|
||||||
variant='borderless'
|
variant='borderless'
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
<PlusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(0.1)} />
|
<PlusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(0.1)} />
|
||||||
<Divider type="vertical" className="rb:h-4" />
|
<Divider type="vertical" className="rb:h-4" />
|
||||||
|
<Tooltip title={`${t('workflow.undo')} (Ctrl+Z)`}><UndoOutlined className={clsx('rb:text-[16px]', canUndo ? 'rb:cursor-pointer' : 'rb:opacity-30 rb:cursor-not-allowed')} onClick={onUndo} /></Tooltip>
|
||||||
|
<Tooltip title={`${t('workflow.redo')} (Ctrl+Y)`}><RedoOutlined className={clsx('rb:text-[16px]', canRedo ? 'rb:cursor-pointer' : 'rb:opacity-30 rb:cursor-not-allowed')} onClick={onRedo} /></Tooltip>
|
||||||
|
<Divider type="vertical" className="rb:h-4" />
|
||||||
<FileAddOutlined onClick={addNotes} />
|
<FileAddOutlined onClick={addNotes} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:17:48
|
* @Date: 2026-02-03 15:17:48
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-15 16:02:49
|
* @Last Modified time: 2026-04-20 16:00:26
|
||||||
*/
|
*/
|
||||||
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, type Edge } from '@antv/x6';
|
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, History, type Edge } from '@antv/x6';
|
||||||
|
import type { HistoryCommand as Command } from '@antv/x6/lib/plugin/history/type';
|
||||||
import { register } from '@antv/x6-react-shape';
|
import { register } from '@antv/x6-react-shape';
|
||||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||||
import { App } from 'antd';
|
import { App } from 'antd';
|
||||||
@@ -64,6 +65,14 @@ export interface UseWorkflowGraphReturn {
|
|||||||
copyEvent: () => boolean | void;
|
copyEvent: () => boolean | void;
|
||||||
/** Handler for paste keyboard event */
|
/** Handler for paste keyboard event */
|
||||||
parseEvent: () => boolean | void;
|
parseEvent: () => boolean | void;
|
||||||
|
/** Whether undo is available */
|
||||||
|
canUndo: boolean;
|
||||||
|
/** Whether redo is available */
|
||||||
|
canRedo: boolean;
|
||||||
|
/** Undo last action */
|
||||||
|
undo: () => void;
|
||||||
|
/** Redo last undone action */
|
||||||
|
redo: () => void;
|
||||||
/** Function to save workflow configuration */
|
/** Function to save workflow configuration */
|
||||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||||
/** Chat variables for workflow */
|
/** Chat variables for workflow */
|
||||||
@@ -108,6 +117,8 @@ export const useWorkflowGraph = ({
|
|||||||
const [config, setConfig] = useState<WorkflowConfig | null>(null);
|
const [config, setConfig] = useState<WorkflowConfig | null>(null);
|
||||||
const [chatVariables, setChatVariables] = useState<ChatVariable[]>([])
|
const [chatVariables, setChatVariables] = useState<ChatVariable[]>([])
|
||||||
const featuresRef = useRef<FeaturesConfigForm | undefined>(undefined)
|
const featuresRef = useRef<FeaturesConfigForm | undefined>(undefined)
|
||||||
|
const [canUndo, setCanUndo] = useState(false)
|
||||||
|
const [canRedo, setCanRedo] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!graphRef.current) return
|
if (!graphRef.current) return
|
||||||
@@ -473,6 +484,8 @@ export const useWorkflowGraph = ({
|
|||||||
graphRef.current.getNodes().forEach(node => {
|
graphRef.current.getNodes().forEach(node => {
|
||||||
if (node.getData()?.cycle) node.toFront();
|
if (node.getData()?.cycle) node.toFront();
|
||||||
});
|
});
|
||||||
|
graphRef.current.enableHistory()
|
||||||
|
graphRef.current.cleanHistory()
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
@@ -508,6 +521,22 @@ export const useWorkflowGraph = ({
|
|||||||
global: true,
|
global: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
graphRef.current.use(
|
||||||
|
new History({
|
||||||
|
enabled: false,
|
||||||
|
beforeAddCommand(_event, args: any) {
|
||||||
|
const event = args?.key ? `cell:change:${args.key}` : _event;
|
||||||
|
if (event.startsWith('cell:change:') &&
|
||||||
|
event !== 'cell:change:position' &&
|
||||||
|
event !== 'cell:change:source' &&
|
||||||
|
event !== 'cell:change:target') return false;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
graphRef.current.on('history:change', ({ cmds }: { cmds: Command[] }) => {
|
||||||
|
setCanUndo(graphRef.current?.canUndo() ?? false)
|
||||||
|
setCanRedo(graphRef.current?.canRedo() ?? false)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
// 显示/隐藏连接桩
|
// 显示/隐藏连接桩
|
||||||
// const showPorts = (show: boolean) => {
|
// const showPorts = (show: boolean) => {
|
||||||
@@ -1096,6 +1125,9 @@ export const useWorkflowGraph = ({
|
|||||||
graphRef.current.bindKey(['ctrl+v', 'cmd+v'], parseEvent);
|
graphRef.current.bindKey(['ctrl+v', 'cmd+v'], parseEvent);
|
||||||
// Delete selected nodes and edges
|
// Delete selected nodes and edges
|
||||||
graphRef.current.bindKey(['ctrl+d', 'cmd+d', 'delete', 'backspace'], deleteEvent);
|
graphRef.current.bindKey(['ctrl+d', 'cmd+d', 'delete', 'backspace'], deleteEvent);
|
||||||
|
// Undo / Redo
|
||||||
|
graphRef.current.bindKey(['ctrl+z', 'cmd+z'], () => { graphRef.current?.undo(); return false; });
|
||||||
|
graphRef.current.bindKey(['ctrl+y', 'cmd+y', 'ctrl+shift+z', 'cmd+shift+z'], () => { graphRef.current?.redo(); return false; });
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1414,6 +1446,9 @@ export const useWorkflowGraph = ({
|
|||||||
return userVars
|
return userVars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const undo = () => graphRef.current?.undo()
|
||||||
|
const redo = () => graphRef.current?.redo()
|
||||||
|
|
||||||
const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => {
|
const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => {
|
||||||
const { statement = '' } = value?.opening_statement || {}
|
const { statement = '' } = value?.opening_statement || {}
|
||||||
featuresRef.current = value
|
featuresRef.current = value
|
||||||
@@ -1498,5 +1533,9 @@ export const useWorkflowGraph = ({
|
|||||||
handleSaveFeaturesConfig,
|
handleSaveFeaturesConfig,
|
||||||
features: featuresRef.current,
|
features: featuresRef.current,
|
||||||
getStartNodeVariables,
|
getStartNodeVariables,
|
||||||
|
canUndo,
|
||||||
|
canRedo,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
|||||||
handleSaveFeaturesConfig,
|
handleSaveFeaturesConfig,
|
||||||
features,
|
features,
|
||||||
getStartNodeVariables,
|
getStartNodeVariables,
|
||||||
|
canUndo,
|
||||||
|
canRedo,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
} = useWorkflowGraph({ containerRef, miniMapRef, onFeaturesLoad });
|
} = useWorkflowGraph({ containerRef, miniMapRef, onFeaturesLoad });
|
||||||
|
|
||||||
const onDragOver = (event: React.DragEvent) => {
|
const onDragOver = (event: React.DragEvent) => {
|
||||||
@@ -96,6 +100,10 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
|||||||
setIsHandMode={setIsHandMode}
|
setIsHandMode={setIsHandMode}
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
addNotes={handleAddNotes}
|
addNotes={handleAddNotes}
|
||||||
|
canUndo={canUndo}
|
||||||
|
canRedo={canRedo}
|
||||||
|
onUndo={undo}
|
||||||
|
onRedo={redo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user