From 90fe54ca9ef24d1739a6a45bc16cb532d8a3aed6 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 5 May 2026 07:12:26 +0800 Subject: [PATCH] refactor(web): migrate workflow panel context menu primitive (#35787) --- .../__tests__/panel-contextmenu.spec.tsx | 181 +++++++------- .../__tests__/use-panel-interactions.spec.ts | 22 +- .../use-selection-interactions.spec.ts | 2 +- .../workflow/hooks/use-panel-interactions.ts | 6 +- .../components/workflow/panel-contextmenu.tsx | 230 ++++++++++-------- .../__tests__/index.spec.tsx | 6 +- .../__tests__/version-history-item.spec.tsx | 12 +- .../__tests__/action-menu-item.spec.tsx} | 6 +- .../__tests__/index.spec.tsx | 14 +- .../__tests__/use-action-menu.spec.tsx} | 12 +- .../action-menu-item.tsx} | 6 +- .../{context-menu => action-menu}/index.tsx | 24 +- .../use-action-menu.ts} | 8 +- .../panel/version-history-panel/index.tsx | 4 +- .../version-history-item.tsx | 12 +- .../store/__tests__/workflow-store.spec.ts | 2 +- .../workflow/__tests__/panel-slice.spec.ts | 4 +- .../workflow/store/workflow/panel-slice.ts | 4 +- 18 files changed, 279 insertions(+), 276 deletions(-) rename web/app/components/workflow/panel/version-history-panel/{context-menu/__tests__/menu-item.spec.tsx => action-menu/__tests__/action-menu-item.spec.tsx} (89%) rename web/app/components/workflow/panel/version-history-panel/{context-menu => action-menu}/__tests__/index.spec.tsx (75%) rename web/app/components/workflow/panel/version-history-panel/{context-menu/__tests__/use-context-menu.spec.tsx => action-menu/__tests__/use-action-menu.spec.tsx} (83%) rename web/app/components/workflow/panel/version-history-panel/{context-menu/menu-item.tsx => action-menu/action-menu-item.tsx} (89%) rename web/app/components/workflow/panel/version-history-panel/{context-menu => action-menu}/index.tsx (68%) rename web/app/components/workflow/panel/version-history-panel/{context-menu/use-context-menu.ts => action-menu/use-action-menu.ts} (89%) diff --git a/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx b/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx index 2c9c457245..6250ba45bd 100644 --- a/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx +++ b/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx @@ -1,107 +1,63 @@ -import type { ReactNode } from 'react' -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import PanelContextmenu from '../panel-contextmenu' +import { BlockEnum } from '../types' +import { createNode } from './fixtures' +import { renderWorkflowFlowComponent } from './workflow-test-env' -const mockUseClickAway = vi.hoisted(() => vi.fn()) const mockUseTranslation = vi.hoisted(() => vi.fn()) -const mockUseStore = vi.hoisted(() => vi.fn()) const mockUseNodesInteractions = vi.hoisted(() => vi.fn()) const mockUsePanelInteractions = vi.hoisted(() => vi.fn()) const mockUseWorkflowStartRun = vi.hoisted(() => vi.fn()) const mockUseWorkflowMoveMode = vi.hoisted(() => vi.fn()) const mockUseOperator = vi.hoisted(() => vi.fn()) const mockUseDSL = vi.hoisted(() => vi.fn()) - -vi.mock('ahooks', () => ({ - useClickAway: (...args: unknown[]) => mockUseClickAway(...args), -})) +const mockUseNodesReadOnly = vi.hoisted(() => vi.fn()) +const mockUseAvailableBlocks = vi.hoisted(() => vi.fn()) +const mockUseNodesMetaData = vi.hoisted(() => vi.fn()) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) vi.mock('react-i18next', () => ({ useTranslation: () => mockUseTranslation(), })) -vi.mock('@/app/components/workflow/store', () => ({ - useStore: (selector: (state: { - panelMenu?: { left: number, top: number } - clipboardElements: unknown[] - pendingComment: null | { pageX: number, pageY: number, elementX: number, elementY: number } - setCommentPlacing: (placing: boolean) => void - setCommentQuickAdd: (quickAdd: boolean) => void - setShowImportDSLModal: (visible: boolean) => void - }) => unknown) => mockUseStore(selector), -})) - vi.mock('@/app/components/workflow/hooks', () => ({ - useNodesInteractions: () => mockUseNodesInteractions(), - usePanelInteractions: () => mockUsePanelInteractions(), - useWorkflowStartRun: () => mockUseWorkflowStartRun(), - useWorkflowMoveMode: () => mockUseWorkflowMoveMode(), + useAvailableBlocks: () => mockUseAvailableBlocks(), useDSL: () => mockUseDSL(), + useIsChatMode: () => mockUseIsChatMode(), + useNodesInteractions: () => mockUseNodesInteractions(), + useNodesMetaData: () => mockUseNodesMetaData(), + useNodesReadOnly: () => mockUseNodesReadOnly(), + usePanelInteractions: () => mockUsePanelInteractions(), + useWorkflowMoveMode: () => mockUseWorkflowMoveMode(), + useWorkflowStartRun: () => mockUseWorkflowStartRun(), })) vi.mock('@/app/components/workflow/operator/hooks', () => ({ useOperator: () => mockUseOperator(), })) -vi.mock('@/app/components/workflow/operator/add-block', () => ({ - __esModule: true, - default: ({ renderTrigger }: { renderTrigger: () => ReactNode }) => ( -
{renderTrigger()}
- ), -})) - -vi.mock('@/app/components/base/divider', () => ({ - __esModule: true, - default: ({ className }: { className?: string }) =>
, -})) - -vi.mock('@/app/components/workflow/shortcuts-name', () => ({ - __esModule: true, - default: ({ keys }: { keys: string[] }) => {keys.join('+')}, -})) - describe('PanelContextmenu', () => { const mockHandleNodesPaste = vi.fn() const mockHandlePaneContextmenuCancel = vi.fn() const mockHandleStartWorkflowRun = vi.fn() + const mockHandleWorkflowStartRunInChatflow = vi.fn() const mockHandleAddNote = vi.fn() const mockExportCheck = vi.fn() - const mockSetShowImportDSLModal = vi.fn() - const mockSetCommentPlacing = vi.fn() - const mockSetCommentQuickAdd = vi.fn() - let panelMenu: { left: number, top: number } | undefined - let clipboardElements: unknown[] - let pendingComment: null | { pageX: number, pageY: number, elementX: number, elementY: number } - let clickAwayHandler: (() => void) | undefined + const defaultNodesMetaDataMap = { + [BlockEnum.Answer]: { + defaultValue: { + title: 'Answer', + desc: '', + type: BlockEnum.Answer, + }, + }, + } beforeEach(() => { vi.clearAllMocks() - panelMenu = undefined - clipboardElements = [] - pendingComment = null - clickAwayHandler = undefined - - mockUseClickAway.mockImplementation((handler: () => void) => { - clickAwayHandler = handler - }) mockUseTranslation.mockReturnValue({ t: (key: string) => key, }) - mockUseStore.mockImplementation((selector: (state: { - panelMenu?: { left: number, top: number } - clipboardElements: unknown[] - pendingComment: null | { pageX: number, pageY: number, elementX: number, elementY: number } - setCommentPlacing: (placing: boolean) => void - setCommentQuickAdd: (quickAdd: boolean) => void - setShowImportDSLModal: (visible: boolean) => void - }) => unknown) => selector({ - panelMenu, - clipboardElements, - pendingComment, - setCommentPlacing: mockSetCommentPlacing, - setCommentQuickAdd: mockSetCommentQuickAdd, - setShowImportDSLModal: mockSetShowImportDSLModal, - })) mockUseNodesInteractions.mockReturnValue({ handleNodesPaste: mockHandleNodesPaste, }) @@ -110,6 +66,7 @@ describe('PanelContextmenu', () => { }) mockUseWorkflowStartRun.mockReturnValue({ handleStartWorkflowRun: mockHandleStartWorkflowRun, + handleWorkflowStartRunInChatflow: mockHandleWorkflowStartRunInChatflow, }) mockUseWorkflowMoveMode.mockReturnValue({ isCommentModeAvailable: false, @@ -120,50 +77,86 @@ describe('PanelContextmenu', () => { mockUseDSL.mockReturnValue({ exportCheck: mockExportCheck, }) + mockUseNodesReadOnly.mockReturnValue({ + nodesReadOnly: false, + }) + mockUseAvailableBlocks.mockReturnValue({ + availableNextBlocks: [BlockEnum.Answer], + }) + mockUseNodesMetaData.mockReturnValue({ + nodesMap: defaultNodesMetaDataMap, + }) + mockUseIsChatMode.mockReturnValue(false) }) it('should stay hidden when the panel menu is absent', () => { - render() + renderWorkflowFlowComponent() - expect(screen.queryByTestId('add-block')).not.toBeInTheDocument() + expect(screen.queryByText('common.addBlock')).not.toBeInTheDocument() }) - it('should keep paste disabled when the clipboard is empty', () => { - panelMenu = { left: 24, top: 48 } - - render() + it('should keep paste disabled when the clipboard is empty', async () => { + renderWorkflowFlowComponent(, { + initialStoreState: { + panelMenu: { clientX: 24, clientY: 48 }, + }, + hooksStoreProps: {}, + }) + await screen.findByText('common.pasteHere') fireEvent.click(screen.getByText('common.pasteHere')) expect(mockHandleNodesPaste).not.toHaveBeenCalled() expect(mockHandlePaneContextmenuCancel).not.toHaveBeenCalled() }) - it('should render actions, position the menu, and execute each action', () => { - panelMenu = { left: 24, top: 48 } - clipboardElements = [{ id: 'copied-node' }] - const { container } = render() - - expect(screen.getByTestId('add-block')).toHaveTextContent('common.addBlock') - expect(screen.getByRole('button', { name: /common\.run/i })).toHaveTextContent(/Alt\s*R/) - expect(screen.getByRole('button', { name: /common\.pasteHere/i })).toHaveTextContent(/Ctrl\s*V/) - expect(container.firstChild).toHaveStyle({ - left: '24px', - top: '48px', + it('should render actions and execute enabled actions', async () => { + const { store } = renderWorkflowFlowComponent(, { + initialStoreState: { + panelMenu: { clientX: 24, clientY: 48 }, + clipboardElements: [createNode({ id: 'copied-node' })], + }, + hooksStoreProps: {}, }) + expect(await screen.findByText('common.addBlock')).toBeInTheDocument() + expect(screen.getByText('common.run')).toBeInTheDocument() + expect(screen.getByText('common.pasteHere')).toBeInTheDocument() + fireEvent.click(screen.getByText('nodes.note.addNote')) fireEvent.click(screen.getByText('common.run')) fireEvent.click(screen.getByText('common.pasteHere')) fireEvent.click(screen.getByText('export')) fireEvent.click(screen.getByText('importApp')) - clickAwayHandler?.() - expect(mockHandleAddNote).toHaveBeenCalledTimes(1) - expect(mockHandleStartWorkflowRun).toHaveBeenCalledTimes(1) - expect(mockHandleNodesPaste).toHaveBeenCalledTimes(1) - expect(mockExportCheck).toHaveBeenCalledTimes(1) - expect(mockSetShowImportDSLModal).toHaveBeenCalledWith(true) - expect(mockHandlePaneContextmenuCancel).toHaveBeenCalledTimes(4) + await waitFor(() => { + expect(mockHandleAddNote).toHaveBeenCalledTimes(1) + expect(mockHandleStartWorkflowRun).toHaveBeenCalledTimes(1) + expect(mockHandleNodesPaste).toHaveBeenCalledTimes(1) + expect(mockExportCheck).toHaveBeenCalledTimes(1) + expect(store.getState().showImportDSLModal).toBe(true) + }) + }) + + it('should render preview action in chat mode', async () => { + mockUseIsChatMode.mockReturnValue(true) + + renderWorkflowFlowComponent(, { + initialStoreState: { + panelMenu: { clientX: 24, clientY: 48 }, + }, + hooksStoreProps: {}, + }) + + expect(await screen.findByText('common.debugAndPreview')).toBeInTheDocument() + expect(screen.queryByText('common.run')).not.toBeInTheDocument() + + fireEvent.click(screen.getByText('common.debugAndPreview')) + + await waitFor(() => { + expect(mockHandleWorkflowStartRunInChatflow).toHaveBeenCalledTimes(1) + expect(mockHandleStartWorkflowRun).not.toHaveBeenCalled() + expect(mockHandlePaneContextmenuCancel).toHaveBeenCalled() + }) }) }) diff --git a/web/app/components/workflow/hooks/__tests__/use-panel-interactions.spec.ts b/web/app/components/workflow/hooks/__tests__/use-panel-interactions.spec.ts index 6b35c511c1..8452087b7c 100644 --- a/web/app/components/workflow/hooks/__tests__/use-panel-interactions.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-panel-interactions.spec.ts @@ -36,7 +36,7 @@ describe('usePanelInteractions', () => { container.remove() }) - it('handlePaneContextMenu should set panelMenu with computed coordinates when container exists', () => { + it('handlePaneContextMenu should set panelMenu with viewport coordinates', () => { const { result, store } = renderWorkflowHook(() => usePanelInteractions(), { initialStoreState: { nodeMenu: { clientX: 40, clientY: 20, nodeId: 'n1' }, @@ -54,28 +54,14 @@ describe('usePanelInteractions', () => { expect(preventDefault).toHaveBeenCalled() expect(store.getState().panelMenu).toEqual({ - top: 200, - left: 250, + clientX: 350, + clientY: 250, }) expect(store.getState().nodeMenu).toBeUndefined() expect(store.getState().selectionMenu).toBeUndefined() expect(store.getState().edgeMenu).toBeUndefined() }) - it('handlePaneContextMenu should throw when container does not exist', () => { - container.remove() - - const { result } = renderWorkflowHook(() => usePanelInteractions()) - - expect(() => { - result.current.handlePaneContextMenu({ - preventDefault: vi.fn(), - clientX: 350, - clientY: 250, - } as unknown as React.MouseEvent) - }).toThrow() - }) - it('handlePaneContextMenu should sync clipboard from navigator clipboard', async () => { const clipboardNode = createNode({ id: 'clipboard-node' }) const clipboardEdge = createEdge({ @@ -106,7 +92,7 @@ describe('usePanelInteractions', () => { it('handlePaneContextmenuCancel should clear panelMenu', () => { const { result, store } = renderWorkflowHook(() => usePanelInteractions(), { - initialStoreState: { panelMenu: { top: 10, left: 20 } }, + initialStoreState: { panelMenu: { clientX: 20, clientY: 10 } }, }) result.current.handlePaneContextmenuCancel() diff --git a/web/app/components/workflow/hooks/__tests__/use-selection-interactions.spec.ts b/web/app/components/workflow/hooks/__tests__/use-selection-interactions.spec.ts index bb35abf743..894c40c4f6 100644 --- a/web/app/components/workflow/hooks/__tests__/use-selection-interactions.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-selection-interactions.spec.ts @@ -174,7 +174,7 @@ describe('useSelectionInteractions', () => { it('handleSelectionContextMenu should set menu only when clicking on selection rect', () => { const { result, store } = renderSelectionInteractions({ nodeMenu: { clientX: 20, clientY: 10, nodeId: 'n1' }, - panelMenu: { top: 30, left: 40 }, + panelMenu: { clientX: 40, clientY: 30 }, edgeMenu: { clientX: 320, clientY: 180, edgeId: 'e1' }, }) diff --git a/web/app/components/workflow/hooks/use-panel-interactions.ts b/web/app/components/workflow/hooks/use-panel-interactions.ts index 3b7a2158a1..687a80613d 100644 --- a/web/app/components/workflow/hooks/use-panel-interactions.ts +++ b/web/app/components/workflow/hooks/use-panel-interactions.ts @@ -22,15 +22,13 @@ export const usePanelInteractions = () => { workflowStore.getState().setClipboardData({ nodes, edges }) }) - const container = document.querySelector('#workflow-container') - const { x, y } = container!.getBoundingClientRect() workflowStore.setState({ nodeMenu: undefined, selectionMenu: undefined, edgeMenu: undefined, panelMenu: { - top: e.clientY - y, - left: e.clientX - x, + clientX: e.clientX, + clientY: e.clientY, }, }) }, [workflowStore, appDslVersion]) diff --git a/web/app/components/workflow/panel-contextmenu.tsx b/web/app/components/workflow/panel-contextmenu.tsx index 5bac2e2364..3c2207adab 100644 --- a/web/app/components/workflow/panel-contextmenu.tsx +++ b/web/app/components/workflow/panel-contextmenu.tsx @@ -1,13 +1,20 @@ import { cn } from '@langgenius/dify-ui/cn' -import { useClickAway } from 'ahooks' +import { + ContextMenu, + ContextMenuContent, + ContextMenuGroup, + ContextMenuItem, + ContextMenuSeparator, +} from '@langgenius/dify-ui/context-menu' import { memo, - useRef, + useCallback, + useMemo, } from 'react' import { useTranslation } from 'react-i18next' -import Divider from '../base/divider' import { useDSL, + useIsChatMode, useNodesInteractions, usePanelInteractions, useWorkflowMoveMode, @@ -20,7 +27,6 @@ import { useStore } from './store' const PanelContextmenu = () => { const { t } = useTranslation() - const ref = useRef(null) const panelMenu = useStore(s => s.panelMenu) const clipboardElements = useStore(s => s.clipboardElements) const setShowImportDSLModal = useStore(s => s.setShowImportDSLModal) @@ -29,127 +35,147 @@ const PanelContextmenu = () => { const setCommentQuickAdd = useStore(s => s.setCommentQuickAdd) const { handleNodesPaste } = useNodesInteractions() const { handlePaneContextmenuCancel } = usePanelInteractions() - const { handleStartWorkflowRun } = useWorkflowStartRun() + const { + handleStartWorkflowRun, + handleWorkflowStartRunInChatflow, + } = useWorkflowStartRun() const { handleAddNote } = useOperator() const { isCommentModeAvailable } = useWorkflowMoveMode() const { exportCheck } = useDSL() + const isChatMode = useIsChatMode() + const panelMenuClientX = panelMenu?.clientX + const panelMenuClientY = panelMenu?.clientY - useClickAway(() => { - handlePaneContextmenuCancel() - }, ref) + const anchor = useMemo(() => { + if (panelMenuClientX === undefined || panelMenuClientY === undefined) + return null - const renderTrigger = () => { + return { + getBoundingClientRect: () => DOMRect.fromRect({ + width: 0, + height: 0, + x: panelMenuClientX, + y: panelMenuClientY, + }), + } + }, [panelMenuClientX, panelMenuClientY]) + + const renderAddBlockTrigger = useCallback(() => { return ( ) - } + }, [t]) - if (!panelMenu) + const handleRunAction = useCallback(() => { + if (isChatMode) + handleWorkflowStartRunInChatflow() + else + handleStartWorkflowRun() + + handlePaneContextmenuCancel() + }, [isChatMode, handleWorkflowStartRunInChatflow, handleStartWorkflowRun, handlePaneContextmenuCancel]) + + if (!panelMenu || !anchor) return null return ( -
!open && handlePaneContextmenuCancel()} > -
- - - {isCommentModeAvailable && ( - - )} - -
- -
- -
- -
- - -
-
+ + {isChatMode ? t('common.debugAndPreview', { ns: 'workflow' }) : t('common.run', { ns: 'workflow' })} + {!isChatMode && } + + + + + { + if (clipboardElements.length) { + handleNodesPaste() + handlePaneContextmenuCancel() + } + }} + > + {t('common.pasteHere', { ns: 'workflow' })} + + + + + + exportCheck?.()} + > + {t('export', { ns: 'app' })} + + setShowImportDSLModal(true)} + > + {t('importApp', { ns: 'app' })} + + + + ) } diff --git a/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx index 3d73d5a61c..3c9f7dba0e 100644 --- a/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx @@ -49,7 +49,7 @@ type MockRestoreConfirmModalProps = { type MockVersionHistoryItemProps = { item: VersionHistory onClick: (item: VersionHistory) => void - handleClickMenuItem: (operation: VersionHistoryContextMenuOptions) => void + handleClickActionMenuItem: (operation: VersionHistoryContextMenuOptions) => void } vi.mock('@/context/app-context', () => ({ @@ -148,7 +148,7 @@ vi.mock('@/app/components/app/app-publisher/version-info-modal', () => ({ vi.mock('../version-history-item', () => ({ default: (props: MockVersionHistoryItemProps) => { const MockVersionHistoryItem = () => { - const { item, onClick, handleClickMenuItem } = props + const { item, onClick, handleClickActionMenuItem } = props useEffect(() => { if (item.version === WorkflowVersion.Draft) @@ -159,7 +159,7 @@ vi.mock('../version-history-item', () => ({
{item.version !== WorkflowVersion.Draft && ( - )} diff --git a/web/app/components/workflow/panel/version-history-panel/__tests__/version-history-item.spec.tsx b/web/app/components/workflow/panel/version-history-panel/__tests__/version-history-item.spec.tsx index 2518b06c5c..545becd1cf 100644 --- a/web/app/components/workflow/panel/version-history-panel/__tests__/version-history-item.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/__tests__/version-history-item.spec.tsx @@ -60,7 +60,7 @@ describe('VersionHistoryItem', () => { currentVersion={null} latestVersionId="latest-version" onClick={onClick} - handleClickMenuItem={vi.fn()} + handleClickActionMenuItem={vi.fn()} isLast={false} />, ) @@ -81,7 +81,7 @@ describe('VersionHistoryItem', () => { describe('Published Items', () => { it('should open the context menu for a latest named version and forward restore', async () => { const user = userEvent.setup() - const handleClickMenuItem = vi.fn() + const handleClickActionMenuItem = vi.fn() const onClick = vi.fn() render( @@ -90,7 +90,7 @@ describe('VersionHistoryItem', () => { currentVersion={null} latestVersionId="version-1" onClick={onClick} - handleClickMenuItem={handleClickMenuItem} + handleClickActionMenuItem={handleClickActionMenuItem} isLast={false} />, ) @@ -120,8 +120,8 @@ describe('VersionHistoryItem', () => { fireEvent.click(restoreItem) - expect(handleClickMenuItem).toHaveBeenCalledTimes(1) - expect(handleClickMenuItem).toHaveBeenCalledWith( + expect(handleClickActionMenuItem).toHaveBeenCalledTimes(1) + expect(handleClickActionMenuItem).toHaveBeenCalledWith( VersionHistoryContextMenuOptions.restore, VersionHistoryContextMenuOptions.restore, ) @@ -138,7 +138,7 @@ describe('VersionHistoryItem', () => { currentVersion={item} latestVersionId="other-version" onClick={onClick} - handleClickMenuItem={vi.fn()} + handleClickActionMenuItem={vi.fn()} isLast />, ) diff --git a/web/app/components/workflow/panel/version-history-panel/context-menu/__tests__/menu-item.spec.tsx b/web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/action-menu-item.spec.tsx similarity index 89% rename from web/app/components/workflow/panel/version-history-panel/context-menu/__tests__/menu-item.spec.tsx rename to web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/action-menu-item.spec.tsx index 7dfc362a90..8001f74840 100644 --- a/web/app/components/workflow/panel/version-history-panel/context-menu/__tests__/menu-item.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/action-menu-item.spec.tsx @@ -2,9 +2,9 @@ import { DropdownMenu, DropdownMenuContent } from '@langgenius/dify-ui/dropdown- import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { VersionHistoryContextMenuOptions } from '../../../../types' -import MenuItem from '../menu-item' +import ActionMenuItem from '../action-menu-item' -describe('MenuItem', () => { +describe('ActionMenuItem', () => { it('forwards the selected operation and supports destructive styling', async () => { const user = userEvent.setup() const onClick = vi.fn() @@ -12,7 +12,7 @@ describe('MenuItem', () => { render( - { +describe('ActionMenu', () => { it('toggles the trigger and forwards menu clicks', async () => { const user = userEvent.setup() const setOpen = vi.fn() - const handleClickMenuItem = vi.fn() + const handleClickActionMenuItem = vi.fn() renderWorkflowComponent( - , ) @@ -25,11 +25,11 @@ describe('ContextMenu', () => { await user.click(screen.getByText('common.operation.delete')) expect(setOpen).toHaveBeenCalled() - expect(handleClickMenuItem).toHaveBeenCalledWith( + expect(handleClickActionMenuItem).toHaveBeenCalledWith( VersionHistoryContextMenuOptions.restore, VersionHistoryContextMenuOptions.restore, ) - expect(handleClickMenuItem).toHaveBeenCalledWith( + expect(handleClickActionMenuItem).toHaveBeenCalledWith( VersionHistoryContextMenuOptions.delete, VersionHistoryContextMenuOptions.delete, ) diff --git a/web/app/components/workflow/panel/version-history-panel/context-menu/__tests__/use-context-menu.spec.tsx b/web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/use-action-menu.spec.tsx similarity index 83% rename from web/app/components/workflow/panel/version-history-panel/context-menu/__tests__/use-context-menu.spec.tsx rename to web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/use-action-menu.spec.tsx index 084016d74e..e08bc787d7 100644 --- a/web/app/components/workflow/panel/version-history-panel/context-menu/__tests__/use-context-menu.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/use-action-menu.spec.tsx @@ -1,15 +1,15 @@ import { renderWorkflowHook } from '../../../../__tests__/workflow-test-env' import { VersionHistoryContextMenuOptions } from '../../../../types' -import useContextMenu from '../use-context-menu' +import useActionMenu from '../use-action-menu' -describe('useContextMenu', () => { +describe('useActionMenu', () => { it('returns restore, edit, export, copy and delete operations for app workflows', () => { - const { result } = renderWorkflowHook(() => useContextMenu({ + const { result } = renderWorkflowHook(() => useActionMenu({ isNamedVersion: true, isShowDelete: false, open: false, setOpen: vi.fn(), - handleClickMenuItem: vi.fn(), + handleClickActionMenuItem: vi.fn(), })) expect(result.current.deleteOperation).toEqual({ @@ -25,12 +25,12 @@ describe('useContextMenu', () => { }) it('omits export for pipelines and renames the edit action for unnamed versions', () => { - const { result } = renderWorkflowHook(() => useContextMenu({ + const { result } = renderWorkflowHook(() => useActionMenu({ isNamedVersion: false, isShowDelete: true, open: false, setOpen: vi.fn(), - handleClickMenuItem: vi.fn(), + handleClickActionMenuItem: vi.fn(), }), { initialStoreState: { pipelineId: 'pipeline-1', diff --git a/web/app/components/workflow/panel/version-history-panel/context-menu/menu-item.tsx b/web/app/components/workflow/panel/version-history-panel/action-menu/action-menu-item.tsx similarity index 89% rename from web/app/components/workflow/panel/version-history-panel/context-menu/menu-item.tsx rename to web/app/components/workflow/panel/version-history-panel/action-menu/action-menu-item.tsx index 0c0096ab25..7d24e81217 100644 --- a/web/app/components/workflow/panel/version-history-panel/context-menu/menu-item.tsx +++ b/web/app/components/workflow/panel/version-history-panel/action-menu/action-menu-item.tsx @@ -4,7 +4,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenuItem } from '@langgenius/dify-ui/dropdown-menu' import * as React from 'react' -type MenuItemProps = { +type ActionMenuItemProps = { item: { key: VersionHistoryContextMenuOptions name: string @@ -13,7 +13,7 @@ type MenuItemProps = { isDestructive?: boolean } -const MenuItem: FC = ({ +const ActionMenuItem: FC = ({ item, onClick, isDestructive = false, @@ -41,4 +41,4 @@ const MenuItem: FC = ({ ) } -export default React.memo(MenuItem) +export default React.memo(ActionMenuItem) diff --git a/web/app/components/workflow/panel/version-history-panel/context-menu/index.tsx b/web/app/components/workflow/panel/version-history-panel/action-menu/index.tsx similarity index 68% rename from web/app/components/workflow/panel/version-history-panel/context-menu/index.tsx rename to web/app/components/workflow/panel/version-history-panel/action-menu/index.tsx index 1b90166f65..8299ff2b30 100644 --- a/web/app/components/workflow/panel/version-history-panel/context-menu/index.tsx +++ b/web/app/components/workflow/panel/version-history-panel/action-menu/index.tsx @@ -9,23 +9,23 @@ import { import { RiMoreFill } from '@remixicon/react' import * as React from 'react' import { VersionHistoryContextMenuOptions } from '../../../types' -import MenuItem from './menu-item' -import useContextMenu from './use-context-menu' +import ActionMenuItem from './action-menu-item' +import useActionMenu from './use-action-menu' -export type ContextMenuProps = { +export type ActionMenuProps = { isShowDelete: boolean isNamedVersion: boolean open: boolean setOpen: React.Dispatch> - handleClickMenuItem: (operation: VersionHistoryContextMenuOptions) => void + handleClickActionMenuItem: (operation: VersionHistoryContextMenuOptions) => void } -const ContextMenu: FC = (props: ContextMenuProps) => { - const { isShowDelete, handleClickMenuItem, open, setOpen } = props +const ActionMenu: FC = (props: ActionMenuProps) => { + const { isShowDelete, handleClickActionMenuItem, open, setOpen } = props const { deleteOperation, options, - } = useContextMenu(props) + } = useActionMenu(props) return ( = (props: ContextMenuProps) => { > { options.map(option => ( - )) } @@ -55,10 +55,10 @@ const ContextMenu: FC = (props: ContextMenuProps) => { isShowDelete && ( <> - ) @@ -68,4 +68,4 @@ const ContextMenu: FC = (props: ContextMenuProps) => { ) } -export default React.memo(ContextMenu) +export default React.memo(ActionMenu) diff --git a/web/app/components/workflow/panel/version-history-panel/context-menu/use-context-menu.ts b/web/app/components/workflow/panel/version-history-panel/action-menu/use-action-menu.ts similarity index 89% rename from web/app/components/workflow/panel/version-history-panel/context-menu/use-context-menu.ts rename to web/app/components/workflow/panel/version-history-panel/action-menu/use-action-menu.ts index 92d6ee6869..4a81809aeb 100644 --- a/web/app/components/workflow/panel/version-history-panel/context-menu/use-context-menu.ts +++ b/web/app/components/workflow/panel/version-history-panel/action-menu/use-action-menu.ts @@ -1,10 +1,10 @@ -import type { ContextMenuProps } from './index' +import type { ActionMenuProps } from './index' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useStore } from '@/app/components/workflow/store' import { VersionHistoryContextMenuOptions } from '../../../types' -const useContextMenu = (props: ContextMenuProps) => { +const useActionMenu = (props: ActionMenuProps) => { const { isNamedVersion, } = props @@ -43,7 +43,7 @@ const useContextMenu = (props: ContextMenuProps) => { name: t('versionHistory.copyId', { ns: 'workflow' }), }, ] - }, [isNamedVersion, t]) + }, [isNamedVersion, pipelineId, t]) return { deleteOperation, @@ -51,4 +51,4 @@ const useContextMenu = (props: ContextMenuProps) => { } } -export default useContextMenu +export default useActionMenu diff --git a/web/app/components/workflow/panel/version-history-panel/index.tsx b/web/app/components/workflow/panel/version-history-panel/index.tsx index eb1f5c962e..851490fc5a 100644 --- a/web/app/components/workflow/panel/version-history-panel/index.tsx +++ b/web/app/components/workflow/panel/version-history-panel/index.tsx @@ -107,7 +107,7 @@ export const VersionHistoryPanel = ({ setIsOnlyShowNamedVersions(false) }, []) - const handleClickMenuItem = useCallback((item: VersionHistory, operation: VersionHistoryContextMenuOptions) => { + const handleClickActionMenuItem = useCallback((item: VersionHistory, operation: VersionHistoryContextMenuOptions) => { setOperatedItem(item) switch (operation) { case VersionHistoryContextMenuOptions.restore: @@ -292,7 +292,7 @@ export const VersionHistoryPanel = ({ currentVersion={currentVersion} latestVersionId={latestVersionId || ''} onClick={handleVersionClick} - handleClickMenuItem={handleClickMenuItem.bind(null, item)} + handleClickActionMenuItem={handleClickActionMenuItem.bind(null, item)} isLast={isLast} /> ) diff --git a/web/app/components/workflow/panel/version-history-panel/version-history-item.tsx b/web/app/components/workflow/panel/version-history-panel/version-history-item.tsx index bd0a967c49..0984b2654f 100644 --- a/web/app/components/workflow/panel/version-history-panel/version-history-item.tsx +++ b/web/app/components/workflow/panel/version-history-panel/version-history-item.tsx @@ -6,14 +6,14 @@ import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { WorkflowVersion } from '../../types' -import ContextMenu from './context-menu' +import ActionMenu from './action-menu' type VersionHistoryItemProps = { item: VersionHistory currentVersion: VersionHistory | null latestVersionId: string onClick: (item: VersionHistory) => void - handleClickMenuItem: (operation: VersionHistoryContextMenuOptions) => void + handleClickActionMenuItem: (operation: VersionHistoryContextMenuOptions) => void isLast: boolean } @@ -41,7 +41,7 @@ const VersionHistoryItem: React.FC = ({ currentVersion, latestVersionId, onClick, - handleClickMenuItem, + handleClickActionMenuItem, isLast, }) => { const { t } = useTranslation() @@ -122,15 +122,15 @@ const VersionHistoryItem: React.FC = ({ ) }
- {/* Context Menu */} + {/* Action Menu */} {!isDraft && isHovering && (
-
)} diff --git a/web/app/components/workflow/store/__tests__/workflow-store.spec.ts b/web/app/components/workflow/store/__tests__/workflow-store.spec.ts index 47819c049f..4b5dc0b302 100644 --- a/web/app/components/workflow/store/__tests__/workflow-store.spec.ts +++ b/web/app/components/workflow/store/__tests__/workflow-store.spec.ts @@ -108,7 +108,7 @@ describe('createWorkflowStore', () => { ['showWorkflowVersionHistoryPanel', 'setShowWorkflowVersionHistoryPanel', true], ['showInputsPanel', 'setShowInputsPanel', true], ['showDebugAndPreviewPanel', 'setShowDebugAndPreviewPanel', true], - ['panelMenu', 'setPanelMenu', { top: 10, left: 20 }], + ['panelMenu', 'setPanelMenu', { clientX: 20, clientY: 10 }], ['selectionMenu', 'setSelectionMenu', { clientX: 50, clientY: 60 }], ['edgeMenu', 'setEdgeMenu', { clientX: 320, clientY: 180, edgeId: 'e1' }], ['showVariableInspectPanel', 'setShowVariableInspectPanel', true], diff --git a/web/app/components/workflow/store/workflow/__tests__/panel-slice.spec.ts b/web/app/components/workflow/store/workflow/__tests__/panel-slice.spec.ts index 0c7f55850f..1f30a2b7cf 100644 --- a/web/app/components/workflow/store/workflow/__tests__/panel-slice.spec.ts +++ b/web/app/components/workflow/store/workflow/__tests__/panel-slice.spec.ts @@ -19,12 +19,12 @@ describe('createPanelSlice', () => { store.getState().setShowFeaturesPanel(true) store.getState().setShowDebugAndPreviewPanel(true) - store.getState().setPanelMenu({ top: 24, left: 48 }) + store.getState().setPanelMenu({ clientX: 48, clientY: 24 }) store.getState().setEdgeMenu({ clientX: 80, clientY: 120, edgeId: 'edge-1' }) expect(store.getState().showFeaturesPanel).toBe(true) expect(store.getState().showDebugAndPreviewPanel).toBe(true) - expect(store.getState().panelMenu).toEqual({ top: 24, left: 48 }) + expect(store.getState().panelMenu).toEqual({ clientX: 48, clientY: 24 }) expect(store.getState().edgeMenu).toEqual({ clientX: 80, clientY: 120, edgeId: 'edge-1' }) }) }) diff --git a/web/app/components/workflow/store/workflow/panel-slice.ts b/web/app/components/workflow/store/workflow/panel-slice.ts index e84d33a1eb..2f4264fc78 100644 --- a/web/app/components/workflow/store/workflow/panel-slice.ts +++ b/web/app/components/workflow/store/workflow/panel-slice.ts @@ -17,8 +17,8 @@ export type PanelSliceShape = { showUserCursors: boolean setShowUserCursors: (showUserCursors: boolean) => void panelMenu?: { - top: number - left: number + clientX: number + clientY: number } setPanelMenu: (panelMenu: PanelSliceShape['panelMenu']) => void selectionMenu?: {