diff --git a/eslint-suppressions.json b/eslint-suppressions.json index e1c8bda126..c724609e88 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3820,21 +3820,6 @@ "count": 4 } }, - "web/app/components/tools/workflow-tool/confirm-modal/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/tools/workflow-tool/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/tools/workflow-tool/method-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow-app/components/workflow-children.tsx": { "ts/no-explicit-any": { "count": 3 diff --git a/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx index 646a095622..7060e29f95 100644 --- a/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx @@ -9,6 +9,8 @@ import WorkflowToolConfigureButton from '../configure-button' import WorkflowToolAsModal from '../index' import MethodSelector from '../method-selector' +vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover')) + // Mock Next.js navigation const mockPush = vi.fn() vi.mock('@/next/navigation', () => ({ @@ -83,12 +85,11 @@ vi.mock('@/app/components/base/drawer-plus', () => ({ }, })) -// Mock EmojiPicker - simplified for testing -vi.mock('@/app/components/base/emoji-picker', () => ({ - default: ({ onSelect, onClose }: { onSelect: (icon: string, background: string) => void, onClose: () => void }) => ( +// Mock EmojiPickerInner - simplified for testing +vi.mock('@/app/components/base/emoji-picker/Inner', () => ({ + default: ({ onSelect }: { onSelect: (icon: string, background: string) => void }) => (
-
), })) @@ -978,6 +979,7 @@ describe('WorkflowToolAsModal', () => { // Select emoji await user.click(screen.getByTestId('select-emoji')) + await user.click(screen.getByRole('button', { name: 'app.iconPicker.ok' })) // Assert const updatedIcon = screen.getByTestId('app-icon') @@ -1002,7 +1004,7 @@ describe('WorkflowToolAsModal', () => { expect(screen.getByTestId('emoji-picker'))!.toBeInTheDocument() - await user.click(screen.getByTestId('close-emoji-picker')) + await user.click(screen.getByRole('button', { name: 'app.iconPicker.cancel' })) // Assert // Assert @@ -1501,7 +1503,7 @@ describe('MethodSelector', () => { // Assert // Assert - expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() + expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument() }) it('should display parameter method text when value is llm', () => { @@ -1562,11 +1564,11 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) // Assert // Assert - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('popover-content'))!.toBeInTheDocument() }) it('should call onChange with llm when parameter option clicked', async () => { @@ -1580,7 +1582,7 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) const paramOption = screen.getAllByText('tools.createTool.toolInput.methodParameter')[0] await user.click(paramOption!) @@ -1600,7 +1602,7 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) const settingOption = screen.getByText('tools.createTool.toolInput.methodSetting') await user.click(settingOption) @@ -1621,12 +1623,12 @@ describe('MethodSelector', () => { render() // First click - open - await user.click(screen.getByTestId('portal-trigger')) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + await user.click(screen.getByTestId('popover-trigger')) + expect(screen.getByTestId('popover-content'))!.toBeInTheDocument() // Second click - close - await user.click(screen.getByTestId('portal-trigger')) - expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() + await user.click(screen.getByTestId('popover-trigger')) + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) }) @@ -1642,10 +1644,10 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) // Assert - the first option (llm) should have a check icon container - const content = screen.getByTestId('portal-content') + const content = screen.getByTestId('popover-content') expect(content)!.toBeInTheDocument() }) @@ -1659,10 +1661,10 @@ describe('MethodSelector', () => { // Act render() - await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByTestId('popover-trigger')) // Assert - const content = screen.getByTestId('portal-content') + const content = screen.getByTestId('popover-content') expect(content)!.toBeInTheDocument() }) }) diff --git a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx index 2ec289fcf6..9f5532f1f7 100644 --- a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx @@ -18,11 +18,10 @@ vi.mock('@/app/components/base/drawer-plus', () => ({ ), })) -vi.mock('@/app/components/base/emoji-picker', () => ({ - default: ({ onSelect, onClose }: { onSelect: (icon: string, background: string) => void, onClose: () => void }) => ( +vi.mock('@/app/components/base/emoji-picker/Inner', () => ({ + default: ({ onSelect }: { onSelect: (icon: string, background: string) => void }) => (
-
), })) @@ -129,6 +128,7 @@ describe('WorkflowToolAsModal', () => { await user.click(screen.getByTestId('append-label')) await user.click(screen.getByTestId('app-icon')) await user.click(screen.getByTestId('select-emoji')) + await user.click(screen.getByRole('button', { name: 'app.iconPicker.ok' })) await user.click(screen.getByRole('button', { name: 'common.operation.save' })) expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({ @@ -195,6 +195,6 @@ describe('WorkflowToolAsModal', () => { />, ) - expect(screen.getAllByText('tools.createTool.toolOutput.reservedParameterDuplicateTip').length).toBeGreaterThan(0) + expect(screen.getAllByTestId('reserved-output-warning').length).toBeGreaterThan(0) }) }) diff --git a/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx index d1126bf762..19b796f2db 100644 --- a/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/method-selector.spec.tsx @@ -4,6 +4,8 @@ import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import MethodSelector from '../method-selector' +vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover')) + // Test utilities const defaultProps: ComponentProps = { value: 'llm', @@ -139,6 +141,24 @@ describe('MethodSelector', () => { expect(onChange).toHaveBeenCalledWith('form') }) + it('should close dropdown after an option is clicked', async () => { + const user = userEvent.setup() + renderComponent({ value: 'llm' }) + + const trigger = screen.getByText('tools.createTool.toolInput.methodParameter') + await user.click(trigger) + + await waitFor(() => { + expect(screen.getByText('tools.createTool.toolInput.methodSettingTip'))!.toBeInTheDocument() + }) + + await user.click(screen.getByText('tools.createTool.toolInput.methodSettingTip')) + + await waitFor(() => { + expect(screen.queryByText('tools.createTool.toolInput.methodSettingTip')).not.toBeInTheDocument() + }) + }) + it('should toggle dropdown open state', async () => { const user = userEvent.setup() renderComponent() @@ -235,10 +255,9 @@ describe('MethodSelector', () => { await user.click(trigger) await waitFor(() => { + expect(screen.getByTestId('popover-content')).toBeInTheDocument() const dropdown = document.querySelector('.w-\\[320px\\]') expect(dropdown)!.toBeInTheDocument() - expect(dropdown)!.toHaveClass('rounded-lg') - expect(dropdown)!.toHaveClass('shadow-lg') }) }) diff --git a/web/app/components/tools/workflow-tool/confirm-modal/__tests__/index.spec.tsx b/web/app/components/tools/workflow-tool/confirm-modal/__tests__/index.spec.tsx index c5bce8b663..6535564a32 100644 --- a/web/app/components/tools/workflow-tool/confirm-modal/__tests__/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/confirm-modal/__tests__/index.spec.tsx @@ -93,13 +93,12 @@ describe('ConfirmModal', () => { // Arrange & Act renderComponent() - // Assert - Check for the dialog panel with modal content - // The real modal structure has nested divs, we need to find the one with our classes - const dialogContent = document.querySelector('.relative.rounded-2xl') + // Assert + const dialogContent = screen.getByRole('dialog') expect(dialogContent).toBeInTheDocument() - expect(dialogContent).toHaveClass('w-[600px]') - expect(dialogContent).toHaveClass('max-w-[600px]') - expect(dialogContent).toHaveClass('p-8') + expect(dialogContent).toHaveClass('w-[600px]!') + expect(dialogContent).toHaveClass('max-w-[600px]!') + expect(dialogContent).toHaveClass('p-8!') }) }) diff --git a/web/app/components/tools/workflow-tool/confirm-modal/index.tsx b/web/app/components/tools/workflow-tool/confirm-modal/index.tsx index ba45387731..4f17862c1a 100644 --- a/web/app/components/tools/workflow-tool/confirm-modal/index.tsx +++ b/web/app/components/tools/workflow-tool/confirm-modal/index.tsx @@ -2,11 +2,9 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/function' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { useTranslation } from 'react-i18next' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' -import Modal from '@/app/components/base/modal' type ConfirmModalProps = { show: boolean @@ -18,28 +16,29 @@ const ConfirmModal = ({ show, onConfirm, onClose }: ConfirmModalProps) => { const { t } = useTranslation() return ( - -
- -
-
- -
-
{t('createTool.confirmTitle', { ns: 'tools' })}
-
- {t('createTool.confirmTip', { ns: 'tools' })} -
-
-
- - + + +
+
-
- +
+ +
+ {t('createTool.confirmTitle', { ns: 'tools' })} +
+ {t('createTool.confirmTip', { ns: 'tools' })} +
+
+
+ + +
+
+ + ) } diff --git a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts index efbf16d590..8bc3db95da 100644 --- a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts +++ b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts @@ -437,7 +437,6 @@ describe('useConfigureButton', () => { expect(onRefreshData).toHaveBeenCalled() expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled() expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') - expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: expect.any(String) }) expect(result.current.showModal).toBe(false) }) diff --git a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts index 9f0a43635c..33965aa5ee 100644 --- a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts +++ b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts @@ -206,7 +206,6 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { onRefreshData?.() invalidateAllWorkflowTools() invalidateDetail(workflowAppId) - toast.success(t('api.actionSuccess', { ns: 'common' })) setShowModal(false) } catch (e) { diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index 353e85beba..6f8258f185 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -3,18 +3,18 @@ import type { FC } from 'react' import type { Emoji, WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' -import { RiErrorWarningLine } from '@remixicon/react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { produce } from 'immer' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' -import Drawer from '@/app/components/base/drawer-plus' -import EmojiPicker from '@/app/components/base/emoji-picker' +import Divider from '@/app/components/base/divider' +import EmojiPickerInner from '@/app/components/base/emoji-picker/Inner' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' -import Tooltip from '@/app/components/base/tooltip' import LabelSelector from '@/app/components/tools/labels/selector' import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal' import MethodSelector from '@/app/components/tools/workflow-tool/method-selector' @@ -53,6 +53,111 @@ type Props = { workflow_tool_id: string }>) => void } + +type WorkflowToolDrawerProps = { + title: string + onHide: () => void + children: React.ReactNode +} + +const InfoTooltip = ({ children }: { children: React.ReactNode }) => { + return ( + + + )} + /> + +
+ {children} +
+
+
+ ) +} + +const WorkflowToolDrawer = ({ title, onHide, children }: WorkflowToolDrawerProps) => { + return ( + + +
+
+
+ + {title} + + +
+
+
+ {children} +
+
+
+
+ ) +} + +type WorkflowToolEmojiPickerProps = { + onSelect: (icon: string, background: string) => void + onClose: () => void +} + +const WorkflowToolEmojiPicker = ({ onSelect, onClose }: WorkflowToolEmojiPickerProps) => { + const { t } = useTranslation() + const [selectedEmoji, setSelectedEmoji] = useState('') + const [selectedBackground, setSelectedBackground] = useState() + + return ( + + + + {t('iconPicker.emoji', { ns: 'app' })} + + { + setSelectedEmoji(emoji) + setSelectedBackground(background) + }} + /> + +
+ + +
+
+
+ ) +} + // Add and Edit const WorkflowToolAsModal: FC = ({ isAdd, @@ -138,210 +243,201 @@ const WorkflowToolAsModal: FC = ({ return ( <> - -
- {/* name & icon */} -
-
- {t('createTool.name', { ns: 'tools' })} - {' '} - * -
-
- { setShowEmojiPicker(true) }} className="cursor-pointer" iconType="emoji" icon={emoji.content} background={emoji.background} /> - setLabel(e.target.value)} - /> -
+ > +
+
+ {/* name & icon */} +
+
+ {t('createTool.name', { ns: 'tools' })} + {' '} + *
- {/* name for tool call */} -
-
- {t('createTool.nameForToolCall', { ns: 'tools' })} - {' '} - * - - {t('createTool.nameForToolCallPlaceHolder', { ns: 'tools' })} -
- )} - /> -
+
+ { setShowEmojiPicker(true) }} className="cursor-pointer" iconType="emoji" icon={emoji.content} background={emoji.background} /> setName(e.target.value)} - /> - {!isWorkflowToolNameValid(name) && ( -
{t('createTool.nameForToolCallTip', { ns: 'tools' })}
- )} -
- {/* description */} -
-
{t('createTool.description', { ns: 'tools' })}
-