diff --git a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx index fd87ab0aeb..c69dae8152 100644 --- a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx +++ b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx @@ -60,21 +60,6 @@ vi.mock('@/app/components/base/app-icon', () => ({ }) => , })) -vi.mock('@/app/components/base/emoji-picker', () => ({ - default: ({ - onClose, - onSelect, - }: { - onClose: () => void - onSelect: (icon: string, background: string) => void - }) => ( -
- - -
- ), -})) - vi.mock('@/app/components/base/features/new-feature-panel/moderation/form-generation', () => ({ default: ({ onChange, @@ -146,7 +131,14 @@ describe('ExternalDataToolModal', () => { }) fireEvent.click(screen.getByText('pick-extension')) fireEvent.click(screen.getByText('open-emoji-picker')) - fireEvent.click(screen.getByText('select-emoji')) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + const emojiButton = document.querySelector('em-emoji')?.closest('button') + expect(emojiButton).toBeTruthy() + fireEvent.click(emojiButton!) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) fireEvent.click(screen.getByText('operation.save')) await waitFor(() => { @@ -155,8 +147,8 @@ describe('ExternalDataToolModal', () => { api_based_extension_id: 'extension-1', }, enabled: true, - icon: 'sparkles', - icon_background: '#fff', + icon: expect.any(String), + icon_background: '#E4FBCC', label: 'Search', type: 'api', variable: 'search_api', @@ -168,8 +160,8 @@ describe('ExternalDataToolModal', () => { api_based_extension_id: 'extension-1', }, enabled: true, - icon: 'sparkles', - icon_background: '#fff', + icon: expect.any(String), + icon_background: '#E4FBCC', label: 'Search', type: 'api', variable: 'search_api', diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index 09685ccc08..be8271be6e 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -217,13 +217,10 @@ const ExternalDataToolModal: FC = ({ { showEmojiPicker && ( { handleValueChange({ icon, icon_background }) - setShowEmojiPicker(false) - }} - onClose={() => { - handleValueChange({ icon: '', icon_background: '' }) - setShowEmojiPicker(false) }} /> ) diff --git a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx index bf1e698fc6..82ffa880b9 100644 --- a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ import type { App } from '@/types/app' import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' @@ -30,6 +31,7 @@ vi.mock('ahooks', () => ({ })) vi.mock('@/next/navigation', () => ({ useRouter: vi.fn(), + useParams: () => ({}), })) vi.mock('@/utils/create-app-tracking', () => ({ trackCreateApp: vi.fn(), @@ -55,19 +57,6 @@ vi.mock('@/app/components/base/app-icon', () => ({ ), })) -vi.mock('@/app/components/base/app-icon-picker', () => ({ - default: ({ onSelect, onClose }: { onSelect: (payload: Record) => void, onClose: () => void }) => ( -
- - -
- ), -})) vi.mock('@/utils/app-redirection', () => ({ getRedirection: vi.fn(), })) @@ -216,14 +205,22 @@ describe('CreateAppModal', () => { expect(onCreateFromTemplate).toHaveBeenCalled() }) - it('creates a beginner chat app with the keyboard shortcut and selected image icon', async () => { + it('creates a beginner chat app with the keyboard shortcut and selected icon style', async () => { + const user = userEvent.setup() mockCreateApp.mockResolvedValue({ id: 'chat-app', mode: AppModeEnum.CHAT } as App) renderModal() fireEvent.click(screen.getByText('app.newApp.forBeginners')) fireEvent.click(screen.getByText('app.types.chatbot')) - fireEvent.click(screen.getByText('open-icon-picker')) - fireEvent.click(screen.getByText('select-image-icon')) + await user.click(screen.getByText('open-icon-picker')) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: '#E4FBCC' })) + await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) fireEvent.change(screen.getByPlaceholderText('app.newApp.appNamePlaceholder'), { target: { value: 'Keyboard App' }, }) @@ -237,9 +234,9 @@ describe('CreateAppModal', () => { expect(mockCreateApp).toHaveBeenCalledWith({ name: 'Keyboard App', description: 'Created from shortcut', - icon_type: 'image', - icon: 'file-1', - icon_background: undefined, + icon_type: 'emoji', + icon: 'πŸ€–', + icon_background: '#E4FBCC', mode: AppModeEnum.CHAT, }) }) @@ -254,7 +251,8 @@ describe('CreateAppModal', () => { expect(mockCreateApp).not.toHaveBeenCalled() }) - it('ignores the keyboard shortcut when the app quota is exhausted and closes the icon picker', () => { + it('ignores the keyboard shortcut when the app quota is exhausted and closes the icon picker', async () => { + const user = userEvent.setup() mockUseProviderContext.mockReturnValue({ plan: { type: AppModeEnum.ADVANCED_CHAT, @@ -267,11 +265,16 @@ describe('CreateAppModal', () => { renderModal() - fireEvent.click(screen.getByText('open-icon-picker')) - expect(screen.getByText('select-image-icon')).toBeInTheDocument() - fireEvent.click(screen.getByText('close-icon-picker')) + await user.click(screen.getByText('open-icon-picker')) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) - expect(screen.queryByText('select-image-icon')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() ahooksMocks.keyPressHandlers.at(-1)?.() diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 7b42d7908b..f6d6106989 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -220,12 +220,13 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: /> {showAppIconPicker && ( { setAppIcon(payload) - setShowAppIconPicker(false) - }} - onClose={() => { - setShowAppIconPicker(false) }} /> )} diff --git a/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx b/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx index d8473378a0..7c45b97c06 100644 --- a/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import DuplicateAppModal from '../index' @@ -32,15 +32,6 @@ vi.mock('@/app/components/base/app-icon', () => ({ ), })) -vi.mock('@/app/components/base/app-icon-picker', () => ({ - default: ({ onSelect, onClose }: { onSelect: (payload: Record) => void, onClose: () => void }) => ( -
- - -
- ), -})) - describe('DuplicateAppModal', () => { beforeEach(() => { vi.clearAllMocks() @@ -94,14 +85,21 @@ describe('DuplicateAppModal', () => { ) await user.click(screen.getByText('open-icon-picker')) - await user.click(screen.getByText('select-icon')) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: '#E4FBCC' })) + await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) await user.click(screen.getByRole('button', { name: 'duplicate' })) expect(onConfirm).toHaveBeenCalledWith({ name: 'Demo App', - icon_type: 'image', - icon: 'file-1', - icon_background: undefined, + icon_type: 'emoji', + icon: 'πŸ€–', + icon_background: '#E4FBCC', }) expect(onHide).toHaveBeenCalled() }) @@ -127,7 +125,7 @@ describe('DuplicateAppModal', () => { expect(onHide).toHaveBeenCalledTimes(1) }) - it('should restore the original image icon when the picker closes without selecting', async () => { + it('should preserve the current image icon when the picker closes without selecting', async () => { const onConfirm = vi.fn() const user = userEvent.setup() @@ -144,16 +142,32 @@ describe('DuplicateAppModal', () => { ) await user.click(screen.getByText('open-icon-picker')) - await user.click(screen.getByText('select-icon')) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + const emojiButton = document.querySelector('em-emoji')?.closest('button') + expect(emojiButton).toBeTruthy() + await user.click(emojiButton!) + await user.click(screen.getByRole('button', { name: '#E4FBCC' })) + await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) await user.click(screen.getByText('open-icon-picker')) - await user.click(screen.getByText('close-icon-picker')) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) await user.click(screen.getByRole('button', { name: 'duplicate' })) - expect(onConfirm).toHaveBeenCalledWith({ + expect(onConfirm).toHaveBeenCalledWith(expect.objectContaining({ name: 'Image App', - icon_type: 'image', - icon: 'original-file', - icon_background: undefined, - }) + icon_type: 'emoji', + icon: expect.any(String), + icon_background: '#E4FBCC', + })) }) }) diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index 2fb9e5552b..8728223296 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -109,15 +109,13 @@ const DuplicateAppModal = ({ {showAppIconPicker && ( { setAppIcon(payload) - setShowAppIconPicker(false) - }} - onClose={() => { - setAppIcon(icon_type === 'image' - ? { type: 'image', url: icon_url!, fileId: icon } - : { type: 'emoji', icon, background: icon_background! }) - setShowAppIconPicker(false) }} /> )} diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index f829ab62b9..15e20910be 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -478,22 +478,16 @@ const SettingsModal: FC = ({ - {showAppIconPicker && ( -
e.stopPropagation()}> - { - setAppIcon(payload) - setShowAppIconPicker(false) - }} - onClose={() => { - setAppIcon(createAppIcon(appInfo)) - setShowAppIconPicker(false) - }} - /> -
- )} + ) } diff --git a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx index d043046dc8..8e9cca423e 100644 --- a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx @@ -84,18 +84,6 @@ vi.mock('@/app/components/base/app-icon', () => ({ ), })) -vi.mock('@/app/components/base/app-icon-picker', () => ({ - default: ({ onSelect, onClose }: { - onSelect: (payload: { type: 'image', url: string, fileId: string }) => void - onClose: () => void - }) => ( -
- - -
- ), -})) - const createMockApp = (overrides: Partial = {}): App => ({ id: 'app-123', name: 'Demo App', @@ -315,17 +303,23 @@ describe('SwitchAppModal', () => { mockSwitchApp.mockResolvedValueOnce({ new_app_id: 'new-app-003' }) await user.click(screen.getByText('open-icon-picker')) - expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) - await user.click(screen.getByText('select-app-icon')) + await user.click(screen.getByRole('button', { name: '#E4FBCC' })) + await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) await user.click(screen.getByRole('button', { name: 'app.switchStart' })) await waitFor(() => { expect(mockSwitchApp).toHaveBeenCalledWith(expect.objectContaining({ appID: appDetail.id, - icon_type: 'image', - icon: 'file-id-1', - icon_background: undefined, + icon_type: 'emoji', + icon: 'πŸš€', + icon_background: '#E4FBCC', })) }) }) @@ -335,9 +329,14 @@ describe('SwitchAppModal', () => { renderComponent() await user.click(screen.getByText('open-icon-picker')) - expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument() - await user.click(screen.getByText('close-app-icon-picker')) - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() await user.click(screen.getByText('app.removeOriginal')) expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument() diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index ca6c69a75c..d01138ed90 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -149,15 +149,13 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo {showAppIconPicker && ( { setAppIcon(payload) - setShowAppIconPicker(false) - }} - onClose={() => { - setAppIcon(appDetail.icon_type === 'image' - ? { type: 'image' as const, url: appDetail.icon_url, fileId: appDetail.icon } - : { type: 'emoji' as const, icon: appDetail.icon, background: appDetail.icon_background }) - setShowAppIconPicker(false) }} /> )} diff --git a/web/app/components/base/app-icon-picker/__tests__/index.spec.tsx b/web/app/components/base/app-icon-picker/__tests__/index.spec.tsx index 589ad5554e..3f6f205cfb 100644 --- a/web/app/components/base/app-icon-picker/__tests__/index.spec.tsx +++ b/web/app/components/base/app-icon-picker/__tests__/index.spec.tsx @@ -125,11 +125,11 @@ describe('AppIconPicker', () => { const renderPicker = (props: Partial> = {}) => { const onSelect = vi.fn() - const onClose = vi.fn() + const onOpenChange = vi.fn() - const { container } = render() + const { container } = render() - return { onSelect, onClose, container } + return { onSelect, onOpenChange, container } } beforeEach(() => { @@ -157,8 +157,9 @@ describe('AppIconPicker', () => { it('should render emoji and image tabs when upload is enabled', async () => { renderPicker() - expect(await screen.findByText(/emoji/i))!.toBeInTheDocument() - expect(screen.getByText(/image/i))!.toBeInTheDocument() + expect(screen.getByRole('dialog', { name: /emoji/i })).toBeInTheDocument() + expect(await screen.findByRole('button', { name: /emoji/i }))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: /image/i }))!.toBeInTheDocument() expect(screen.getByText(/cancel/i))!.toBeInTheDocument() expect(screen.getByText(/ok/i))!.toBeInTheDocument() }) @@ -173,12 +174,12 @@ describe('AppIconPicker', () => { }) describe('User Interactions', () => { - it('should call onClose when cancel is clicked', async () => { - const { onClose } = renderPicker() + it('should close when cancel is clicked', async () => { + const { onOpenChange } = renderPicker() await userEvent.click(screen.getByText(/cancel/i)) - expect(onClose).toHaveBeenCalledTimes(1) + expect(onOpenChange).toHaveBeenCalledWith(false) }) it('should switch between emoji and image tabs', async () => { @@ -187,7 +188,7 @@ describe('AppIconPicker', () => { await userEvent.click(screen.getByText(/image/i)) expect(screen.getByText(/drop.*here/i))!.toBeInTheDocument() - await userEvent.click(screen.getByText(/emoji/i)) + await userEvent.click(screen.getByRole('button', { name: /emoji/i })) expect(screen.getByPlaceholderText(/search/i))!.toBeInTheDocument() }) @@ -214,6 +215,14 @@ describe('AppIconPicker', () => { }) }) + it('should close through the dialog open change contract when Escape is pressed', async () => { + const { onOpenChange } = renderPicker() + + await userEvent.keyboard('{Escape}') + + expect(onOpenChange).toHaveBeenCalledWith(false, expect.anything()) + }) + it('should not call onSelect when no emoji has been selected', async () => { const { onSelect } = renderPicker() diff --git a/web/app/components/base/app-icon-picker/index.stories.tsx b/web/app/components/base/app-icon-picker/index.stories.tsx index 3792e7b358..465428aa8a 100644 --- a/web/app/components/base/app-icon-picker/index.stories.tsx +++ b/web/app/components/base/app-icon-picker/index.stories.tsx @@ -48,20 +48,20 @@ const AppIconPickerDemo = () => { - {open && ( - { - setSelection(result) - setOpen(false) - }} - onClose={() => setOpen(false)} - /> - )} + ) } export const Playground: Story = { + args: { + open: false, + onOpenChange: () => {}, + }, render: () => , parameters: { docs: { @@ -74,15 +74,11 @@ const [selection, setSelection] = useState(null) return ( <> - {open && ( - { - setSelection(result) - setOpen(false) - }} - onClose={() => setOpen(false)} - /> - )} + ) `.trim(), diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index 9fcf9faf43..2418b3bc95 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -1,15 +1,15 @@ -import type { FC } from 'react' import type { Area } from 'react-easy-crop' import type { OnImageInput } from './ImageInput' import type { AppIconType, ImageFile } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { RiImageCircleAiLine } from '@remixicon/react' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' import Divider from '../divider' +import { defaultEmojiBackground } from '../emoji-picker/constants' import EmojiPickerInner from '../emoji-picker/Inner' import { useLocalFileUploader } from '../image-uploader/hooks' import ImageInput from './ImageInput' @@ -31,8 +31,10 @@ export type AppIconImageSelection = { export type AppIconSelection = AppIconEmojiSelection | AppIconImageSelection type AppIconPickerProps = { + open: boolean + onOpenChange: (open: boolean) => void onSelect?: (payload: AppIconSelection) => void - onClose?: () => void + enableImageUpload?: boolean initialEmoji?: { icon: string background?: string | null @@ -40,11 +42,50 @@ type AppIconPickerProps = { className?: string } -const AppIconPicker: FC = ({ +function AppIconPicker({ + open, + onOpenChange, onSelect, - onClose, + enableImageUpload = true, initialEmoji, -}) => { + className, +}: AppIconPickerProps) { + return ( + + {open + ? ( + + ) + : null} + + ) +} + +type AppIconPickerContentProps = { + className?: string + initialEmoji?: { + icon: string + background?: string | null + } + enableImageUpload: boolean + onOpenChange: (open: boolean) => void + onSelect?: (payload: AppIconSelection) => void +} + +function AppIconPickerContent({ + className, + initialEmoji, + enableImageUpload, + onOpenChange, + onSelect, +}: AppIconPickerContentProps) { const { t } = useTranslation() const tabs = [ @@ -52,11 +93,17 @@ const AppIconPicker: FC = ({ { key: 'image', label: t('iconPicker.image', { ns: 'app' }), icon: }, ] const [activeTab, setActiveTab] = useState('emoji') + const showImageUpload = enableImageUpload && !DISABLE_UPLOAD_IMAGE_AS_ICON - const [emoji, setEmoji] = useState<{ emoji: string, background: string }>() - const handleSelectEmoji = useCallback((emoji: string, background: string) => { - setEmoji({ emoji, background }) - }, [setEmoji]) + const [emoji, setEmoji] = useState<{ emoji: string, background: string } | undefined>(() => { + if (!initialEmoji?.icon) + return undefined + + return { + emoji: initialEmoji.icon, + background: initialEmoji.background ?? defaultEmojiBackground, + } + }) const [uploading, setUploading] = useState() @@ -71,6 +118,7 @@ const AppIconPicker: FC = ({ fileId: imageFile.fileId, url: imageFile.url, }) + onOpenChange(false) } }, }) @@ -94,6 +142,7 @@ const AppIconPicker: FC = ({ icon: emoji.emoji, background: emoji.background, }) + onOpenChange(false) } } else { @@ -111,54 +160,55 @@ const AppIconPicker: FC = ({ } return ( - - + + + {t('iconPicker.emoji', { ns: 'app' })} + - {!DISABLE_UPLOAD_IMAGE_AS_ICON && ( -
-
- {tabs.map(tab => ( - - ))} -
+ {tab.label} + + ))}
- )} - - {activeTab === 'emoji' && ( - - )} - {activeTab === 'image' && } - - -
- - -
-
-
+ )} + + {activeTab === 'emoji' && ( + setEmoji({ emoji, background })} + /> + )} + {activeTab === 'image' && } + + +
+ + + +
+ ) } diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index 2c727aa767..5a41d4d9d0 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -1,6 +1,6 @@ 'use client' import type { EmojiMartData } from '@emoji-mart/data' -import type { ChangeEvent, FC } from 'react' +import type { ChangeEvent } from 'react' import data from '@emoji-mart/data' import { MagnifyingGlassIcon, @@ -12,31 +12,10 @@ import { useState } from 'react' import Divider from '@/app/components/base/divider' import Input from '@/app/components/base/input' import { searchEmoji } from '@/utils/emoji' +import { backgroundColors, defaultEmojiBackground } from './constants' init({ data }) -const backgroundColors = [ - '#FFEAD5', - '#E4FBCC', - '#D3F8DF', - '#E0F2FE', - - '#E0EAFF', - '#EFF1F5', - '#FBE8FF', - '#FCE7F6', - - '#FEF7C3', - '#E6F4D7', - '#D5F5F6', - '#D1E9FF', - - '#D1E0FF', - '#D5D9EB', - '#ECE9FE', - '#FFE4E8', -] - type IEmojiPickerInnerProps = { emoji?: string background?: string @@ -44,28 +23,32 @@ type IEmojiPickerInnerProps = { className?: string } -const EmojiPickerInner: FC = ({ +function EmojiPickerInner({ emoji, background, onSelect, className, -}) => { +}: IEmojiPickerInnerProps) { const { categories } = data as EmojiMartData const [selectedEmoji, setSelectedEmoji] = useState(emoji || '') - const [selectedBackground, setSelectedBackground] = useState(background || backgroundColors[0]) + const [selectedBackground, setSelectedBackground] = useState(background || defaultEmojiBackground) const [showStyleColors, setShowStyleColors] = useState(!!emoji) const [searchedEmojis, setSearchedEmojis] = useState([]) const [isSearching, setIsSearching] = useState(false) const styleColorsLabelId = React.useId() - React.useEffect(() => { - if (selectedEmoji) { - /* v8 ignore next 2 - @preserve */ - if (selectedBackground) - onSelect?.(selectedEmoji, selectedBackground) - } - }, [onSelect, selectedEmoji, selectedBackground]) + const handleEmojiSelect = (emoji: string) => { + setSelectedEmoji(emoji) + setShowStyleColors(true) + onSelect?.(emoji, selectedBackground) + } + + const handleBackgroundSelect = (background: string) => { + setSelectedBackground(background) + if (selectedEmoji) + onSelect?.(selectedEmoji, background) + } return (
@@ -108,8 +91,7 @@ const EmojiPickerInner: FC = ({ aria-label={emoji} className="inline-flex size-10 items-center justify-center rounded-lg border-none bg-transparent p-0" onClick={() => { - setSelectedEmoji(emoji) - setShowStyleColors(true) + handleEmojiSelect(emoji) }} > @@ -136,8 +118,7 @@ const EmojiPickerInner: FC = ({ aria-label={emoji} className="inline-flex size-10 items-center justify-center rounded-lg border-none bg-transparent p-0" onClick={() => { - setSelectedEmoji(emoji) - setShowStyleColors(true) + handleEmojiSelect(emoji) }} > @@ -194,7 +175,7 @@ const EmojiPickerInner: FC = ({ ) } onClick={() => { - setSelectedBackground(color) + handleBackgroundSelect(color) }} > { expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() }) - it('initializes selected emoji and background when provided', async () => { + it('initializes selected emoji and background when provided', () => { render() expect(screen.getByText('Choose Style'))!.toBeInTheDocument() - await waitFor(() => { - expect(mockOnSelect).toHaveBeenCalledWith('rabbit', '#E4FBCC') - }) + expect(mockOnSelect).not.toHaveBeenCalled() }) }) diff --git a/web/app/components/base/emoji-picker/__tests__/index.spec.tsx b/web/app/components/base/emoji-picker/__tests__/index.spec.tsx index ed5f9bed75..45b12352c9 100644 --- a/web/app/components/base/emoji-picker/__tests__/index.spec.tsx +++ b/web/app/components/base/emoji-picker/__tests__/index.spec.tsx @@ -26,26 +26,27 @@ vi.mock('@/utils/emoji', () => ({ describe('EmojiPicker', () => { const mockOnSelect = vi.fn() - const mockOnClose = vi.fn() + const mockOnOpenChange = vi.fn() beforeEach(() => { vi.clearAllMocks() }) describe('Rendering', () => { - it('renders nothing when isModal is false', () => { + it('renders nothing when closed', () => { const { container } = render( - , + , ) expect(container.firstChild).toBeNull() }) - it('renders modal when isModal is true', async () => { + it('renders modal when open', async () => { await act(async () => { render( - , + , ) }) + expect(screen.getByRole('dialog', { name: /Emoji/i }))!.toBeInTheDocument() expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() expect(screen.getByText(/Cancel/i))!.toBeInTheDocument() expect(screen.getByText(/OK/i))!.toBeInTheDocument() @@ -54,7 +55,7 @@ describe('EmojiPicker', () => { it('OK button is disabled initially', async () => { await act(async () => { render( - , + , ) }) const okButton = screen.getByText(/OK/i).closest('button') @@ -65,7 +66,7 @@ describe('EmojiPicker', () => { const customClass = 'custom-wrapper-class' await act(async () => { render( - , + , ) }) const dialog = screen.getByRole('dialog') @@ -77,7 +78,7 @@ describe('EmojiPicker', () => { it('calls onSelect with selected emoji and background when OK is clicked', async () => { await act(async () => { render( - , + , ) }) @@ -95,10 +96,10 @@ describe('EmojiPicker', () => { expect(mockOnSelect).toHaveBeenCalledWith(expect.any(String), expect.any(String)) }) - it('calls onClose when Cancel is clicked', async () => { + it('closes when Cancel is clicked', async () => { await act(async () => { render( - , + , ) }) @@ -107,7 +108,7 @@ describe('EmojiPicker', () => { fireEvent.click(cancelButton) }) - expect(mockOnClose).toHaveBeenCalled() + expect(mockOnOpenChange).toHaveBeenCalledWith(false) }) }) }) diff --git a/web/app/components/base/emoji-picker/constants.ts b/web/app/components/base/emoji-picker/constants.ts new file mode 100644 index 0000000000..927520cb38 --- /dev/null +++ b/web/app/components/base/emoji-picker/constants.ts @@ -0,0 +1,23 @@ +export const backgroundColors = [ + '#FFEAD5', + '#E4FBCC', + '#D3F8DF', + '#E0F2FE', + + '#E0EAFF', + '#EFF1F5', + '#FBE8FF', + '#FCE7F6', + + '#FEF7C3', + '#E6F4D7', + '#D5F5F6', + '#D1E9FF', + + '#D1E0FF', + '#D5D9EB', + '#ECE9FE', + '#FFE4E8', +] + +export const defaultEmojiBackground = backgroundColors[0]! diff --git a/web/app/components/base/emoji-picker/index.stories.tsx b/web/app/components/base/emoji-picker/index.stories.tsx index a9804adc40..68ada2269a 100644 --- a/web/app/components/base/emoji-picker/index.stories.tsx +++ b/web/app/components/base/emoji-picker/index.stories.tsx @@ -47,20 +47,20 @@ const EmojiPickerDemo = () => {
- {open && ( - { - setSelection({ emoji, background }) - setOpen(false) - }} - onClose={() => setOpen(false)} - /> - )} + setSelection({ emoji, background })} + /> ) } export const Playground: Story = { + args: { + open: false, + onOpenChange: () => {}, + }, render: () => , parameters: { docs: { @@ -73,15 +73,11 @@ const [selection, setSelection] = useState<{ emoji: string; background: string } return ( <> - {open && ( - { - setSelection({ emoji, background }) - setOpen(false) - }} - onClose={() => setOpen(false)} - /> - )} + setSelection({ emoji, background })} + /> ) `.trim(), diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index 9ff81b999f..54e463bd9b 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -1,75 +1,95 @@ 'use client' -import type { FC } from 'react' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import * as React from 'react' -import { useCallback, useState } from 'react' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import EmojiPickerInner from './Inner' -type IEmojiPickerProps = { - isModal?: boolean +type EmojiPickerProps = { + open: boolean + onOpenChange: (open: boolean) => void onSelect?: (emoji: string, background: string) => void - onClose?: () => void className?: string } -const EmojiPicker: FC = ({ - isModal = true, +function EmojiPicker({ + open, + onOpenChange, onSelect, - onClose, className, -}) => { +}: EmojiPickerProps) { + return ( + + {open + ? ( + + ) + : null} + + ) +} + +type EmojiPickerContentProps = { + className?: string + onOpenChange: (open: boolean) => void + onSelect?: (emoji: string, background: string) => void +} + +function EmojiPickerContent({ + className, + onOpenChange, + onSelect, +}: EmojiPickerContentProps) { const { t } = useTranslation() const [selectedEmoji, setSelectedEmoji] = useState('') const [selectedBackground, setSelectedBackground] = useState() - const handleSelectEmoji = useCallback((emoji: string, background: string) => { - setSelectedEmoji(emoji) - setSelectedBackground(background) - }, [setSelectedEmoji, setSelectedBackground]) + return ( + + + {t('iconPicker.emoji', { ns: 'app' })} + - return isModal - ? ( - - - - - -
- - -
-
-
- ) - : <> + { + setSelectedEmoji(emoji) + setSelectedBackground(background) + }} + /> + +
+ + +
+
+ ) } export default EmojiPicker diff --git a/web/app/components/base/image-uploader/hooks.ts b/web/app/components/base/image-uploader/hooks.ts index 37cd5b5995..3ed2a36464 100644 --- a/web/app/components/base/image-uploader/hooks.ts +++ b/web/app/components/base/image-uploader/hooks.ts @@ -81,7 +81,7 @@ export const useImageFiles = () => { filesRef.current = newFiles setFiles(newFiles) }, - }, !!params.token) + }, !!params?.token) } } const handleClear = () => { @@ -145,13 +145,13 @@ export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useL toast.error(errorMessage) onUpload({ ...imageFile, progress: -1 }) }, - }, !!params.token) + }, !!params?.token) }, false) reader.addEventListener('error', () => { toast.error(t('imageUploader.uploadFromComputerReadError', { ns: 'common' })) }, false) reader.readAsDataURL(file) - }, [disabled, limit, t, onUpload, params.token]) + }, [disabled, limit, t, onUpload, params?.token]) return { disabled, handleLocalFileUpload } } type useClipboardUploaderProps = { diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx index f0867545cc..6560c8617c 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx @@ -29,33 +29,6 @@ vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => { } }) -// Mock AppIconPicker to capture interactions -let _mockOnSelect: ((icon: { type: 'emoji' | 'image', icon?: string, background?: string, fileId?: string, url?: string }) => void) | undefined -let _mockOnClose: (() => void) | undefined - -vi.mock('@/app/components/base/app-icon-picker', () => ({ - default: ({ onSelect, onClose }: { - onSelect: (icon: { type: 'emoji' | 'image', icon?: string, background?: string, fileId?: string, url?: string }) => void - onClose: () => void - }) => { - _mockOnSelect = onSelect - _mockOnClose = onClose - return ( -
- - - -
- ) - }, -})) - const createPipelineTemplate = (overrides: Partial = {}): PipelineTemplate => ({ id: 'pipeline-1', name: 'Test Pipeline', @@ -96,8 +69,6 @@ describe('EditPipelineInfo', () => { beforeEach(() => { vi.clearAllMocks() mockToastError.mockReset() - _mockOnSelect = undefined - _mockOnClose = undefined }) describe('Rendering', () => { @@ -303,7 +274,7 @@ describe('EditPipelineInfo', () => { // Open icon picker const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument() + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() }) it('should save correct icon_info when starting with image icon type', async () => { @@ -358,7 +329,7 @@ describe('EditPipelineInfo', () => { }) }) - it('should revert to initial image icon when picker is closed without selection', () => { + it('should revert to initial image icon when picker is closed without selection', async () => { const props = { ...defaultProps, pipeline: createImagePipelineTemplate(), @@ -368,13 +339,14 @@ describe('EditPipelineInfo', () => { // Open picker const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument() + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() // Close without selection - should revert to original image icon - const closeButton = screen.getByTestId('close-picker') - fireEvent.click(closeButton) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) }) it('should switch from image icon to emoji icon when selected', async () => { @@ -392,8 +364,11 @@ describe('EditPipelineInfo', () => { // Open picker and select emoji const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - const selectEmojiButton = screen.getByTestId('select-emoji') - fireEvent.click(selectEmojiButton) + const emojiButton = document.querySelector('em-emoji')?.closest('button') + expect(emojiButton).toBeTruthy() + fireEvent.click(emojiButton!) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) const saveButton = screen.getByText(/operation\.save/i) fireEvent.click(saveButton) @@ -403,7 +378,8 @@ describe('EditPipelineInfo', () => { expect.objectContaining({ icon_info: expect.objectContaining({ icon_type: 'emoji', - icon: '🎯', + icon: expect.any(String), + icon_background: '#E4FBCC', }), }), expect.any(Object), @@ -411,34 +387,14 @@ describe('EditPipelineInfo', () => { }) }) - it('should switch from emoji icon to image icon when selected', async () => { - mockUpdatePipeline.mockImplementation((_data, callbacks) => { - callbacks.onSuccess() - return Promise.resolve() - }) - + it('should switch to the image tab in the real picker', () => { const { container } = render() - // Open picker and select image const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - const selectImageButton = screen.getByTestId('select-image') - fireEvent.click(selectImageButton) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.image/ })) - const saveButton = screen.getByText(/operation\.save/i) - fireEvent.click(saveButton) - - await waitFor(() => { - expect(mockUpdatePipeline).toHaveBeenCalledWith( - expect.objectContaining({ - icon_info: expect.objectContaining({ - icon_type: 'image', - icon: 'new-file-id', - }), - }), - expect.any(Object), - ) - }) + expect(screen.getByRole('button', { name: /iconPicker\.ok/ })).toBeInTheDocument() }) }) @@ -446,7 +402,7 @@ describe('EditPipelineInfo', () => { describe('AppIconPicker', () => { it('should not show picker initially', () => { render() - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() }) it('should open picker when icon is clicked', () => { @@ -454,43 +410,42 @@ describe('EditPipelineInfo', () => { const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument() + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() }) - it('should close picker and update icon when emoji is selected', () => { + it('should close picker and update icon when emoji style is selected', async () => { const { container } = render() const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - const selectEmojiButton = screen.getByTestId('select-emoji') - fireEvent.click(selectEmojiButton) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) - // Picker should close - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) }) - it('should close picker and update icon when image is selected', () => { + it('should keep picker open when only switching to image tab', () => { const { container } = render() const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - const selectImageButton = screen.getByTestId('select-image') - fireEvent.click(selectImageButton) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.image/ })) - // Picker should close - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: /iconPicker\.ok/ })).toBeInTheDocument() }) - it('should revert icon when picker is closed without selection', () => { + it('should revert icon when picker is closed without selection', async () => { const { container } = render() const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - const closeButton = screen.getByTestId('close-picker') - fireEvent.click(closeButton) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) - // Picker should close - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) }) it('should save with new emoji icon selection', async () => { @@ -504,8 +459,8 @@ describe('EditPipelineInfo', () => { // Open picker and select new emoji const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - const selectEmojiButton = screen.getByTestId('select-emoji') - fireEvent.click(selectEmojiButton) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) const saveButton = screen.getByText(/operation\.save/i) fireEvent.click(saveButton) @@ -515,8 +470,8 @@ describe('EditPipelineInfo', () => { expect.objectContaining({ icon_info: expect.objectContaining({ icon_type: 'emoji', - icon: '🎯', - icon_background: '#FFEAD5', + icon: 'πŸ“Š', + icon_background: '#E4FBCC', }), }), expect.any(Object), @@ -524,19 +479,21 @@ describe('EditPipelineInfo', () => { }) }) - it('should save with new image icon selection', async () => { + it('should save after confirming a real emoji selection from an image icon', async () => { mockUpdatePipeline.mockImplementation((_data, callbacks) => { callbacks.onSuccess() return Promise.resolve() }) - const { container } = render() + const { container } = render() - // Open picker and select new image const appIcon = container.querySelector('[class*="cursor-pointer"]') fireEvent.click(appIcon!) - const selectImageButton = screen.getByTestId('select-image') - fireEvent.click(selectImageButton) + const emojiButton = document.querySelector('em-emoji')?.closest('button') + expect(emojiButton).toBeTruthy() + fireEvent.click(emojiButton!) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) const saveButton = screen.getByText(/operation\.save/i) fireEvent.click(saveButton) @@ -545,9 +502,9 @@ describe('EditPipelineInfo', () => { expect(mockUpdatePipeline).toHaveBeenCalledWith( expect.objectContaining({ icon_info: expect.objectContaining({ - icon_type: 'image', - icon: 'new-file-id', - icon_url: 'https://new-icon.com/icon.png', + icon_type: 'emoji', + icon: expect.any(String), + icon_background: '#E4FBCC', }), }), expect.any(Object), diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx index 2cf2782185..e5622806b8 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx @@ -4,7 +4,7 @@ import { Button } from '@langgenius/dify-ui/button' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import * as React from 'react' -import { useCallback, useRef, useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import AppIconPicker from '@/app/components/base/app-icon-picker' @@ -31,11 +31,6 @@ const EditPipelineInfo = ({ ) const [description, setDescription] = useState(pipeline.description) const [showAppIconPicker, setShowAppIconPicker] = useState(false) - const previousAppIcon = useRef( - iconInfo.icon_type === 'image' - ? { type: 'image' as const, url: iconInfo.icon_url || '', fileId: iconInfo.icon || '' } - : { type: 'emoji' as const, icon: iconInfo.icon || '', background: iconInfo.icon_background || '' }, - ) const handleAppNameChange = useCallback((event: React.ChangeEvent) => { const value = event.target.value @@ -44,17 +39,10 @@ const EditPipelineInfo = ({ const handleOpenAppIconPicker = useCallback(() => { setShowAppIconPicker(true) - previousAppIcon.current = appIcon - }, [appIcon]) + }, []) const handleSelectAppIcon = useCallback((icon: AppIconSelection) => { setAppIcon(icon) - setShowAppIconPicker(false) - }, []) - - const handleCloseAppIconPicker = useCallback(() => { - setAppIcon(previousAppIcon.current) - setShowAppIconPicker(false) }, []) const handleDescriptionChange = useCallback((event: React.ChangeEvent) => { @@ -156,8 +144,12 @@ const EditPipelineInfo = ({ {showAppIconPicker && ( )} diff --git a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx index ac4075fa5b..1322d4f6e6 100644 --- a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ import type { DataSet } from '@/models/datasets' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import RenameDatasetModal from '../index' @@ -33,24 +34,6 @@ vi.mock('../../../base/app-icon', () => ({ ), })) -// Mock AppIconPicker - simplified mock to test onSelect and onClose callbacks -vi.mock('../../../base/app-icon-picker', () => ({ - default: ({ onSelect, onClose }: { - onSelect?: (icon: { type: string, icon?: string, background?: string, fileId?: string, url?: string }) => void - onClose?: () => void - }) => ( -
- - - -
- ), -})) - // The mock returns 'ns.key' format, e.g., 'common.operation.cancel' describe('RenameDatasetModal', () => { @@ -859,67 +842,32 @@ describe('RenameDatasetModal', () => { // Initially picker should not be visible // Initially picker should not be visible // Initially picker should not be visible - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() const appIcon = screen.getByTestId('app-icon') await act(async () => { fireEvent.click(appIcon) }) - // Picker should now be visible - // Picker should now be visible - expect(screen.getByTestId('app-icon-picker'))!.toBeInTheDocument() + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) }) - it('should select emoji icon and close picker (handleSelectAppIcon)', async () => { + it('should select emoji style and close picker (handleSelectAppIcon)', async () => { + const user = userEvent.setup() render() - // Open picker - const appIcon = screen.getByTestId('app-icon') - await act(async () => { - fireEvent.click(appIcon) + await user.click(screen.getByTestId('app-icon')) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() }) - - // Select emoji - const selectEmojiBtn = screen.getByTestId('select-emoji') - await act(async () => { - fireEvent.click(selectEmojiBtn) + await user.click(screen.getByRole('button', { name: '#E4FBCC' })) + await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() }) - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() - // Save and verify new icon is used const saveButton = screen.getByText('common.operation.save') await act(async () => { @@ -931,9 +879,9 @@ describe('RenameDatasetModal', () => { datasetId: 'dataset-1', body: expect.objectContaining({ icon_info: { - icon: 'πŸš€', + icon: 'πŸ“Š', icon_type: 'emoji', - icon_background: '#E0F2FE', + icon_background: '#E4FBCC', icon_url: undefined, }, }), @@ -941,56 +889,20 @@ describe('RenameDatasetModal', () => { }) }) - it('should select image icon and close picker (handleSelectAppIcon)', async () => { + it('should update emoji style through the picker (handleSelectAppIcon)', async () => { + const user = userEvent.setup() render() - // Open picker - const appIcon = screen.getByTestId('app-icon') - await act(async () => { - fireEvent.click(appIcon) + await user.click(screen.getByTestId('app-icon')) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: '#E0F2FE' })) + await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() }) - // Select image - const selectImageBtn = screen.getByTestId('select-image') - await act(async () => { - fireEvent.click(selectImageBtn) - }) - - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - // Picker should close after selection - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() - - // Save and verify new image icon is used const saveButton = screen.getByText('common.operation.save') await act(async () => { fireEvent.click(saveButton) @@ -1001,10 +913,10 @@ describe('RenameDatasetModal', () => { datasetId: 'dataset-1', body: expect.objectContaining({ icon_info: { - icon: 'new-file', - icon_type: 'image', - icon_background: undefined, - icon_url: 'https://new.png', + icon: 'πŸ“Š', + icon_type: 'emoji', + icon_background: '#E0F2FE', + icon_url: undefined, }, }), }) @@ -1020,45 +932,14 @@ describe('RenameDatasetModal', () => { fireEvent.click(appIcon) }) - // Close picker without selecting - const closeBtn = screen.getByTestId('close-picker') - await act(async () => { - fireEvent.click(closeBtn) + const user = userEvent.setup() + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() }) - - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - // Picker should close - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() // Save and verify original icon is preserved const saveButton = screen.getByText('common.operation.save') diff --git a/web/app/components/datasets/rename-modal/index.tsx b/web/app/components/datasets/rename-modal/index.tsx index 71f8afcc92..c72e910363 100644 --- a/web/app/components/datasets/rename-modal/index.tsx +++ b/web/app/components/datasets/rename-modal/index.tsx @@ -7,7 +7,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' -import { useCallback, useRef, useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' @@ -32,20 +32,11 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset ? { type: 'image' as const, url: dataset.icon_info?.icon_url || '', fileId: dataset.icon_info?.icon || '' } : { type: 'emoji' as const, icon: dataset.icon_info?.icon || '', background: dataset.icon_info?.icon_background || '' }) const [showAppIconPicker, setShowAppIconPicker] = useState(false) - const previousAppIcon = useRef(dataset.icon_info?.icon_type === 'image' - ? { type: 'image' as const, url: dataset.icon_info?.icon_url || '', fileId: dataset.icon_info?.icon || '' } - : { type: 'emoji' as const, icon: dataset.icon_info?.icon || '', background: dataset.icon_info?.icon_background || '' }) const handleOpenAppIconPicker = useCallback(() => { setShowAppIconPicker(true) - previousAppIcon.current = appIcon - }, [appIcon]) + }, []) const handleSelectAppIcon = useCallback((icon: AppIconSelection) => { setAppIcon(icon) - setShowAppIconPicker(false) - }, []) - const handleCloseAppIconPicker = useCallback(() => { - setAppIcon(previousAppIcon.current) - setShowAppIconPicker(false) }, []) const onConfirm: MouseEventHandler = useCallback(async () => { if (!name.trim()) { @@ -125,7 +116,16 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset - {showAppIconPicker && ()} + {showAppIconPicker && ( + + )} ) diff --git a/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx b/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx index f34269f058..7681d35702 100644 --- a/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx +++ b/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx @@ -127,7 +127,7 @@ describe('BasicInfoSection', () => { showAppIconPicker: false, handleOpenAppIconPicker: vi.fn(), handleSelectAppIcon: vi.fn(), - handleCloseAppIconPicker: vi.fn(), + setShowAppIconPicker: vi.fn(), permission: DatasetPermission.onlyMe, setPermission: vi.fn(), selectedMemberIDs: ['user-1'], diff --git a/web/app/components/datasets/settings/form/components/basic-info-section.tsx b/web/app/components/datasets/settings/form/components/basic-info-section.tsx index 3d3cf75851..fc7545d7dd 100644 --- a/web/app/components/datasets/settings/form/components/basic-info-section.tsx +++ b/web/app/components/datasets/settings/form/components/basic-info-section.tsx @@ -24,7 +24,7 @@ type BasicInfoSectionProps = { showAppIconPicker: boolean handleOpenAppIconPicker: () => void handleSelectAppIcon: (icon: AppIconSelection) => void - handleCloseAppIconPicker: () => void + setShowAppIconPicker: (show: boolean) => void permission: DatasetPermission | undefined setPermission: (value: DatasetPermission | undefined) => void selectedMemberIDs: string[] @@ -43,7 +43,7 @@ const BasicInfoSection = ({ showAppIconPicker, handleOpenAppIconPicker, handleSelectAppIcon, - handleCloseAppIconPicker, + setShowAppIconPicker, permission, setPermission, selectedMemberIDs, @@ -113,8 +113,12 @@ const BasicInfoSection = ({ {showAppIconPicker && ( )} diff --git a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts index 62d41a4ce0..cefcedafc6 100644 --- a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts +++ b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts @@ -258,7 +258,7 @@ describe('useFormState', () => { expect(result.current.showAppIconPicker).toBe(true) }) - it('should select emoji icon and close picker', () => { + it('should select emoji icon without owning picker close state', () => { const { result } = renderHook(() => useFormState()) act(() => { @@ -273,7 +273,7 @@ describe('useFormState', () => { }) }) - expect(result.current.showAppIconPicker).toBe(false) + expect(result.current.showAppIconPicker).toBe(true) expect(result.current.iconInfo).toEqual({ icon_type: 'emoji', icon: 'πŸŽ‰', @@ -282,7 +282,7 @@ describe('useFormState', () => { }) }) - it('should select image icon and close picker', () => { + it('should select image icon without owning picker close state', () => { const { result } = renderHook(() => useFormState()) act(() => { @@ -297,7 +297,7 @@ describe('useFormState', () => { }) }) - expect(result.current.showAppIconPicker).toBe(false) + expect(result.current.showAppIconPicker).toBe(true) expect(result.current.iconInfo).toEqual({ icon_type: 'image', icon: 'file-123', @@ -306,7 +306,7 @@ describe('useFormState', () => { }) }) - it('should restore previous icon when picker is closed', () => { + it('should close picker through open state setter without changing icon', () => { const { result } = renderHook(() => useFormState()) act(() => { @@ -322,15 +322,10 @@ describe('useFormState', () => { }) act(() => { - result.current.handleOpenAppIconPicker() - }) - - act(() => { - result.current.handleCloseAppIconPicker() + result.current.setShowAppIconPicker(false) }) expect(result.current.showAppIconPicker).toBe(false) - // After close, icon should be restored to the icon before opening expect(result.current.iconInfo).toEqual({ icon_type: 'emoji', icon: 'πŸŽ‰', diff --git a/web/app/components/datasets/settings/form/hooks/use-form-state.ts b/web/app/components/datasets/settings/form/hooks/use-form-state.ts index ff61c1e815..0ebd60a69a 100644 --- a/web/app/components/datasets/settings/form/hooks/use-form-state.ts +++ b/web/app/components/datasets/settings/form/hooks/use-form-state.ts @@ -5,7 +5,7 @@ import type { Member } from '@/models/common' import type { IconInfo, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' import { toast } from '@langgenius/dify-ui/toast' -import { useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -39,7 +39,6 @@ export const useFormState = () => { // Icon state const [iconInfo, setIconInfo] = useState(currentDataset?.icon_info || DEFAULT_APP_ICON) const [showAppIconPicker, setShowAppIconPicker] = useState(false) - const previousAppIcon = useRef(DEFAULT_APP_ICON) // Permission state const [permission, setPermission] = useState(currentDataset?.permission) @@ -83,8 +82,7 @@ export const useFormState = () => { // Icon handlers const handleOpenAppIconPicker = useCallback(() => { setShowAppIconPicker(true) - previousAppIcon.current = iconInfo - }, [iconInfo]) + }, []) const handleSelectAppIcon = useCallback((icon: AppIconSelection) => { const newIconInfo: IconInfo = { @@ -94,12 +92,6 @@ export const useFormState = () => { icon_url: icon.type === 'emoji' ? undefined : icon.url, } setIconInfo(newIconInfo) - setShowAppIconPicker(false) - }, []) - - const handleCloseAppIconPicker = useCallback(() => { - setIconInfo(previousAppIcon.current) - setShowAppIconPicker(false) }, []) // External retrieval settings handler @@ -223,9 +215,9 @@ export const useFormState = () => { // Icon iconInfo, showAppIconPicker, + setShowAppIconPicker, handleOpenAppIconPicker, handleSelectAppIcon, - handleCloseAppIconPicker, // Permission permission, diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index f103d9d951..a483c94be3 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -26,9 +26,9 @@ const Form = () => { // Icon iconInfo, showAppIconPicker, + setShowAppIconPicker, handleOpenAppIconPicker, handleSelectAppIcon, - handleCloseAppIconPicker, // Permission permission, @@ -78,9 +78,9 @@ const Form = () => { setDescription={setDescription} iconInfo={iconInfo} showAppIconPicker={showAppIconPicker} + setShowAppIconPicker={setShowAppIconPicker} handleOpenAppIconPicker={handleOpenAppIconPicker} handleSelectAppIcon={handleSelectAppIcon} - handleCloseAppIconPicker={handleCloseAppIconPicker} permission={permission} setPermission={setPermission} selectedMemberIDs={selectedMemberIDs} diff --git a/web/app/components/explore/create-app-modal/index.tsx b/web/app/components/explore/create-app-modal/index.tsx index 1b52ec2ad7..af45bbcbf0 100644 --- a/web/app/components/explore/create-app-modal/index.tsx +++ b/web/app/components/explore/create-app-modal/index.tsx @@ -198,15 +198,13 @@ const CreateAppModal = ({ {showAppIconPicker && ( { setAppIcon(payload) - setShowAppIconPicker(false) - }} - onClose={() => { - setShowAppIconPicker(false) }} /> )} diff --git a/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx index 2f21811f86..949872d14b 100644 --- a/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx @@ -1,6 +1,5 @@ import type { EnvironmentVariable } from '@/app/components/workflow/types' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' -import { useState } from 'react' import { createMockProviderContextValue } from '@/__mocks__/provider-context' import Conversion from '../conversion' @@ -348,58 +347,6 @@ vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({ ), })) -vi.mock('@/app/components/base/app-icon-picker', () => ({ - default: function MockAppIconPicker({ onSelect, onClose }: { - onSelect?: (payload: - | { type: 'emoji', icon: string, background: string } - | { type: 'image', fileId: string, url: string }, - ) => void - onClose?: () => void - }) { - const [activeTab, setActiveTab] = useState<'emoji' | 'image'>('emoji') - const [selectedEmoji, setSelectedEmoji] = useState({ icon: 'πŸ˜€', background: '#FFFFFF' }) - - return ( -
- - - {activeTab === 'emoji' && ( - - )} - {activeTab === 'image' &&
picker-image-panel
} - - -
- ) - }, -})) - // Silence expected console.error from Dialog/Modal rendering beforeEach(() => { vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -767,7 +714,7 @@ describe('PublishAsKnowledgePipelineModal', () => { const appIcon = getAppIcon() fireEvent.click(appIcon) - fireEvent.click(screen.getByTestId('picker-emoji-option')) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) // Click OK to confirm selection fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) @@ -1087,7 +1034,7 @@ describe('Integration Tests', () => { // Open picker and select an emoji const appIcon = getAppIcon() fireEvent.click(appIcon) - fireEvent.click(screen.getByTestId('picker-emoji-option')) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.publish/i })) diff --git a/web/app/components/rag-pipeline/components/__tests__/publish-as-knowledge-pipeline-modal.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/publish-as-knowledge-pipeline-modal.spec.tsx index 4a795ba516..712ea4205f 100644 --- a/web/app/components/rag-pipeline/components/__tests__/publish-as-knowledge-pipeline-modal.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/publish-as-knowledge-pipeline-modal.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import PublishAsKnowledgePipelineModal from '../publish-as-knowledge-pipeline-modal' @@ -23,6 +23,9 @@ vi.mock('@langgenius/dify-ui/dialog', () => ({ DialogContent: ({ children, className }: { children: React.ReactNode, className?: string }) => (
{children}
), + DialogTitle: ({ children, className }: { children: React.ReactNode, className?: string }) => ( +

{children}

+ ), })) vi.mock('@langgenius/dify-ui/button', () => ({ @@ -61,22 +64,6 @@ vi.mock('@/app/components/base/app-icon', () => ({ ), })) -vi.mock('@/app/components/base/app-icon-picker', () => ({ - default: ({ onSelect, onClose }: { onSelect: (item: { type: string, icon: string, background: string, url: string }) => void, onClose: () => void }) => ( -
- - - -
- ), -})) - vi.mock('es-toolkit/function', () => ({ noop: () => {}, })) @@ -190,41 +177,46 @@ describe('PublishAsKnowledgePipelineModal', () => { expect(mockOnConfirm).not.toHaveBeenCalled() }) - it('should show icon picker when app icon clicked', () => { + it('should show icon picker when app icon clicked', async () => { render() - expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() fireEvent.click(screen.getByTestId('app-icon')) - expect(screen.getByTestId('icon-picker')).toBeInTheDocument() + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() }) - it('should update icon when emoji is selected', () => { + it('should update icon when emoji style is selected', async () => { render() fireEvent.click(screen.getByTestId('app-icon')) - fireEvent.click(screen.getByTestId('select-emoji')) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) - expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) }) - it('should update icon when image is selected', () => { + it('should keep icon picker open until confirmation', () => { render() fireEvent.click(screen.getByTestId('app-icon')) - fireEvent.click(screen.getByTestId('select-image')) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) - expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument() + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() }) - it('should close icon picker when close is clicked', () => { + it('should close icon picker when cancel is clicked', async () => { render() fireEvent.click(screen.getByTestId('app-icon')) - fireEvent.click(screen.getByTestId('close-picker')) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) - expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) }) it('should trim name and description before submitting', () => { diff --git a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx b/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx index 025c84ef2f..ad908e8727 100644 --- a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx +++ b/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx @@ -51,17 +51,7 @@ const PublishAsKnowledgePipelineModal = ({ icon_url: '', }) } - setShowAppIconPicker(false) }, []) - const handleCloseIconPicker = useCallback(() => { - setPipelineIcon({ - icon_type: pipelineIcon.icon_type, - icon: pipelineIcon.icon, - icon_background: pipelineIcon.icon_background, - icon_url: pipelineIcon.icon_url, - }) - setShowAppIconPicker(false) - }, [pipelineIcon]) const handleConfirm = () => { if (confirmDisabled) @@ -141,8 +131,12 @@ const PublishAsKnowledgePipelineModal = ({ {showAppIconPicker && ( )} diff --git a/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx b/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx index faa91a1cdf..1822b8d933 100644 --- a/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx @@ -53,18 +53,6 @@ vi.mock('@/context/i18n', async () => { } }) -// Mock EmojiPicker -vi.mock('@/app/components/base/emoji-picker', () => ({ - default: ({ onSelect, onClose }: { onSelect: (icon: string, background: string) => void, onClose: () => void }) => { - return ( -
- - -
- ) - }, -})) - describe('EditCustomCollectionModal', () => { const mockOnHide = vi.fn() const mockOnAdd = vi.fn() diff --git a/web/app/components/tools/edit-custom-collection-modal/index.tsx b/web/app/components/tools/edit-custom-collection-modal/index.tsx index 39ce47b193..452dfafd01 100644 --- a/web/app/components/tools/edit-custom-collection-modal/index.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/index.tsx @@ -386,12 +386,10 @@ const EditCustomCollectionModal: FC = ({ {showEmojiPicker && ( { setEmoji({ content: icon, background: icon_background }) - setShowEmojiPicker(false) - }} - onClose={() => { - setShowEmojiPicker(false) }} /> )} diff --git a/web/app/components/tools/mcp/__tests__/modal.spec.tsx b/web/app/components/tools/mcp/__tests__/modal.spec.tsx index 435d06b75f..ad554d1f7d 100644 --- a/web/app/components/tools/mcp/__tests__/modal.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/modal.spec.tsx @@ -11,31 +11,6 @@ vi.mock('@/service/common', () => ({ uploadRemoteFileInfo: vi.fn().mockResolvedValue({ url: 'https://example.com/icon.png' }), })) -// Mock the AppIconPicker component -type IconPayload = { - type: string - icon: string - background: string -} - -type AppIconPickerProps = { - onSelect: (payload: IconPayload) => void - onClose: () => void -} - -vi.mock('@/app/components/base/app-icon-picker', () => ({ - default: ({ onSelect, onClose }: AppIconPickerProps) => ( -
- - -
- ), -})) - // Mock the plugins service to avoid React Query issues from TabSlider vi.mock('@/service/use-plugins', () => ({ useInstalledPluginList: () => ({ @@ -695,9 +670,8 @@ describe('MCPModal', () => { if (appIconContainer) { fireEvent.click(appIconContainer) - // The mocked AppIconPicker should now be visible await waitFor(() => { - expect(screen.getByTestId('app-icon-picker'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() }) } }) @@ -712,16 +686,14 @@ describe('MCPModal', () => { fireEvent.click(appIconContainer) await waitFor(() => { - expect(screen.getByTestId('app-icon-picker'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() }) - // Click the select emoji button - const selectBtn = screen.getByTestId('select-emoji-btn') - fireEvent.click(selectBtn) + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) - // The picker should be closed await waitFor(() => { - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() }) } }) @@ -736,16 +708,13 @@ describe('MCPModal', () => { fireEvent.click(appIconContainer) await waitFor(() => { - expect(screen.getByTestId('app-icon-picker'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() }) - // Click the close button - const closeBtn = screen.getByTestId('close-picker-btn') - fireEvent.click(closeBtn) + fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) - // The picker should be closed await waitFor(() => { - expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() }) } }) diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index 7495338a70..b5ea71e8e0 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -117,12 +117,6 @@ const MCPModalContent: FC = ({ const handleIconSelect = (payload: AppIconSelection) => { actions.setAppIcon(payload) - actions.setShowAppIconPicker(false) - } - - const handleIconClose = () => { - actions.resetIcon() - actions.setShowAppIconPicker(false) } const isSubmitDisabled = !state.name || !state.url || !state.serverIdentifier || state.isFetchingIcon @@ -260,8 +254,12 @@ const MCPModalContent: FC = ({ {state.showAppIconPicker && ( )} 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 9db9852e0b..5dd850abe5 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 @@ -19,6 +19,7 @@ vi.mock('@/next/navigation', () => ({ }), usePathname: () => '/app/workflow-app-id', useSearchParams: () => new URLSearchParams(), + useParams: () => ({}), })) // Mock app context @@ -68,15 +69,6 @@ vi.mock('@/app/components/plugins/hooks', () => ({ }), })) -// Mock EmojiPickerInner - simplified for testing -vi.mock('@/app/components/base/emoji-picker/Inner', () => ({ - default: ({ onSelect }: { onSelect: (icon: string, background: string) => void }) => ( -
- -
- ), -})) - // Mock AppIcon - simplified for testing vi.mock('@/app/components/base/app-icon', () => ({ default: ({ onClick, icon, background }: { onClick?: () => void, icon: string, background: string }) => ( @@ -814,8 +806,9 @@ describe('WorkflowToolDrawer', () => { await user.click(iconButton) // Assert - // Assert - expect(screen.getByTestId('emoji-picker'))!.toBeInTheDocument() + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) }) it('should update emoji on selection', async () => { @@ -834,14 +827,19 @@ describe('WorkflowToolDrawer', () => { const iconButton = screen.getByTestId('app-icon') await user.click(iconButton) - // Select emoji - await user.click(screen.getByTestId('select-emoji')) - await user.click(screen.getByRole('button', { name: 'app.iconPicker.ok' })) + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: '#E4FBCC' })) + await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) // Assert const updatedIcon = screen.getByTestId('app-icon') - expect(updatedIcon)!.toHaveAttribute('data-icon', 'πŸš€') - expect(updatedIcon)!.toHaveAttribute('data-background', '#f0f0f0') + expect(updatedIcon)!.toHaveAttribute('data-icon', 'πŸ”§') + expect(updatedIcon)!.toHaveAttribute('data-background', '#E4FBCC') }) it('should close emoji picker on close button', async () => { @@ -859,43 +857,15 @@ describe('WorkflowToolDrawer', () => { const iconButton = screen.getByTestId('app-icon') await user.click(iconButton) - expect(screen.getByTestId('emoji-picker'))!.toBeInTheDocument() + await waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) - await user.click(screen.getByRole('button', { name: 'app.iconPicker.cancel' })) - - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - // Assert - expect(screen.queryByTestId('emoji-picker')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() }) it('should update labels when label selector changes', async () => { 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 3a8e3a539b..c0811ba6d5 100644 --- a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx @@ -4,14 +4,6 @@ import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import { WorkflowToolDrawer } from '../index' -vi.mock('@/app/components/base/emoji-picker/Inner', () => ({ - default: ({ onSelect }: { onSelect: (icon: string, background: string) => void }) => ( -
- -
- ), -})) - vi.mock('@/app/components/base/app-icon', () => ({ default: ({ onClick, icon }: { onClick?: () => void, icon: string }) => ( @@ -98,14 +90,20 @@ describe('WorkflowToolDrawer', () => { await user.type(screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder'), 'Created Tool') 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 waitFor(() => { + expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: '#E4FBCC' })) + await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + }) await user.click(screen.getByRole('button', { name: 'common.operation.save' })) expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({ workflow_app_id: 'workflow-app-1', label: 'Created Tool', - icon: { content: 'πŸš€', background: '#000000' }, + icon: { content: 'πŸ”§', background: '#E4FBCC' }, labels: ['label1', 'new-label'], })) }) diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index 9da6793092..8924ed7a29 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -3,7 +3,6 @@ import type { DrawerRootProps } from '@langgenius/dify-ui/drawer' 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 { Drawer, DrawerBackdrop, @@ -21,8 +20,7 @@ import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' -import Divider from '@/app/components/base/divider' -import EmojiPickerInner from '@/app/components/base/emoji-picker/Inner' +import AppIconPicker from '@/app/components/base/app-icon-picker' import { Infotip } from '@/app/components/base/infotip' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' @@ -126,51 +124,6 @@ const WorkflowToolDrawerFrame = ({ title, closeLabel, onHide, children }: Workfl ) } -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) - }} - /> - -
- - -
-
-
- ) -} - export function WorkflowToolDrawer({ isAdd, payload, @@ -449,17 +402,19 @@ export function WorkflowToolDrawer({ - {showEmojiPicker && ( - { - setEmoji({ content: icon, background: icon_background }) - setShowEmojiPicker(false) - }} - onClose={() => { - setShowEmojiPicker(false) - }} - /> - )} + { + if (payload.type === 'emoji') + setEmoji({ content: payload.icon, background: payload.background }) + }} + /> {confirmModalOpen && (