diff --git a/src/pages/scenario/flowgram/components/minimap.tsx b/src/pages/scenario/flowgram/components/minimap.tsx new file mode 100644 index 00000000..279c5bbc --- /dev/null +++ b/src/pages/scenario/flowgram/components/minimap.tsx @@ -0,0 +1,22 @@ +/** + * Minimap component for workflow editor + * Following Flowgram demo pattern from: + * https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/tools/minimap.tsx + */ +import React from 'react'; +import { Minimap as FlowgramMinimap } from '@flowgram.ai/minimap-plugin'; + +export const Minimap: React.FC = () => { + return ( +
+ +
+ ); +}; diff --git a/src/pages/scenario/flowgram/components/node-panel/index.tsx b/src/pages/scenario/flowgram/components/node-panel/index.tsx new file mode 100644 index 00000000..719f9fd1 --- /dev/null +++ b/src/pages/scenario/flowgram/components/node-panel/index.tsx @@ -0,0 +1,93 @@ +/** + * NodePanel component for node type selection + * Following Flowgram demo pattern from: + * https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/node-panel/index.tsx + */ +import React from 'react'; +import { NodePanelProps } from '@flowgram.ai/free-node-panel-plugin'; +import { Card, Row, Col } from 'antd'; +import { + ApiOutlined, + CodeOutlined, + BranchesOutlined, + ClockCircleOutlined, + SyncOutlined, + PlayCircleOutlined, + StopOutlined, +} from '@ant-design/icons'; +import intl from 'react-intl-universal'; +import { NODE_TYPES } from '../../nodes/constants'; +import './styles.less'; + +export const NodePanel: React.FC = ({ onSelect, onClose }) => { + const nodeTypes = [ + { + type: NODE_TYPES.START, + label: intl.get('scenario_start'), + icon: , + }, + { + type: NODE_TYPES.HTTP, + label: intl.get('scenario_http_node'), + icon: , + }, + { + type: NODE_TYPES.SCRIPT, + label: intl.get('scenario_script_node'), + icon: , + }, + { + type: NODE_TYPES.CONDITION, + label: intl.get('scenario_condition_node'), + icon: , + }, + { + type: NODE_TYPES.DELAY, + label: intl.get('scenario_delay_node'), + icon: , + }, + { + type: NODE_TYPES.LOOP, + label: intl.get('scenario_loop_node'), + icon: , + }, + { + type: NODE_TYPES.END, + label: intl.get('scenario_end'), + icon: , + }, + ]; + + const handleNodeClick = (type: string) => { + onSelect?.({ + nodeType: type, + nodeJSON: {}, + }); + }; + + return ( +
+
+ {intl.get('scenario_add_node')} +
+
+ + {nodeTypes.map((node) => ( + + handleNodeClick(node.type)} + > +
+ {node.icon} +
{node.label}
+
+
+ + ))} +
+
+
+ ); +}; diff --git a/src/pages/scenario/flowgram/components/node-panel/styles.less b/src/pages/scenario/flowgram/components/node-panel/styles.less new file mode 100644 index 00000000..8db6cb02 --- /dev/null +++ b/src/pages/scenario/flowgram/components/node-panel/styles.less @@ -0,0 +1,77 @@ +.node-panel { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + min-width: 320px; + max-width: 400px; + + .node-panel-header { + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + font-weight: 500; + font-size: 14px; + } + + .node-panel-content { + padding: 12px; + max-height: 400px; + overflow-y: auto; + } + + .node-panel-card { + .ant-card-body { + padding: 12px; + } + + .node-panel-card-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + text-align: center; + + .node-panel-card-label { + font-size: 12px; + color: rgba(0, 0, 0, 0.85); + } + } + + &:hover { + border-color: #1890ff; + .node-panel-card-content { + color: #1890ff; + .node-panel-card-label { + color: #1890ff; + } + } + } + } +} + +// Dark mode support +[data-prefers-color='dark'] { + .node-panel { + background: #1f1f1f; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45); + + .node-panel-header { + border-bottom-color: #303030; + } + + .node-panel-card { + background: #141414; + border-color: #303030; + + .node-panel-card-label { + color: rgba(255, 255, 255, 0.85); + } + + &:hover { + border-color: #177ddc; + .node-panel-card-label { + color: #177ddc; + } + } + } + } +} diff --git a/src/pages/scenario/flowgram/hooks/use-add-node.ts b/src/pages/scenario/flowgram/hooks/use-add-node.ts new file mode 100644 index 00000000..968868da --- /dev/null +++ b/src/pages/scenario/flowgram/hooks/use-add-node.ts @@ -0,0 +1,110 @@ +/** + * Hook for adding new workflow nodes + * Following Flowgram demo pattern from: + * https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/add-node/use-add-node.ts + */ +import { useCallback } from 'react'; +import { + useService, + WorkflowDocument, + usePlayground, + PositionSchema, + WorkflowNodeEntity, + WorkflowSelectService, + getAntiOverlapPosition, + WorkflowNodeMeta, + FlowNodeBaseType, +} from '@flowgram.ai/free-layout-editor'; +import { NodePanelResult, WorkflowNodePanelService } from '@flowgram.ai/free-node-panel-plugin'; + +// Hook to get panel position from mouse event +const useGetPanelPosition = () => { + const playground = usePlayground(); + return useCallback( + (targetBoundingRect: DOMRect): PositionSchema => + playground.config.getPosFromMouseEvent({ + clientX: targetBoundingRect.left + 64, + clientY: targetBoundingRect.top - 7, + }), + [playground] + ); +}; + +// Hook to handle node selection +const useSelectNode = () => { + const selectService = useService(WorkflowSelectService); + return useCallback( + (node?: WorkflowNodeEntity) => { + if (!node) { + return; + } + selectService.selectNode(node); + }, + [selectService] + ); +}; + +const getContainerNode = (selectService: WorkflowSelectService) => { + const { activatedNode } = selectService; + if (!activatedNode) { + return; + } + const { isContainer } = activatedNode.getNodeMeta(); + if (isContainer) { + return activatedNode; + } + const parentNode = activatedNode.parent; + if (!parentNode || parentNode.flowNodeType === FlowNodeBaseType.ROOT) { + return; + } + return parentNode; +}; + +// Main hook for adding new nodes +export const useAddNode = () => { + const workflowDocument = useService(WorkflowDocument); + const nodePanelService = useService(WorkflowNodePanelService); + const selectService = useService(WorkflowSelectService); + const playground = usePlayground(); + const getPanelPosition = useGetPanelPosition(); + const select = useSelectNode(); + + return useCallback( + async (targetBoundingRect: DOMRect): Promise => { + const panelPosition = getPanelPosition(targetBoundingRect); + const containerNode = getContainerNode(selectService); + + await new Promise((resolve) => { + nodePanelService.callNodePanel({ + position: panelPosition, + enableMultiAdd: true, + containerNode, + panelProps: {}, + onSelect: async (panelParams?: NodePanelResult) => { + if (!panelParams) { + return; + } + const { nodeType, nodeJSON } = panelParams; + const position = Boolean(containerNode) + ? getAntiOverlapPosition(workflowDocument, { + x: 0, + y: 200, + }) + : undefined; + + const node: WorkflowNodeEntity = workflowDocument.createWorkflowNodeByType( + nodeType, + position, + containerNode?.id, + nodeJSON + ); + + select(node); + resolve(); + }, + }); + }); + }, + [workflowDocument, nodePanelService, selectService, playground, getPanelPosition, select] + ); +}; diff --git a/src/pages/scenario/flowgram/hooks/use-editor-props.tsx b/src/pages/scenario/flowgram/hooks/use-editor-props.tsx index 02d42852..090c8522 100644 --- a/src/pages/scenario/flowgram/hooks/use-editor-props.tsx +++ b/src/pages/scenario/flowgram/hooks/use-editor-props.tsx @@ -3,11 +3,11 @@ import { FreeLayoutProps } from '@flowgram.ai/free-layout-editor'; import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin'; import { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin'; import { createFreeNodePanelPlugin } from '@flowgram.ai/free-node-panel-plugin'; -import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin'; import { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin'; import { createHistoryNodePlugin } from '@flowgram.ai/history-node-plugin'; import { FlowNodeRegistry } from '../nodes/http'; import { createToolsPlugin } from '../plugins/tools-plugin'; +import { NodePanel } from '../components/node-panel'; export function useEditorProps( initialData: any, @@ -26,16 +26,10 @@ export function useEditorProps( plugins: () => [ createFreeSnapPlugin({}), createFreeLinesPlugin({}), - createFreeNodePanelPlugin({}), - createHistoryNodePlugin(), - createMinimapPlugin({ - style: { - width: '150px', - height: '100px', - bottom: '20px', - right: '20px', - }, + createFreeNodePanelPlugin({ + renderer: NodePanel, }), + createHistoryNodePlugin({}), createPanelManagerPlugin({ factories: [], layerProps: {}, diff --git a/src/pages/scenario/flowgram/plugins/tools-plugin/DemoTools.tsx b/src/pages/scenario/flowgram/plugins/tools-plugin/DemoTools.tsx index df4f69a4..a68264ce 100644 --- a/src/pages/scenario/flowgram/plugins/tools-plugin/DemoTools.tsx +++ b/src/pages/scenario/flowgram/plugins/tools-plugin/DemoTools.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import { usePlaygroundTools, useHistoryService, - useNodeService, } from '@flowgram.ai/free-layout-editor'; import { DesktopOutlined, @@ -21,13 +20,15 @@ import { } from '@ant-design/icons'; import { Dropdown, Menu, Button, Tooltip } from 'antd'; import intl from 'react-intl-universal'; +import { Minimap } from '../../components/minimap'; +import { useAddNode } from '../../hooks/use-add-node'; import { NODE_TYPES } from '../../nodes/constants'; import './styles.less'; export const DemoTools: React.FC = () => { const playgroundTools = usePlaygroundTools(); const historyService = useHistoryService(); - const nodeService = useNodeService(); + const addNode = useAddNode(); const [zoom, setZoom] = useState(100); const [canUndo, setCanUndo] = useState(false); @@ -37,7 +38,8 @@ export const DemoTools: React.FC = () => { useEffect(() => { if (playgroundTools) { const updateZoom = () => { - const currentZoom = playgroundTools.getZoom(); + // Use playgroundTools.zoom for reading (Feedback #1) + const currentZoom = playgroundTools.zoom; setZoom(Math.round(currentZoom * 100)); }; updateZoom(); @@ -62,22 +64,23 @@ export const DemoTools: React.FC = () => { }, [historyService]); const handleZoom = (value: number) => { - if (playgroundTools) { - playgroundTools.setZoom(value / 100); + if (playgroundTools?.config) { + // Use playgroundTools.config.updateZoom for writing (Feedback #1) + playgroundTools.config.updateZoom(value / 100); } }; const handleZoomIn = () => { - if (playgroundTools) { - const current = playgroundTools.getZoom(); - playgroundTools.setZoom(Math.min(current + 0.1, 2)); + if (playgroundTools?.config) { + const current = playgroundTools.zoom; + playgroundTools.config.updateZoom(Math.min(current + 0.1, 2)); } }; const handleZoomOut = () => { - if (playgroundTools) { - const current = playgroundTools.getZoom(); - playgroundTools.setZoom(Math.max(current - 0.1, 0.5)); + if (playgroundTools?.config) { + const current = playgroundTools.zoom; + playgroundTools.config.updateZoom(Math.max(current - 0.1, 0.5)); } }; @@ -99,14 +102,10 @@ export const DemoTools: React.FC = () => { } }; - const handleAddNode = (type: string) => { - if (nodeService && playgroundTools?.viewport) { - const center = playgroundTools.viewport.getCenter(); - nodeService.createNode({ - type, - position: center, - }); - } + // Use useAddNode hook for node addition (Feedback #2) + const handleAddNodeClick = async (event: React.MouseEvent) => { + const target = event.currentTarget.getBoundingClientRect(); + await addNode(target); }; const zoomMenu = ( @@ -123,19 +122,6 @@ export const DemoTools: React.FC = () => { /> ); - const addNodeMenu = ( - handleAddNode(key)} - items={[ - { key: NODE_TYPES.HTTP, label: intl.get('scenario_http_node'), icon: }, - { key: NODE_TYPES.SCRIPT, label: intl.get('scenario_script_node'), icon: }, - { key: NODE_TYPES.CONDITION, label: intl.get('scenario_condition_node'), icon: }, - { key: NODE_TYPES.DELAY, label: intl.get('scenario_delay_node'), icon: }, - { key: NODE_TYPES.LOOP, label: intl.get('scenario_loop_node'), icon: }, - ]} - /> - ); - return (
@@ -234,6 +220,11 @@ export const DemoTools: React.FC = () => {
+ {/* Minimap component added to toolbar (Feedback #3) */} + + +
+ - +