mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
fix: normalize app icon picker dialog state (#36621)
This commit is contained in:
parent
b1f0a11d84
commit
fe86fa31ec
@ -60,21 +60,6 @@ vi.mock('@/app/components/base/app-icon', () => ({
|
|||||||
}) => <button onClick={onClick}>open-emoji-picker</button>,
|
}) => <button onClick={onClick}>open-emoji-picker</button>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/app/components/base/emoji-picker', () => ({
|
|
||||||
default: ({
|
|
||||||
onClose,
|
|
||||||
onSelect,
|
|
||||||
}: {
|
|
||||||
onClose: () => void
|
|
||||||
onSelect: (icon: string, background: string) => void
|
|
||||||
}) => (
|
|
||||||
<div>
|
|
||||||
<button onClick={() => onSelect('sparkles', '#fff')}>select-emoji</button>
|
|
||||||
<button onClick={onClose}>close-emoji</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/base/features/new-feature-panel/moderation/form-generation', () => ({
|
vi.mock('@/app/components/base/features/new-feature-panel/moderation/form-generation', () => ({
|
||||||
default: ({
|
default: ({
|
||||||
onChange,
|
onChange,
|
||||||
@ -146,7 +131,14 @@ describe('ExternalDataToolModal', () => {
|
|||||||
})
|
})
|
||||||
fireEvent.click(screen.getByText('pick-extension'))
|
fireEvent.click(screen.getByText('pick-extension'))
|
||||||
fireEvent.click(screen.getByText('open-emoji-picker'))
|
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'))
|
fireEvent.click(screen.getByText('operation.save'))
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -155,8 +147,8 @@ describe('ExternalDataToolModal', () => {
|
|||||||
api_based_extension_id: 'extension-1',
|
api_based_extension_id: 'extension-1',
|
||||||
},
|
},
|
||||||
enabled: true,
|
enabled: true,
|
||||||
icon: 'sparkles',
|
icon: expect.any(String),
|
||||||
icon_background: '#fff',
|
icon_background: '#E4FBCC',
|
||||||
label: 'Search',
|
label: 'Search',
|
||||||
type: 'api',
|
type: 'api',
|
||||||
variable: 'search_api',
|
variable: 'search_api',
|
||||||
@ -168,8 +160,8 @@ describe('ExternalDataToolModal', () => {
|
|||||||
api_based_extension_id: 'extension-1',
|
api_based_extension_id: 'extension-1',
|
||||||
},
|
},
|
||||||
enabled: true,
|
enabled: true,
|
||||||
icon: 'sparkles',
|
icon: expect.any(String),
|
||||||
icon_background: '#fff',
|
icon_background: '#E4FBCC',
|
||||||
label: 'Search',
|
label: 'Search',
|
||||||
type: 'api',
|
type: 'api',
|
||||||
variable: 'search_api',
|
variable: 'search_api',
|
||||||
|
|||||||
@ -217,13 +217,10 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
|||||||
{
|
{
|
||||||
showEmojiPicker && (
|
showEmojiPicker && (
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
open={showEmojiPicker}
|
||||||
|
onOpenChange={setShowEmojiPicker}
|
||||||
onSelect={(icon, icon_background) => {
|
onSelect={(icon, icon_background) => {
|
||||||
handleValueChange({ icon, icon_background })
|
handleValueChange({ icon, icon_background })
|
||||||
setShowEmojiPicker(false)
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
handleValueChange({ icon: '', icon_background: '' })
|
|
||||||
setShowEmojiPicker(false)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
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 { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
@ -30,6 +31,7 @@ vi.mock('ahooks', () => ({
|
|||||||
}))
|
}))
|
||||||
vi.mock('@/next/navigation', () => ({
|
vi.mock('@/next/navigation', () => ({
|
||||||
useRouter: vi.fn(),
|
useRouter: vi.fn(),
|
||||||
|
useParams: () => ({}),
|
||||||
}))
|
}))
|
||||||
vi.mock('@/utils/create-app-tracking', () => ({
|
vi.mock('@/utils/create-app-tracking', () => ({
|
||||||
trackCreateApp: vi.fn(),
|
trackCreateApp: vi.fn(),
|
||||||
@ -55,19 +57,6 @@ vi.mock('@/app/components/base/app-icon', () => ({
|
|||||||
<button type="button" onClick={onClick}>open-icon-picker</button>
|
<button type="button" onClick={onClick}>open-icon-picker</button>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
vi.mock('@/app/components/base/app-icon-picker', () => ({
|
|
||||||
default: ({ onSelect, onClose }: { onSelect: (payload: Record<string, unknown>) => void, onClose: () => void }) => (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onSelect({ type: 'image', fileId: 'file-1', url: 'https://example.com/icon.png' })}
|
|
||||||
>
|
|
||||||
select-image-icon
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={onClose}>close-icon-picker</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
vi.mock('@/utils/app-redirection', () => ({
|
vi.mock('@/utils/app-redirection', () => ({
|
||||||
getRedirection: vi.fn(),
|
getRedirection: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@ -216,14 +205,22 @@ describe('CreateAppModal', () => {
|
|||||||
expect(onCreateFromTemplate).toHaveBeenCalled()
|
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)
|
mockCreateApp.mockResolvedValue({ id: 'chat-app', mode: AppModeEnum.CHAT } as App)
|
||||||
renderModal()
|
renderModal()
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('app.newApp.forBeginners'))
|
fireEvent.click(screen.getByText('app.newApp.forBeginners'))
|
||||||
fireEvent.click(screen.getByText('app.types.chatbot'))
|
fireEvent.click(screen.getByText('app.types.chatbot'))
|
||||||
fireEvent.click(screen.getByText('open-icon-picker'))
|
await user.click(screen.getByText('open-icon-picker'))
|
||||||
fireEvent.click(screen.getByText('select-image-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()
|
||||||
|
})
|
||||||
fireEvent.change(screen.getByPlaceholderText('app.newApp.appNamePlaceholder'), {
|
fireEvent.change(screen.getByPlaceholderText('app.newApp.appNamePlaceholder'), {
|
||||||
target: { value: 'Keyboard App' },
|
target: { value: 'Keyboard App' },
|
||||||
})
|
})
|
||||||
@ -237,9 +234,9 @@ describe('CreateAppModal', () => {
|
|||||||
expect(mockCreateApp).toHaveBeenCalledWith({
|
expect(mockCreateApp).toHaveBeenCalledWith({
|
||||||
name: 'Keyboard App',
|
name: 'Keyboard App',
|
||||||
description: 'Created from shortcut',
|
description: 'Created from shortcut',
|
||||||
icon_type: 'image',
|
icon_type: 'emoji',
|
||||||
icon: 'file-1',
|
icon: '🤖',
|
||||||
icon_background: undefined,
|
icon_background: '#E4FBCC',
|
||||||
mode: AppModeEnum.CHAT,
|
mode: AppModeEnum.CHAT,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -254,7 +251,8 @@ describe('CreateAppModal', () => {
|
|||||||
expect(mockCreateApp).not.toHaveBeenCalled()
|
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({
|
mockUseProviderContext.mockReturnValue({
|
||||||
plan: {
|
plan: {
|
||||||
type: AppModeEnum.ADVANCED_CHAT,
|
type: AppModeEnum.ADVANCED_CHAT,
|
||||||
@ -267,11 +265,16 @@ describe('CreateAppModal', () => {
|
|||||||
|
|
||||||
renderModal()
|
renderModal()
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('open-icon-picker'))
|
await user.click(screen.getByText('open-icon-picker'))
|
||||||
expect(screen.getByText('select-image-icon')).toBeInTheDocument()
|
await waitFor(() => {
|
||||||
fireEvent.click(screen.getByText('close-icon-picker'))
|
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)?.()
|
ahooksMocks.keyPressHandlers.at(-1)?.()
|
||||||
|
|
||||||
|
|||||||
@ -220,12 +220,13 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||||||
/>
|
/>
|
||||||
{showAppIconPicker && (
|
{showAppIconPicker && (
|
||||||
<AppIconPicker
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
|
initialEmoji={appIcon.type === 'emoji'
|
||||||
|
? { icon: appIcon.icon, background: appIcon.background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
onSelect={(payload) => {
|
onSelect={(payload) => {
|
||||||
setAppIcon(payload)
|
setAppIcon(payload)
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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 userEvent from '@testing-library/user-event'
|
||||||
import DuplicateAppModal from '../index'
|
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<string, unknown>) => void, onClose: () => void }) => (
|
|
||||||
<div data-testid="app-icon-picker">
|
|
||||||
<button type="button" onClick={() => onSelect({ type: 'image', fileId: 'file-1', url: 'https://example.com/icon.png' })}>select-icon</button>
|
|
||||||
<button type="button" onClick={onClose}>close-icon-picker</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('DuplicateAppModal', () => {
|
describe('DuplicateAppModal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
@ -94,14 +85,21 @@ describe('DuplicateAppModal', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await user.click(screen.getByText('open-icon-picker'))
|
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' }))
|
await user.click(screen.getByRole('button', { name: 'duplicate' }))
|
||||||
|
|
||||||
expect(onConfirm).toHaveBeenCalledWith({
|
expect(onConfirm).toHaveBeenCalledWith({
|
||||||
name: 'Demo App',
|
name: 'Demo App',
|
||||||
icon_type: 'image',
|
icon_type: 'emoji',
|
||||||
icon: 'file-1',
|
icon: '🤖',
|
||||||
icon_background: undefined,
|
icon_background: '#E4FBCC',
|
||||||
})
|
})
|
||||||
expect(onHide).toHaveBeenCalled()
|
expect(onHide).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -127,7 +125,7 @@ describe('DuplicateAppModal', () => {
|
|||||||
expect(onHide).toHaveBeenCalledTimes(1)
|
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 onConfirm = vi.fn()
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
@ -144,16 +142,32 @@ describe('DuplicateAppModal', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await user.click(screen.getByText('open-icon-picker'))
|
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('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' }))
|
await user.click(screen.getByRole('button', { name: 'duplicate' }))
|
||||||
|
|
||||||
expect(onConfirm).toHaveBeenCalledWith({
|
expect(onConfirm).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
name: 'Image App',
|
name: 'Image App',
|
||||||
icon_type: 'image',
|
icon_type: 'emoji',
|
||||||
icon: 'original-file',
|
icon: expect.any(String),
|
||||||
icon_background: undefined,
|
icon_background: '#E4FBCC',
|
||||||
})
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -109,15 +109,13 @@ const DuplicateAppModal = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
{showAppIconPicker && (
|
{showAppIconPicker && (
|
||||||
<AppIconPicker
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
|
initialEmoji={appIcon.type === 'emoji'
|
||||||
|
? { icon: appIcon.icon, background: appIcon.background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
onSelect={(payload) => {
|
onSelect={(payload) => {
|
||||||
setAppIcon(payload)
|
setAppIcon(payload)
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setAppIcon(icon_type === 'image'
|
|
||||||
? { type: 'image', url: icon_url!, fileId: icon }
|
|
||||||
: { type: 'emoji', icon, background: icon_background! })
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -478,22 +478,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||||||
<Button className="mr-2" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
<Button className="mr-2" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||||
<Button variant="primary" onClick={onClickSave} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
|
<Button variant="primary" onClick={onClickSave} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
|
||||||
</div>
|
</div>
|
||||||
{showAppIconPicker && (
|
|
||||||
<div onClick={e => e.stopPropagation()}>
|
|
||||||
<AppIconPicker
|
|
||||||
onSelect={(payload) => {
|
|
||||||
setAppIcon(payload)
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setAppIcon(createAppIcon(appInfo))
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
|
initialEmoji={appIcon.type === 'emoji'
|
||||||
|
? { icon: appIcon.icon, background: appIcon.background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
|
onSelect={setAppIcon}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}) => (
|
|
||||||
<div data-testid="app-icon-picker">
|
|
||||||
<button onClick={() => onSelect({ type: 'image', url: 'https://example.com/icon.png', fileId: 'file-id-1' })}>select-app-icon</button>
|
|
||||||
<button onClick={onClose}>close-app-icon-picker</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const createMockApp = (overrides: Partial<App> = {}): App => ({
|
const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||||
id: 'app-123',
|
id: 'app-123',
|
||||||
name: 'Demo App',
|
name: 'Demo App',
|
||||||
@ -315,17 +303,23 @@ describe('SwitchAppModal', () => {
|
|||||||
mockSwitchApp.mockResolvedValueOnce({ new_app_id: 'new-app-003' })
|
mockSwitchApp.mockResolvedValueOnce({ new_app_id: 'new-app-003' })
|
||||||
|
|
||||||
await user.click(screen.getByText('open-icon-picker'))
|
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 user.click(screen.getByRole('button', { name: 'app.switchStart' }))
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockSwitchApp).toHaveBeenCalledWith(expect.objectContaining({
|
expect(mockSwitchApp).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
appID: appDetail.id,
|
appID: appDetail.id,
|
||||||
icon_type: 'image',
|
icon_type: 'emoji',
|
||||||
icon: 'file-id-1',
|
icon: '🚀',
|
||||||
icon_background: undefined,
|
icon_background: '#E4FBCC',
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -335,9 +329,14 @@ describe('SwitchAppModal', () => {
|
|||||||
renderComponent()
|
renderComponent()
|
||||||
|
|
||||||
await user.click(screen.getByText('open-icon-picker'))
|
await user.click(screen.getByText('open-icon-picker'))
|
||||||
expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument()
|
await waitFor(() => {
|
||||||
await user.click(screen.getByText('close-app-icon-picker'))
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('app-icon-picker')).not.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'))
|
await user.click(screen.getByText('app.removeOriginal'))
|
||||||
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
|
||||||
|
|||||||
@ -149,15 +149,13 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
|||||||
</div>
|
</div>
|
||||||
{showAppIconPicker && (
|
{showAppIconPicker && (
|
||||||
<AppIconPicker
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
|
initialEmoji={appIcon.type === 'emoji'
|
||||||
|
? { icon: appIcon.icon, background: appIcon.background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
onSelect={(payload) => {
|
onSelect={(payload) => {
|
||||||
setAppIcon(payload)
|
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)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -125,11 +125,11 @@ describe('AppIconPicker', () => {
|
|||||||
|
|
||||||
const renderPicker = (props: Partial<ComponentProps<typeof AppIconPicker>> = {}) => {
|
const renderPicker = (props: Partial<ComponentProps<typeof AppIconPicker>> = {}) => {
|
||||||
const onSelect = vi.fn()
|
const onSelect = vi.fn()
|
||||||
const onClose = vi.fn()
|
const onOpenChange = vi.fn()
|
||||||
|
|
||||||
const { container } = render(<AppIconPicker onSelect={onSelect} onClose={onClose} {...props} />)
|
const { container } = render(<AppIconPicker open onOpenChange={onOpenChange} onSelect={onSelect} {...props} />)
|
||||||
|
|
||||||
return { onSelect, onClose, container }
|
return { onSelect, onOpenChange, container }
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -157,8 +157,9 @@ describe('AppIconPicker', () => {
|
|||||||
it('should render emoji and image tabs when upload is enabled', async () => {
|
it('should render emoji and image tabs when upload is enabled', async () => {
|
||||||
renderPicker()
|
renderPicker()
|
||||||
|
|
||||||
expect(await screen.findByText(/emoji/i))!.toBeInTheDocument()
|
expect(screen.getByRole('dialog', { name: /emoji/i })).toBeInTheDocument()
|
||||||
expect(screen.getByText(/image/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(/cancel/i))!.toBeInTheDocument()
|
||||||
expect(screen.getByText(/ok/i))!.toBeInTheDocument()
|
expect(screen.getByText(/ok/i))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@ -173,12 +174,12 @@ describe('AppIconPicker', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('User Interactions', () => {
|
describe('User Interactions', () => {
|
||||||
it('should call onClose when cancel is clicked', async () => {
|
it('should close when cancel is clicked', async () => {
|
||||||
const { onClose } = renderPicker()
|
const { onOpenChange } = renderPicker()
|
||||||
|
|
||||||
await userEvent.click(screen.getByText(/cancel/i))
|
await userEvent.click(screen.getByText(/cancel/i))
|
||||||
|
|
||||||
expect(onClose).toHaveBeenCalledTimes(1)
|
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should switch between emoji and image tabs', async () => {
|
it('should switch between emoji and image tabs', async () => {
|
||||||
@ -187,7 +188,7 @@ describe('AppIconPicker', () => {
|
|||||||
await userEvent.click(screen.getByText(/image/i))
|
await userEvent.click(screen.getByText(/image/i))
|
||||||
expect(screen.getByText(/drop.*here/i))!.toBeInTheDocument()
|
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()
|
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 () => {
|
it('should not call onSelect when no emoji has been selected', async () => {
|
||||||
const { onSelect } = renderPicker()
|
const { onSelect } = renderPicker()
|
||||||
|
|
||||||
|
|||||||
@ -48,20 +48,20 @@ const AppIconPickerDemo = () => {
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{open && (
|
<AppIconPicker
|
||||||
<AppIconPicker
|
open={open}
|
||||||
onSelect={(result) => {
|
onOpenChange={setOpen}
|
||||||
setSelection(result)
|
onSelect={setSelection}
|
||||||
setOpen(false)
|
/>
|
||||||
}}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Playground: Story = {
|
export const Playground: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
onOpenChange: () => {},
|
||||||
|
},
|
||||||
render: () => <AppIconPickerDemo />,
|
render: () => <AppIconPickerDemo />,
|
||||||
parameters: {
|
parameters: {
|
||||||
docs: {
|
docs: {
|
||||||
@ -74,15 +74,11 @@ const [selection, setSelection] = useState<AppIconSelection | null>(null)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button onClick={() => setOpen(true)}>Choose icon…</button>
|
<button onClick={() => setOpen(true)}>Choose icon…</button>
|
||||||
{open && (
|
<AppIconPicker
|
||||||
<AppIconPicker
|
open={open}
|
||||||
onSelect={(result) => {
|
onOpenChange={setOpen}
|
||||||
setSelection(result)
|
onSelect={setSelection}
|
||||||
setOpen(false)
|
/>
|
||||||
}}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
`.trim(),
|
`.trim(),
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import type { FC } from 'react'
|
|
||||||
import type { Area } from 'react-easy-crop'
|
import type { Area } from 'react-easy-crop'
|
||||||
import type { OnImageInput } from './ImageInput'
|
import type { OnImageInput } from './ImageInput'
|
||||||
import type { AppIconType, ImageFile } from '@/types/app'
|
import type { AppIconType, ImageFile } from '@/types/app'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
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 { RiImageCircleAiLine } from '@remixicon/react'
|
||||||
import { useCallback, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
||||||
import Divider from '../divider'
|
import Divider from '../divider'
|
||||||
|
import { defaultEmojiBackground } from '../emoji-picker/constants'
|
||||||
import EmojiPickerInner from '../emoji-picker/Inner'
|
import EmojiPickerInner from '../emoji-picker/Inner'
|
||||||
import { useLocalFileUploader } from '../image-uploader/hooks'
|
import { useLocalFileUploader } from '../image-uploader/hooks'
|
||||||
import ImageInput from './ImageInput'
|
import ImageInput from './ImageInput'
|
||||||
@ -31,8 +31,10 @@ export type AppIconImageSelection = {
|
|||||||
export type AppIconSelection = AppIconEmojiSelection | AppIconImageSelection
|
export type AppIconSelection = AppIconEmojiSelection | AppIconImageSelection
|
||||||
|
|
||||||
type AppIconPickerProps = {
|
type AppIconPickerProps = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
onSelect?: (payload: AppIconSelection) => void
|
onSelect?: (payload: AppIconSelection) => void
|
||||||
onClose?: () => void
|
enableImageUpload?: boolean
|
||||||
initialEmoji?: {
|
initialEmoji?: {
|
||||||
icon: string
|
icon: string
|
||||||
background?: string | null
|
background?: string | null
|
||||||
@ -40,11 +42,50 @@ type AppIconPickerProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppIconPicker: FC<AppIconPickerProps> = ({
|
function AppIconPicker({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
enableImageUpload = true,
|
||||||
initialEmoji,
|
initialEmoji,
|
||||||
}) => {
|
className,
|
||||||
|
}: AppIconPickerProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
{open
|
||||||
|
? (
|
||||||
|
<AppIconPickerContent
|
||||||
|
key={`${initialEmoji?.icon ?? ''}:${initialEmoji?.background ?? ''}`}
|
||||||
|
initialEmoji={initialEmoji}
|
||||||
|
enableImageUpload={enableImageUpload}
|
||||||
|
className={className}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { t } = useTranslation()
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@ -52,11 +93,17 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
|||||||
{ key: 'image', label: t('iconPicker.image', { ns: 'app' }), icon: <RiImageCircleAiLine className="size-4" /> },
|
{ key: 'image', label: t('iconPicker.image', { ns: 'app' }), icon: <RiImageCircleAiLine className="size-4" /> },
|
||||||
]
|
]
|
||||||
const [activeTab, setActiveTab] = useState<AppIconType>('emoji')
|
const [activeTab, setActiveTab] = useState<AppIconType>('emoji')
|
||||||
|
const showImageUpload = enableImageUpload && !DISABLE_UPLOAD_IMAGE_AS_ICON
|
||||||
|
|
||||||
const [emoji, setEmoji] = useState<{ emoji: string, background: string }>()
|
const [emoji, setEmoji] = useState<{ emoji: string, background: string } | undefined>(() => {
|
||||||
const handleSelectEmoji = useCallback((emoji: string, background: string) => {
|
if (!initialEmoji?.icon)
|
||||||
setEmoji({ emoji, background })
|
return undefined
|
||||||
}, [setEmoji])
|
|
||||||
|
return {
|
||||||
|
emoji: initialEmoji.icon,
|
||||||
|
background: initialEmoji.background ?? defaultEmojiBackground,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const [uploading, setUploading] = useState<boolean>()
|
const [uploading, setUploading] = useState<boolean>()
|
||||||
|
|
||||||
@ -71,6 +118,7 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
|||||||
fileId: imageFile.fileId,
|
fileId: imageFile.fileId,
|
||||||
url: imageFile.url,
|
url: imageFile.url,
|
||||||
})
|
})
|
||||||
|
onOpenChange(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -94,6 +142,7 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
|||||||
icon: emoji.emoji,
|
icon: emoji.emoji,
|
||||||
background: emoji.background,
|
background: emoji.background,
|
||||||
})
|
})
|
||||||
|
onOpenChange(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -111,54 +160,55 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open>
|
<DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', s.container, 'h-[min(462px,calc(100dvh-2rem))]! max-h-none! w-[362px]! p-0!', className)}>
|
||||||
<DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', s.container, 'h-[min(462px,calc(100dvh-2rem))]! max-h-none! w-[362px]! p-0!')}>
|
<DialogTitle className="sr-only">
|
||||||
|
{t('iconPicker.emoji', { ns: 'app' })}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
{!DISABLE_UPLOAD_IMAGE_AS_ICON && (
|
{showImageUpload && (
|
||||||
<div className="w-full p-2 pb-0">
|
<div className="w-full p-2 pb-0">
|
||||||
<div className="flex items-center justify-center gap-2 rounded-xl bg-background-body p-1 text-text-primary">
|
<div className="flex items-center justify-center gap-2 rounded-xl bg-background-body p-1 text-text-primary">
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-8 flex-1 shrink-0 items-center justify-center rounded-lg p-2 system-sm-medium text-text-tertiary',
|
'flex h-8 flex-1 shrink-0 items-center justify-center rounded-lg p-2 system-sm-medium text-text-tertiary',
|
||||||
activeTab === tab.key && 'bg-components-main-nav-nav-button-bg-active text-text-accent shadow-md',
|
activeTab === tab.key && 'bg-components-main-nav-nav-button-bg-active text-text-accent shadow-md',
|
||||||
)}
|
)}
|
||||||
onClick={() => setActiveTab(tab.key as AppIconType)}
|
onClick={() => setActiveTab(tab.key as AppIconType)}
|
||||||
>
|
>
|
||||||
{tab.icon}
|
{tab.icon}
|
||||||
{' '}
|
{' '}
|
||||||
|
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'emoji' && (
|
|
||||||
<EmojiPickerInner
|
|
||||||
className={cn('flex-1 overflow-hidden pt-2')}
|
|
||||||
emoji={initialEmoji?.icon}
|
|
||||||
background={initialEmoji?.background ?? undefined}
|
|
||||||
onSelect={handleSelectEmoji}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeTab === 'image' && <ImageInput className={cn('flex-1 overflow-hidden')} onImageInput={handleImageInput} />}
|
|
||||||
|
|
||||||
<Divider className="m-0" />
|
|
||||||
<div className="flex w-full items-center justify-center gap-2 p-3">
|
|
||||||
<Button className="w-full" onClick={() => onClose?.()}>
|
|
||||||
{t('iconPicker.cancel', { ns: 'app' })}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="primary" className="w-full" disabled={uploading} loading={uploading} onClick={handleSelect}>
|
|
||||||
{t('iconPicker.ok', { ns: 'app' })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
)}
|
||||||
</Dialog>
|
|
||||||
|
{activeTab === 'emoji' && (
|
||||||
|
<EmojiPickerInner
|
||||||
|
className={cn('flex-1 overflow-hidden pt-2')}
|
||||||
|
emoji={initialEmoji?.icon}
|
||||||
|
background={initialEmoji?.background ?? undefined}
|
||||||
|
onSelect={(emoji, background) => setEmoji({ emoji, background })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === 'image' && <ImageInput className={cn('flex-1 overflow-hidden')} onImageInput={handleImageInput} />}
|
||||||
|
|
||||||
|
<Divider className="m-0" />
|
||||||
|
<div className="flex w-full items-center justify-center gap-2 p-3">
|
||||||
|
<Button className="w-full" onClick={() => onOpenChange(false)}>
|
||||||
|
{t('iconPicker.cancel', { ns: 'app' })}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="primary" className="w-full" disabled={uploading} loading={uploading} onClick={handleSelect}>
|
||||||
|
{t('iconPicker.ok', { ns: 'app' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { EmojiMartData } from '@emoji-mart/data'
|
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 data from '@emoji-mart/data'
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
@ -12,31 +12,10 @@ import { useState } from 'react'
|
|||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import { searchEmoji } from '@/utils/emoji'
|
import { searchEmoji } from '@/utils/emoji'
|
||||||
|
import { backgroundColors, defaultEmojiBackground } from './constants'
|
||||||
|
|
||||||
init({ data })
|
init({ data })
|
||||||
|
|
||||||
const backgroundColors = [
|
|
||||||
'#FFEAD5',
|
|
||||||
'#E4FBCC',
|
|
||||||
'#D3F8DF',
|
|
||||||
'#E0F2FE',
|
|
||||||
|
|
||||||
'#E0EAFF',
|
|
||||||
'#EFF1F5',
|
|
||||||
'#FBE8FF',
|
|
||||||
'#FCE7F6',
|
|
||||||
|
|
||||||
'#FEF7C3',
|
|
||||||
'#E6F4D7',
|
|
||||||
'#D5F5F6',
|
|
||||||
'#D1E9FF',
|
|
||||||
|
|
||||||
'#D1E0FF',
|
|
||||||
'#D5D9EB',
|
|
||||||
'#ECE9FE',
|
|
||||||
'#FFE4E8',
|
|
||||||
]
|
|
||||||
|
|
||||||
type IEmojiPickerInnerProps = {
|
type IEmojiPickerInnerProps = {
|
||||||
emoji?: string
|
emoji?: string
|
||||||
background?: string
|
background?: string
|
||||||
@ -44,28 +23,32 @@ type IEmojiPickerInnerProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
function EmojiPickerInner({
|
||||||
emoji,
|
emoji,
|
||||||
background,
|
background,
|
||||||
onSelect,
|
onSelect,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}: IEmojiPickerInnerProps) {
|
||||||
const { categories } = data as EmojiMartData
|
const { categories } = data as EmojiMartData
|
||||||
const [selectedEmoji, setSelectedEmoji] = useState(emoji || '')
|
const [selectedEmoji, setSelectedEmoji] = useState(emoji || '')
|
||||||
const [selectedBackground, setSelectedBackground] = useState(background || backgroundColors[0])
|
const [selectedBackground, setSelectedBackground] = useState(background || defaultEmojiBackground)
|
||||||
const [showStyleColors, setShowStyleColors] = useState(!!emoji)
|
const [showStyleColors, setShowStyleColors] = useState(!!emoji)
|
||||||
|
|
||||||
const [searchedEmojis, setSearchedEmojis] = useState<string[]>([])
|
const [searchedEmojis, setSearchedEmojis] = useState<string[]>([])
|
||||||
const [isSearching, setIsSearching] = useState(false)
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
const styleColorsLabelId = React.useId()
|
const styleColorsLabelId = React.useId()
|
||||||
|
|
||||||
React.useEffect(() => {
|
const handleEmojiSelect = (emoji: string) => {
|
||||||
if (selectedEmoji) {
|
setSelectedEmoji(emoji)
|
||||||
/* v8 ignore next 2 - @preserve */
|
setShowStyleColors(true)
|
||||||
if (selectedBackground)
|
onSelect?.(emoji, selectedBackground)
|
||||||
onSelect?.(selectedEmoji, selectedBackground)
|
}
|
||||||
}
|
|
||||||
}, [onSelect, selectedEmoji, selectedBackground])
|
const handleBackgroundSelect = (background: string) => {
|
||||||
|
setSelectedBackground(background)
|
||||||
|
if (selectedEmoji)
|
||||||
|
onSelect?.(selectedEmoji, background)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(className, 'flex flex-col')}>
|
<div className={cn(className, 'flex flex-col')}>
|
||||||
@ -108,8 +91,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||||||
aria-label={emoji}
|
aria-label={emoji}
|
||||||
className="inline-flex size-10 items-center justify-center rounded-lg border-none bg-transparent p-0"
|
className="inline-flex size-10 items-center justify-center rounded-lg border-none bg-transparent p-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEmoji(emoji)
|
handleEmojiSelect(emoji)
|
||||||
setShowStyleColors(true)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex size-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
<span className="flex size-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
||||||
@ -136,8 +118,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||||||
aria-label={emoji}
|
aria-label={emoji}
|
||||||
className="inline-flex size-10 items-center justify-center rounded-lg border-none bg-transparent p-0"
|
className="inline-flex size-10 items-center justify-center rounded-lg border-none bg-transparent p-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEmoji(emoji)
|
handleEmojiSelect(emoji)
|
||||||
setShowStyleColors(true)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex size-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
<span className="flex size-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
||||||
@ -194,7 +175,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedBackground(color)
|
handleBackgroundSelect(color)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -46,13 +46,11 @@ describe('EmojiPickerInner', () => {
|
|||||||
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
|
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('initializes selected emoji and background when provided', async () => {
|
it('initializes selected emoji and background when provided', () => {
|
||||||
render(<EmojiPickerInner emoji="rabbit" background="#E4FBCC" onSelect={mockOnSelect} />)
|
render(<EmojiPickerInner emoji="rabbit" background="#E4FBCC" onSelect={mockOnSelect} />)
|
||||||
|
|
||||||
expect(screen.getByText('Choose Style'))!.toBeInTheDocument()
|
expect(screen.getByText('Choose Style'))!.toBeInTheDocument()
|
||||||
await waitFor(() => {
|
expect(mockOnSelect).not.toHaveBeenCalled()
|
||||||
expect(mockOnSelect).toHaveBeenCalledWith('rabbit', '#E4FBCC')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -26,26 +26,27 @@ vi.mock('@/utils/emoji', () => ({
|
|||||||
|
|
||||||
describe('EmojiPicker', () => {
|
describe('EmojiPicker', () => {
|
||||||
const mockOnSelect = vi.fn()
|
const mockOnSelect = vi.fn()
|
||||||
const mockOnClose = vi.fn()
|
const mockOnOpenChange = vi.fn()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('renders nothing when isModal is false', () => {
|
it('renders nothing when closed', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<EmojiPicker isModal={false} />,
|
<EmojiPicker open={false} onOpenChange={mockOnOpenChange} />,
|
||||||
)
|
)
|
||||||
expect(container.firstChild).toBeNull()
|
expect(container.firstChild).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders modal when isModal is true', async () => {
|
it('renders modal when open', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
<EmojiPicker isModal={true} />,
|
<EmojiPicker open onOpenChange={mockOnOpenChange} />,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
expect(screen.getByRole('dialog', { name: /Emoji/i }))!.toBeInTheDocument()
|
||||||
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
|
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
|
||||||
expect(screen.getByText(/Cancel/i))!.toBeInTheDocument()
|
expect(screen.getByText(/Cancel/i))!.toBeInTheDocument()
|
||||||
expect(screen.getByText(/OK/i))!.toBeInTheDocument()
|
expect(screen.getByText(/OK/i))!.toBeInTheDocument()
|
||||||
@ -54,7 +55,7 @@ describe('EmojiPicker', () => {
|
|||||||
it('OK button is disabled initially', async () => {
|
it('OK button is disabled initially', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
<EmojiPicker />,
|
<EmojiPicker open onOpenChange={mockOnOpenChange} />,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const okButton = screen.getByText(/OK/i).closest('button')
|
const okButton = screen.getByText(/OK/i).closest('button')
|
||||||
@ -65,7 +66,7 @@ describe('EmojiPicker', () => {
|
|||||||
const customClass = 'custom-wrapper-class'
|
const customClass = 'custom-wrapper-class'
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
<EmojiPicker className={customClass} />,
|
<EmojiPicker open onOpenChange={mockOnOpenChange} className={customClass} />,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const dialog = screen.getByRole('dialog')
|
const dialog = screen.getByRole('dialog')
|
||||||
@ -77,7 +78,7 @@ describe('EmojiPicker', () => {
|
|||||||
it('calls onSelect with selected emoji and background when OK is clicked', async () => {
|
it('calls onSelect with selected emoji and background when OK is clicked', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
<EmojiPicker onSelect={mockOnSelect} />,
|
<EmojiPicker open onOpenChange={mockOnOpenChange} onSelect={mockOnSelect} />,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -95,10 +96,10 @@ describe('EmojiPicker', () => {
|
|||||||
expect(mockOnSelect).toHaveBeenCalledWith(expect.any(String), expect.any(String))
|
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 () => {
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
<EmojiPicker onClose={mockOnClose} />,
|
<EmojiPicker open onOpenChange={mockOnOpenChange} />,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -107,7 +108,7 @@ describe('EmojiPicker', () => {
|
|||||||
fireEvent.click(cancelButton)
|
fireEvent.click(cancelButton)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(mockOnClose).toHaveBeenCalled()
|
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
23
web/app/components/base/emoji-picker/constants.ts
Normal file
23
web/app/components/base/emoji-picker/constants.ts
Normal file
@ -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]!
|
||||||
@ -47,20 +47,20 @@ const EmojiPickerDemo = () => {
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{open && (
|
<EmojiPicker
|
||||||
<EmojiPicker
|
open={open}
|
||||||
onSelect={(emoji, background) => {
|
onOpenChange={setOpen}
|
||||||
setSelection({ emoji, background })
|
onSelect={(emoji, background) => setSelection({ emoji, background })}
|
||||||
setOpen(false)
|
/>
|
||||||
}}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Playground: Story = {
|
export const Playground: Story = {
|
||||||
|
args: {
|
||||||
|
open: false,
|
||||||
|
onOpenChange: () => {},
|
||||||
|
},
|
||||||
render: () => <EmojiPickerDemo />,
|
render: () => <EmojiPickerDemo />,
|
||||||
parameters: {
|
parameters: {
|
||||||
docs: {
|
docs: {
|
||||||
@ -73,15 +73,11 @@ const [selection, setSelection] = useState<{ emoji: string; background: string }
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button onClick={() => setOpen(true)}>Open emoji picker…</button>
|
<button onClick={() => setOpen(true)}>Open emoji picker…</button>
|
||||||
{open && (
|
<EmojiPicker
|
||||||
<EmojiPicker
|
open={open}
|
||||||
onSelect={(emoji, background) => {
|
onOpenChange={setOpen}
|
||||||
setSelection({ emoji, background })
|
onSelect={(emoji, background) => setSelection({ emoji, background })}
|
||||||
setOpen(false)
|
/>
|
||||||
}}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
`.trim(),
|
`.trim(),
|
||||||
|
|||||||
@ -1,75 +1,95 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
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 * as React from 'react'
|
import { useState } from 'react'
|
||||||
import { useCallback, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import EmojiPickerInner from './Inner'
|
import EmojiPickerInner from './Inner'
|
||||||
|
|
||||||
type IEmojiPickerProps = {
|
type EmojiPickerProps = {
|
||||||
isModal?: boolean
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
onSelect?: (emoji: string, background: string) => void
|
onSelect?: (emoji: string, background: string) => void
|
||||||
onClose?: () => void
|
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiPicker: FC<IEmojiPickerProps> = ({
|
function EmojiPicker({
|
||||||
isModal = true,
|
open,
|
||||||
|
onOpenChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
|
||||||
className,
|
className,
|
||||||
}) => {
|
}: EmojiPickerProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
{open
|
||||||
|
? (
|
||||||
|
<EmojiPickerContent
|
||||||
|
className={className}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmojiPickerContentProps = {
|
||||||
|
className?: string
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onSelect?: (emoji: string, background: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiPickerContent({
|
||||||
|
className,
|
||||||
|
onOpenChange,
|
||||||
|
onSelect,
|
||||||
|
}: EmojiPickerContentProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [selectedEmoji, setSelectedEmoji] = useState('')
|
const [selectedEmoji, setSelectedEmoji] = useState('')
|
||||||
const [selectedBackground, setSelectedBackground] = useState<string>()
|
const [selectedBackground, setSelectedBackground] = useState<string>()
|
||||||
|
|
||||||
const handleSelectEmoji = useCallback((emoji: string, background: string) => {
|
return (
|
||||||
setSelectedEmoji(emoji)
|
<DialogContent
|
||||||
setSelectedBackground(background)
|
className={cn(
|
||||||
}, [setSelectedEmoji, setSelectedBackground])
|
'max-h-none w-full overflow-hidden! text-left align-middle',
|
||||||
|
'flex max-h-[552px] flex-col rounded-xl border-[0.5px] border-divider-subtle p-0 shadow-xl',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DialogTitle className="sr-only">
|
||||||
|
{t('iconPicker.emoji', { ns: 'app' })}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
return isModal
|
<EmojiPickerInner
|
||||||
? (
|
className="pt-3"
|
||||||
<Dialog open>
|
onSelect={(emoji, background) => {
|
||||||
<DialogContent
|
setSelectedEmoji(emoji)
|
||||||
className={cn(
|
setSelectedBackground(background)
|
||||||
'max-h-none w-full overflow-hidden! text-left align-middle',
|
}}
|
||||||
'flex max-h-[552px] flex-col rounded-xl border-[0.5px] border-divider-subtle p-0 shadow-xl',
|
/>
|
||||||
className,
|
<Divider className="mt-3 mb-0" />
|
||||||
)}
|
<div className="flex w-full items-center justify-center gap-2 p-3">
|
||||||
>
|
<Button
|
||||||
|
className="w-full"
|
||||||
<EmojiPickerInner
|
onClick={() => onOpenChange(false)}
|
||||||
className="pt-3"
|
>
|
||||||
onSelect={handleSelectEmoji}
|
{t('iconPicker.cancel', { ns: 'app' })}
|
||||||
/>
|
</Button>
|
||||||
<Divider className="mt-3 mb-0" />
|
<Button
|
||||||
<div className="flex w-full items-center justify-center gap-2 p-3">
|
disabled={selectedEmoji === '' || !selectedBackground}
|
||||||
<Button
|
variant="primary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose?.()
|
onSelect?.(selectedEmoji, selectedBackground!)
|
||||||
}}
|
onOpenChange(false)
|
||||||
>
|
}}
|
||||||
{t('iconPicker.cancel', { ns: 'app' })}
|
>
|
||||||
</Button>
|
{t('iconPicker.ok', { ns: 'app' })}
|
||||||
<Button
|
</Button>
|
||||||
disabled={selectedEmoji === '' || !selectedBackground}
|
</div>
|
||||||
variant="primary"
|
</DialogContent>
|
||||||
className="w-full"
|
)
|
||||||
onClick={() => {
|
|
||||||
onSelect?.(selectedEmoji, selectedBackground!)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('iconPicker.ok', { ns: 'app' })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
: <></>
|
|
||||||
}
|
}
|
||||||
export default EmojiPicker
|
export default EmojiPicker
|
||||||
|
|||||||
@ -81,7 +81,7 @@ export const useImageFiles = () => {
|
|||||||
filesRef.current = newFiles
|
filesRef.current = newFiles
|
||||||
setFiles(newFiles)
|
setFiles(newFiles)
|
||||||
},
|
},
|
||||||
}, !!params.token)
|
}, !!params?.token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
@ -145,13 +145,13 @@ export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useL
|
|||||||
toast.error(errorMessage)
|
toast.error(errorMessage)
|
||||||
onUpload({ ...imageFile, progress: -1 })
|
onUpload({ ...imageFile, progress: -1 })
|
||||||
},
|
},
|
||||||
}, !!params.token)
|
}, !!params?.token)
|
||||||
}, false)
|
}, false)
|
||||||
reader.addEventListener('error', () => {
|
reader.addEventListener('error', () => {
|
||||||
toast.error(t('imageUploader.uploadFromComputerReadError', { ns: 'common' }))
|
toast.error(t('imageUploader.uploadFromComputerReadError', { ns: 'common' }))
|
||||||
}, false)
|
}, false)
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}, [disabled, limit, t, onUpload, params.token])
|
}, [disabled, limit, t, onUpload, params?.token])
|
||||||
return { disabled, handleLocalFileUpload }
|
return { disabled, handleLocalFileUpload }
|
||||||
}
|
}
|
||||||
type useClipboardUploaderProps = {
|
type useClipboardUploaderProps = {
|
||||||
|
|||||||
@ -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 (
|
|
||||||
<div data-testid="app-icon-picker">
|
|
||||||
<button data-testid="select-emoji" onClick={() => onSelect({ type: 'emoji', icon: '🎯', background: '#FFEAD5' })}>
|
|
||||||
Select Emoji
|
|
||||||
</button>
|
|
||||||
<button data-testid="select-image" onClick={() => onSelect({ type: 'image', fileId: 'new-file-id', url: 'https://new-icon.com/icon.png' })}>
|
|
||||||
Select Image
|
|
||||||
</button>
|
|
||||||
<button data-testid="close-picker" onClick={onClose}>
|
|
||||||
Close Picker
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const createPipelineTemplate = (overrides: Partial<PipelineTemplate> = {}): PipelineTemplate => ({
|
const createPipelineTemplate = (overrides: Partial<PipelineTemplate> = {}): PipelineTemplate => ({
|
||||||
id: 'pipeline-1',
|
id: 'pipeline-1',
|
||||||
name: 'Test Pipeline',
|
name: 'Test Pipeline',
|
||||||
@ -96,8 +69,6 @@ describe('EditPipelineInfo', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockToastError.mockReset()
|
mockToastError.mockReset()
|
||||||
_mockOnSelect = undefined
|
|
||||||
_mockOnClose = undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
@ -303,7 +274,7 @@ describe('EditPipelineInfo', () => {
|
|||||||
// Open icon picker
|
// Open icon picker
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
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 () => {
|
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 = {
|
const props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
pipeline: createImagePipelineTemplate(),
|
pipeline: createImagePipelineTemplate(),
|
||||||
@ -368,13 +339,14 @@ describe('EditPipelineInfo', () => {
|
|||||||
// Open picker
|
// Open picker
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
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
|
// Close without selection - should revert to original image icon
|
||||||
const closeButton = screen.getByTestId('close-picker')
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ }))
|
||||||
fireEvent.click(closeButton)
|
|
||||||
|
|
||||||
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 () => {
|
it('should switch from image icon to emoji icon when selected', async () => {
|
||||||
@ -392,8 +364,11 @@ describe('EditPipelineInfo', () => {
|
|||||||
// Open picker and select emoji
|
// Open picker and select emoji
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
fireEvent.click(appIcon!)
|
||||||
const selectEmojiButton = screen.getByTestId('select-emoji')
|
const emojiButton = document.querySelector('em-emoji')?.closest('button')
|
||||||
fireEvent.click(selectEmojiButton)
|
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)
|
const saveButton = screen.getByText(/operation\.save/i)
|
||||||
fireEvent.click(saveButton)
|
fireEvent.click(saveButton)
|
||||||
@ -403,7 +378,8 @@ describe('EditPipelineInfo', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
icon_info: expect.objectContaining({
|
icon_info: expect.objectContaining({
|
||||||
icon_type: 'emoji',
|
icon_type: 'emoji',
|
||||||
icon: '🎯',
|
icon: expect.any(String),
|
||||||
|
icon_background: '#E4FBCC',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@ -411,34 +387,14 @@ describe('EditPipelineInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should switch from emoji icon to image icon when selected', async () => {
|
it('should switch to the image tab in the real picker', () => {
|
||||||
mockUpdatePipeline.mockImplementation((_data, callbacks) => {
|
|
||||||
callbacks.onSuccess()
|
|
||||||
return Promise.resolve()
|
|
||||||
})
|
|
||||||
|
|
||||||
const { container } = render(<EditPipelineInfo {...defaultProps} />)
|
const { container } = render(<EditPipelineInfo {...defaultProps} />)
|
||||||
|
|
||||||
// Open picker and select image
|
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
fireEvent.click(appIcon!)
|
||||||
const selectImageButton = screen.getByTestId('select-image')
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.image/ }))
|
||||||
fireEvent.click(selectImageButton)
|
|
||||||
|
|
||||||
const saveButton = screen.getByText(/operation\.save/i)
|
expect(screen.getByRole('button', { name: /iconPicker\.ok/ })).toBeInTheDocument()
|
||||||
fireEvent.click(saveButton)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockUpdatePipeline).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
icon_info: expect.objectContaining({
|
|
||||||
icon_type: 'image',
|
|
||||||
icon: 'new-file-id',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.any(Object),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -446,7 +402,7 @@ describe('EditPipelineInfo', () => {
|
|||||||
describe('AppIconPicker', () => {
|
describe('AppIconPicker', () => {
|
||||||
it('should not show picker initially', () => {
|
it('should not show picker initially', () => {
|
||||||
render(<EditPipelineInfo {...defaultProps} />)
|
render(<EditPipelineInfo {...defaultProps} />)
|
||||||
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should open picker when icon is clicked', () => {
|
it('should open picker when icon is clicked', () => {
|
||||||
@ -454,43 +410,42 @@ describe('EditPipelineInfo', () => {
|
|||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
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(<EditPipelineInfo {...defaultProps} />)
|
const { container } = render(<EditPipelineInfo {...defaultProps} />)
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
fireEvent.click(appIcon!)
|
||||||
|
|
||||||
const selectEmojiButton = screen.getByTestId('select-emoji')
|
fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' }))
|
||||||
fireEvent.click(selectEmojiButton)
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||||
|
|
||||||
// Picker should close
|
await waitFor(() => {
|
||||||
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
|
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(<EditPipelineInfo {...defaultProps} />)
|
const { container } = render(<EditPipelineInfo {...defaultProps} />)
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
fireEvent.click(appIcon!)
|
||||||
|
|
||||||
const selectImageButton = screen.getByTestId('select-image')
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.image/ }))
|
||||||
fireEvent.click(selectImageButton)
|
|
||||||
|
|
||||||
// Picker should close
|
expect(screen.getByRole('button', { name: /iconPicker\.ok/ })).toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('app-icon-picker')).not.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(<EditPipelineInfo {...defaultProps} />)
|
const { container } = render(<EditPipelineInfo {...defaultProps} />)
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
fireEvent.click(appIcon!)
|
||||||
|
|
||||||
const closeButton = screen.getByTestId('close-picker')
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ }))
|
||||||
fireEvent.click(closeButton)
|
|
||||||
|
|
||||||
// Picker should close
|
await waitFor(() => {
|
||||||
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should save with new emoji icon selection', async () => {
|
it('should save with new emoji icon selection', async () => {
|
||||||
@ -504,8 +459,8 @@ describe('EditPipelineInfo', () => {
|
|||||||
// Open picker and select new emoji
|
// Open picker and select new emoji
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
fireEvent.click(appIcon!)
|
||||||
const selectEmojiButton = screen.getByTestId('select-emoji')
|
fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' }))
|
||||||
fireEvent.click(selectEmojiButton)
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||||
|
|
||||||
const saveButton = screen.getByText(/operation\.save/i)
|
const saveButton = screen.getByText(/operation\.save/i)
|
||||||
fireEvent.click(saveButton)
|
fireEvent.click(saveButton)
|
||||||
@ -515,8 +470,8 @@ describe('EditPipelineInfo', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
icon_info: expect.objectContaining({
|
icon_info: expect.objectContaining({
|
||||||
icon_type: 'emoji',
|
icon_type: 'emoji',
|
||||||
icon: '🎯',
|
icon: '📊',
|
||||||
icon_background: '#FFEAD5',
|
icon_background: '#E4FBCC',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expect.any(Object),
|
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) => {
|
mockUpdatePipeline.mockImplementation((_data, callbacks) => {
|
||||||
callbacks.onSuccess()
|
callbacks.onSuccess()
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
})
|
})
|
||||||
|
|
||||||
const { container } = render(<EditPipelineInfo {...defaultProps} />)
|
const { container } = render(<EditPipelineInfo {...defaultProps} pipeline={createImagePipelineTemplate()} />)
|
||||||
|
|
||||||
// Open picker and select new image
|
|
||||||
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
const appIcon = container.querySelector('[class*="cursor-pointer"]')
|
||||||
fireEvent.click(appIcon!)
|
fireEvent.click(appIcon!)
|
||||||
const selectImageButton = screen.getByTestId('select-image')
|
const emojiButton = document.querySelector('em-emoji')?.closest('button')
|
||||||
fireEvent.click(selectImageButton)
|
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)
|
const saveButton = screen.getByText(/operation\.save/i)
|
||||||
fireEvent.click(saveButton)
|
fireEvent.click(saveButton)
|
||||||
@ -545,9 +502,9 @@ describe('EditPipelineInfo', () => {
|
|||||||
expect(mockUpdatePipeline).toHaveBeenCalledWith(
|
expect(mockUpdatePipeline).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
icon_info: expect.objectContaining({
|
icon_info: expect.objectContaining({
|
||||||
icon_type: 'image',
|
icon_type: 'emoji',
|
||||||
icon: 'new-file-id',
|
icon: expect.any(String),
|
||||||
icon_url: 'https://new-icon.com/icon.png',
|
icon_background: '#E4FBCC',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
|||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useRef, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||||
@ -31,11 +31,6 @@ const EditPipelineInfo = ({
|
|||||||
)
|
)
|
||||||
const [description, setDescription] = useState(pipeline.description)
|
const [description, setDescription] = useState(pipeline.description)
|
||||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||||
const previousAppIcon = useRef<AppIconSelection>(
|
|
||||||
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<HTMLInputElement>) => {
|
const handleAppNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
@ -44,17 +39,10 @@ const EditPipelineInfo = ({
|
|||||||
|
|
||||||
const handleOpenAppIconPicker = useCallback(() => {
|
const handleOpenAppIconPicker = useCallback(() => {
|
||||||
setShowAppIconPicker(true)
|
setShowAppIconPicker(true)
|
||||||
previousAppIcon.current = appIcon
|
}, [])
|
||||||
}, [appIcon])
|
|
||||||
|
|
||||||
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
|
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
|
||||||
setAppIcon(icon)
|
setAppIcon(icon)
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleCloseAppIconPicker = useCallback(() => {
|
|
||||||
setAppIcon(previousAppIcon.current)
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDescriptionChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleDescriptionChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
@ -156,8 +144,12 @@ const EditPipelineInfo = ({
|
|||||||
</div>
|
</div>
|
||||||
{showAppIconPicker && (
|
{showAppIconPicker && (
|
||||||
<AppIconPicker
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
|
initialEmoji={appIcon.type === 'emoji'
|
||||||
|
? { icon: appIcon.icon, background: appIcon.background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
onSelect={handleSelectAppIcon}
|
onSelect={handleSelectAppIcon}
|
||||||
onClose={handleCloseAppIconPicker}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { DataSet } from '@/models/datasets'
|
import type { DataSet } from '@/models/datasets'
|
||||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
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 { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||||
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
|
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
|
||||||
import RenameDatasetModal from '../index'
|
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
|
|
||||||
}) => (
|
|
||||||
<div data-testid="app-icon-picker">
|
|
||||||
<button data-testid="select-emoji" onClick={() => onSelect?.({ type: 'emoji', icon: '🚀', background: '#E0F2FE' })}>
|
|
||||||
Select Emoji
|
|
||||||
</button>
|
|
||||||
<button data-testid="select-image" onClick={() => onSelect?.({ type: 'image', fileId: 'new-file', url: 'https://new.png' })}>
|
|
||||||
Select Image
|
|
||||||
</button>
|
|
||||||
<button data-testid="close-picker" onClick={onClose}>Close</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// The mock returns 'ns.key' format, e.g., 'common.operation.cancel'
|
// The mock returns 'ns.key' format, e.g., 'common.operation.cancel'
|
||||||
|
|
||||||
describe('RenameDatasetModal', () => {
|
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
|
// 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')
|
const appIcon = screen.getByTestId('app-icon')
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(appIcon)
|
fireEvent.click(appIcon)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Picker should now be visible
|
await waitFor(() => {
|
||||||
// Picker should now be visible
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('app-icon-picker'))!.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(<RenameDatasetModal {...defaultProps} />)
|
render(<RenameDatasetModal {...defaultProps} />)
|
||||||
|
|
||||||
// Open picker
|
await user.click(screen.getByTestId('app-icon'))
|
||||||
const appIcon = screen.getByTestId('app-icon')
|
await waitFor(() => {
|
||||||
await act(async () => {
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||||
fireEvent.click(appIcon)
|
|
||||||
})
|
})
|
||||||
|
await user.click(screen.getByRole('button', { name: '#E4FBCC' }))
|
||||||
// Select emoji
|
await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||||
const selectEmojiBtn = screen.getByTestId('select-emoji')
|
await waitFor(() => {
|
||||||
await act(async () => {
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||||
fireEvent.click(selectEmojiBtn)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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
|
// Save and verify new icon is used
|
||||||
const saveButton = screen.getByText('common.operation.save')
|
const saveButton = screen.getByText('common.operation.save')
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@ -931,9 +879,9 @@ describe('RenameDatasetModal', () => {
|
|||||||
datasetId: 'dataset-1',
|
datasetId: 'dataset-1',
|
||||||
body: expect.objectContaining({
|
body: expect.objectContaining({
|
||||||
icon_info: {
|
icon_info: {
|
||||||
icon: '🚀',
|
icon: '📊',
|
||||||
icon_type: 'emoji',
|
icon_type: 'emoji',
|
||||||
icon_background: '#E0F2FE',
|
icon_background: '#E4FBCC',
|
||||||
icon_url: undefined,
|
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(<RenameDatasetModal {...defaultProps} />)
|
render(<RenameDatasetModal {...defaultProps} />)
|
||||||
|
|
||||||
// Open picker
|
await user.click(screen.getByTestId('app-icon'))
|
||||||
const appIcon = screen.getByTestId('app-icon')
|
await waitFor(() => {
|
||||||
await act(async () => {
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||||
fireEvent.click(appIcon)
|
})
|
||||||
|
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')
|
const saveButton = screen.getByText('common.operation.save')
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(saveButton)
|
fireEvent.click(saveButton)
|
||||||
@ -1001,10 +913,10 @@ describe('RenameDatasetModal', () => {
|
|||||||
datasetId: 'dataset-1',
|
datasetId: 'dataset-1',
|
||||||
body: expect.objectContaining({
|
body: expect.objectContaining({
|
||||||
icon_info: {
|
icon_info: {
|
||||||
icon: 'new-file',
|
icon: '📊',
|
||||||
icon_type: 'image',
|
icon_type: 'emoji',
|
||||||
icon_background: undefined,
|
icon_background: '#E0F2FE',
|
||||||
icon_url: 'https://new.png',
|
icon_url: undefined,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@ -1020,45 +932,14 @@ describe('RenameDatasetModal', () => {
|
|||||||
fireEvent.click(appIcon)
|
fireEvent.click(appIcon)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Close picker without selecting
|
const user = userEvent.setup()
|
||||||
const closeBtn = screen.getByTestId('close-picker')
|
await waitFor(() => {
|
||||||
await act(async () => {
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||||
fireEvent.click(closeBtn)
|
})
|
||||||
|
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
|
// Save and verify original icon is preserved
|
||||||
const saveButton = screen.getByText('common.operation.save')
|
const saveButton = screen.getByText('common.operation.save')
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
|||||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
import { useCallback, useRef, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Textarea from '@/app/components/base/textarea'
|
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: '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 || '' })
|
: { type: 'emoji' as const, icon: dataset.icon_info?.icon || '', background: dataset.icon_info?.icon_background || '' })
|
||||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||||
const previousAppIcon = useRef<AppIconSelection>(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(() => {
|
const handleOpenAppIconPicker = useCallback(() => {
|
||||||
setShowAppIconPicker(true)
|
setShowAppIconPicker(true)
|
||||||
previousAppIcon.current = appIcon
|
}, [])
|
||||||
}, [appIcon])
|
|
||||||
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
|
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
|
||||||
setAppIcon(icon)
|
setAppIcon(icon)
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}, [])
|
|
||||||
const handleCloseAppIconPicker = useCallback(() => {
|
|
||||||
setAppIcon(previousAppIcon.current)
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}, [])
|
}, [])
|
||||||
const onConfirm: MouseEventHandler = useCallback(async () => {
|
const onConfirm: MouseEventHandler = useCallback(async () => {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
@ -125,7 +116,16 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
|
|||||||
<Button className="mr-2" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
|
<Button className="mr-2" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||||
<Button disabled={loading} variant="primary" onClick={onConfirm}>{t('operation.save', { ns: 'common' })}</Button>
|
<Button disabled={loading} variant="primary" onClick={onConfirm}>{t('operation.save', { ns: 'common' })}</Button>
|
||||||
</div>
|
</div>
|
||||||
{showAppIconPicker && (<AppIconPicker onSelect={handleSelectAppIcon} onClose={handleCloseAppIconPicker} />)}
|
{showAppIconPicker && (
|
||||||
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
|
initialEmoji={appIcon.type === 'emoji'
|
||||||
|
? { icon: appIcon.icon, background: appIcon.background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
|
onSelect={handleSelectAppIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -127,7 +127,7 @@ describe('BasicInfoSection', () => {
|
|||||||
showAppIconPicker: false,
|
showAppIconPicker: false,
|
||||||
handleOpenAppIconPicker: vi.fn(),
|
handleOpenAppIconPicker: vi.fn(),
|
||||||
handleSelectAppIcon: vi.fn(),
|
handleSelectAppIcon: vi.fn(),
|
||||||
handleCloseAppIconPicker: vi.fn(),
|
setShowAppIconPicker: vi.fn(),
|
||||||
permission: DatasetPermission.onlyMe,
|
permission: DatasetPermission.onlyMe,
|
||||||
setPermission: vi.fn(),
|
setPermission: vi.fn(),
|
||||||
selectedMemberIDs: ['user-1'],
|
selectedMemberIDs: ['user-1'],
|
||||||
|
|||||||
@ -24,7 +24,7 @@ type BasicInfoSectionProps = {
|
|||||||
showAppIconPicker: boolean
|
showAppIconPicker: boolean
|
||||||
handleOpenAppIconPicker: () => void
|
handleOpenAppIconPicker: () => void
|
||||||
handleSelectAppIcon: (icon: AppIconSelection) => void
|
handleSelectAppIcon: (icon: AppIconSelection) => void
|
||||||
handleCloseAppIconPicker: () => void
|
setShowAppIconPicker: (show: boolean) => void
|
||||||
permission: DatasetPermission | undefined
|
permission: DatasetPermission | undefined
|
||||||
setPermission: (value: DatasetPermission | undefined) => void
|
setPermission: (value: DatasetPermission | undefined) => void
|
||||||
selectedMemberIDs: string[]
|
selectedMemberIDs: string[]
|
||||||
@ -43,7 +43,7 @@ const BasicInfoSection = ({
|
|||||||
showAppIconPicker,
|
showAppIconPicker,
|
||||||
handleOpenAppIconPicker,
|
handleOpenAppIconPicker,
|
||||||
handleSelectAppIcon,
|
handleSelectAppIcon,
|
||||||
handleCloseAppIconPicker,
|
setShowAppIconPicker,
|
||||||
permission,
|
permission,
|
||||||
setPermission,
|
setPermission,
|
||||||
selectedMemberIDs,
|
selectedMemberIDs,
|
||||||
@ -113,8 +113,12 @@ const BasicInfoSection = ({
|
|||||||
|
|
||||||
{showAppIconPicker && (
|
{showAppIconPicker && (
|
||||||
<AppIconPicker
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
|
initialEmoji={iconInfo.icon_type === 'emoji'
|
||||||
|
? { icon: iconInfo.icon, background: iconInfo.icon_background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
onSelect={handleSelectAppIcon}
|
onSelect={handleSelectAppIcon}
|
||||||
onClose={handleCloseAppIconPicker}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -258,7 +258,7 @@ describe('useFormState', () => {
|
|||||||
expect(result.current.showAppIconPicker).toBe(true)
|
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())
|
const { result } = renderHook(() => useFormState())
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -273,7 +273,7 @@ describe('useFormState', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.current.showAppIconPicker).toBe(false)
|
expect(result.current.showAppIconPicker).toBe(true)
|
||||||
expect(result.current.iconInfo).toEqual({
|
expect(result.current.iconInfo).toEqual({
|
||||||
icon_type: 'emoji',
|
icon_type: 'emoji',
|
||||||
icon: '🎉',
|
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())
|
const { result } = renderHook(() => useFormState())
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -297,7 +297,7 @@ describe('useFormState', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.current.showAppIconPicker).toBe(false)
|
expect(result.current.showAppIconPicker).toBe(true)
|
||||||
expect(result.current.iconInfo).toEqual({
|
expect(result.current.iconInfo).toEqual({
|
||||||
icon_type: 'image',
|
icon_type: 'image',
|
||||||
icon: 'file-123',
|
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())
|
const { result } = renderHook(() => useFormState())
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -322,15 +322,10 @@ describe('useFormState', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.handleOpenAppIconPicker()
|
result.current.setShowAppIconPicker(false)
|
||||||
})
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.handleCloseAppIconPicker()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.current.showAppIconPicker).toBe(false)
|
expect(result.current.showAppIconPicker).toBe(false)
|
||||||
// After close, icon should be restored to the icon before opening
|
|
||||||
expect(result.current.iconInfo).toEqual({
|
expect(result.current.iconInfo).toEqual({
|
||||||
icon_type: 'emoji',
|
icon_type: 'emoji',
|
||||||
icon: '🎉',
|
icon: '🎉',
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { Member } from '@/models/common'
|
|||||||
import type { IconInfo, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
|
import type { IconInfo, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
|
||||||
import type { RetrievalConfig } from '@/types/app'
|
import type { RetrievalConfig } from '@/types/app'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
|
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
|
||||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
@ -39,7 +39,6 @@ export const useFormState = () => {
|
|||||||
// Icon state
|
// Icon state
|
||||||
const [iconInfo, setIconInfo] = useState(currentDataset?.icon_info || DEFAULT_APP_ICON)
|
const [iconInfo, setIconInfo] = useState(currentDataset?.icon_info || DEFAULT_APP_ICON)
|
||||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||||
const previousAppIcon = useRef(DEFAULT_APP_ICON)
|
|
||||||
|
|
||||||
// Permission state
|
// Permission state
|
||||||
const [permission, setPermission] = useState(currentDataset?.permission)
|
const [permission, setPermission] = useState(currentDataset?.permission)
|
||||||
@ -83,8 +82,7 @@ export const useFormState = () => {
|
|||||||
// Icon handlers
|
// Icon handlers
|
||||||
const handleOpenAppIconPicker = useCallback(() => {
|
const handleOpenAppIconPicker = useCallback(() => {
|
||||||
setShowAppIconPicker(true)
|
setShowAppIconPicker(true)
|
||||||
previousAppIcon.current = iconInfo
|
}, [])
|
||||||
}, [iconInfo])
|
|
||||||
|
|
||||||
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
|
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
|
||||||
const newIconInfo: IconInfo = {
|
const newIconInfo: IconInfo = {
|
||||||
@ -94,12 +92,6 @@ export const useFormState = () => {
|
|||||||
icon_url: icon.type === 'emoji' ? undefined : icon.url,
|
icon_url: icon.type === 'emoji' ? undefined : icon.url,
|
||||||
}
|
}
|
||||||
setIconInfo(newIconInfo)
|
setIconInfo(newIconInfo)
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleCloseAppIconPicker = useCallback(() => {
|
|
||||||
setIconInfo(previousAppIcon.current)
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// External retrieval settings handler
|
// External retrieval settings handler
|
||||||
@ -223,9 +215,9 @@ export const useFormState = () => {
|
|||||||
// Icon
|
// Icon
|
||||||
iconInfo,
|
iconInfo,
|
||||||
showAppIconPicker,
|
showAppIconPicker,
|
||||||
|
setShowAppIconPicker,
|
||||||
handleOpenAppIconPicker,
|
handleOpenAppIconPicker,
|
||||||
handleSelectAppIcon,
|
handleSelectAppIcon,
|
||||||
handleCloseAppIconPicker,
|
|
||||||
|
|
||||||
// Permission
|
// Permission
|
||||||
permission,
|
permission,
|
||||||
|
|||||||
@ -26,9 +26,9 @@ const Form = () => {
|
|||||||
// Icon
|
// Icon
|
||||||
iconInfo,
|
iconInfo,
|
||||||
showAppIconPicker,
|
showAppIconPicker,
|
||||||
|
setShowAppIconPicker,
|
||||||
handleOpenAppIconPicker,
|
handleOpenAppIconPicker,
|
||||||
handleSelectAppIcon,
|
handleSelectAppIcon,
|
||||||
handleCloseAppIconPicker,
|
|
||||||
|
|
||||||
// Permission
|
// Permission
|
||||||
permission,
|
permission,
|
||||||
@ -78,9 +78,9 @@ const Form = () => {
|
|||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
iconInfo={iconInfo}
|
iconInfo={iconInfo}
|
||||||
showAppIconPicker={showAppIconPicker}
|
showAppIconPicker={showAppIconPicker}
|
||||||
|
setShowAppIconPicker={setShowAppIconPicker}
|
||||||
handleOpenAppIconPicker={handleOpenAppIconPicker}
|
handleOpenAppIconPicker={handleOpenAppIconPicker}
|
||||||
handleSelectAppIcon={handleSelectAppIcon}
|
handleSelectAppIcon={handleSelectAppIcon}
|
||||||
handleCloseAppIconPicker={handleCloseAppIconPicker}
|
|
||||||
permission={permission}
|
permission={permission}
|
||||||
setPermission={setPermission}
|
setPermission={setPermission}
|
||||||
selectedMemberIDs={selectedMemberIDs}
|
selectedMemberIDs={selectedMemberIDs}
|
||||||
|
|||||||
@ -198,15 +198,13 @@ const CreateAppModal = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
{showAppIconPicker && (
|
{showAppIconPicker && (
|
||||||
<AppIconPicker
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
initialEmoji={appIcon.type === 'emoji'
|
initialEmoji={appIcon.type === 'emoji'
|
||||||
? { icon: appIcon.icon, background: appIcon.background }
|
? { icon: appIcon.icon, background: appIcon.background }
|
||||||
: undefined}
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
onSelect={(payload) => {
|
onSelect={(payload) => {
|
||||||
setAppIcon(payload)
|
setAppIcon(payload)
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { useState } from 'react'
|
|
||||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||||
|
|
||||||
import Conversion from '../conversion'
|
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 (
|
|
||||||
<div data-testid="app-icon-picker">
|
|
||||||
<button type="button" onClick={() => setActiveTab('emoji')}>iconPicker.emoji</button>
|
|
||||||
<button type="button" onClick={() => setActiveTab('image')}>iconPicker.image</button>
|
|
||||||
{activeTab === 'emoji' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-testid="picker-emoji-option"
|
|
||||||
onClick={() => setSelectedEmoji({ icon: '🎯', background: '#FFAA00' })}
|
|
||||||
>
|
|
||||||
picker-emoji-option
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{activeTab === 'image' && <div data-testid="picker-image-panel">picker-image-panel</div>}
|
|
||||||
<button type="button" onClick={() => onClose?.()}>iconPicker.cancel</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (activeTab === 'emoji') {
|
|
||||||
onSelect?.({
|
|
||||||
type: 'emoji',
|
|
||||||
icon: selectedEmoji.icon,
|
|
||||||
background: selectedEmoji.background,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect?.({
|
|
||||||
type: 'image',
|
|
||||||
fileId: 'test-file-id',
|
|
||||||
url: 'https://example.com/icon.png',
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
iconPicker.ok
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Silence expected console.error from Dialog/Modal rendering
|
// Silence expected console.error from Dialog/Modal rendering
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
@ -767,7 +714,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
|||||||
const appIcon = getAppIcon()
|
const appIcon = getAppIcon()
|
||||||
fireEvent.click(appIcon)
|
fireEvent.click(appIcon)
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('picker-emoji-option'))
|
fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' }))
|
||||||
|
|
||||||
// Click OK to confirm selection
|
// Click OK to confirm selection
|
||||||
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||||
@ -1087,7 +1034,7 @@ describe('Integration Tests', () => {
|
|||||||
// Open picker and select an emoji
|
// Open picker and select an emoji
|
||||||
const appIcon = getAppIcon()
|
const appIcon = getAppIcon()
|
||||||
fireEvent.click(appIcon)
|
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: /iconPicker\.ok/ }))
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.publish/i }))
|
fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.publish/i }))
|
||||||
|
|||||||
@ -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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import PublishAsKnowledgePipelineModal from '../publish-as-knowledge-pipeline-modal'
|
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 }) => (
|
DialogContent: ({ children, className }: { children: React.ReactNode, className?: string }) => (
|
||||||
<div data-testid="modal" className={className}>{children}</div>
|
<div data-testid="modal" className={className}>{children}</div>
|
||||||
),
|
),
|
||||||
|
DialogTitle: ({ children, className }: { children: React.ReactNode, className?: string }) => (
|
||||||
|
<h2 className={className}>{children}</h2>
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
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 }) => (
|
|
||||||
<div data-testid="icon-picker">
|
|
||||||
<button data-testid="select-emoji" onClick={() => onSelect({ type: 'emoji', icon: '🎉', background: '#eee', url: '' })}>
|
|
||||||
Select Emoji
|
|
||||||
</button>
|
|
||||||
<button data-testid="select-image" onClick={() => onSelect({ type: 'image', icon: '', background: '', url: 'http://img.png' })}>
|
|
||||||
Select Image
|
|
||||||
</button>
|
|
||||||
<button data-testid="close-picker" onClick={onClose}>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('es-toolkit/function', () => ({
|
vi.mock('es-toolkit/function', () => ({
|
||||||
noop: () => {},
|
noop: () => {},
|
||||||
}))
|
}))
|
||||||
@ -190,41 +177,46 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
|||||||
expect(mockOnConfirm).not.toHaveBeenCalled()
|
expect(mockOnConfirm).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show icon picker when app icon clicked', () => {
|
it('should show icon picker when app icon clicked', async () => {
|
||||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||||
|
|
||||||
expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument()
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('app-icon'))
|
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(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('app-icon'))
|
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(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('app-icon'))
|
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(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('app-icon'))
|
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', () => {
|
it('should trim name and description before submitting', () => {
|
||||||
|
|||||||
@ -51,17 +51,7 @@ const PublishAsKnowledgePipelineModal = ({
|
|||||||
icon_url: '',
|
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 = () => {
|
const handleConfirm = () => {
|
||||||
if (confirmDisabled)
|
if (confirmDisabled)
|
||||||
@ -141,8 +131,12 @@ const PublishAsKnowledgePipelineModal = ({
|
|||||||
</div>
|
</div>
|
||||||
{showAppIconPicker && (
|
{showAppIconPicker && (
|
||||||
<AppIconPicker
|
<AppIconPicker
|
||||||
|
open={showAppIconPicker}
|
||||||
|
initialEmoji={pipelineIcon.icon_type === 'emoji'
|
||||||
|
? { icon: pipelineIcon.icon, background: pipelineIcon.icon_background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={setShowAppIconPicker}
|
||||||
onSelect={handleSelectIcon}
|
onSelect={handleSelectIcon}
|
||||||
onClose={handleCloseIconPicker}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@ -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 (
|
|
||||||
<div data-testid="emoji-picker">
|
|
||||||
<button data-testid="select-emoji" onClick={() => onSelect('🚀', '#FF0000')}>Select Emoji</button>
|
|
||||||
<button data-testid="close-emoji-picker" onClick={onClose}>Close</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('EditCustomCollectionModal', () => {
|
describe('EditCustomCollectionModal', () => {
|
||||||
const mockOnHide = vi.fn()
|
const mockOnHide = vi.fn()
|
||||||
const mockOnAdd = vi.fn()
|
const mockOnAdd = vi.fn()
|
||||||
|
|||||||
@ -386,12 +386,10 @@ const EditCustomCollectionModal: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
{showEmojiPicker && (
|
{showEmojiPicker && (
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
open={showEmojiPicker}
|
||||||
|
onOpenChange={setShowEmojiPicker}
|
||||||
onSelect={(icon, icon_background) => {
|
onSelect={(icon, icon_background) => {
|
||||||
setEmoji({ content: icon, background: icon_background })
|
setEmoji({ content: icon, background: icon_background })
|
||||||
setShowEmojiPicker(false)
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setShowEmojiPicker(false)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -11,31 +11,6 @@ vi.mock('@/service/common', () => ({
|
|||||||
uploadRemoteFileInfo: vi.fn().mockResolvedValue({ url: 'https://example.com/icon.png' }),
|
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) => (
|
|
||||||
<div data-testid="app-icon-picker">
|
|
||||||
<button data-testid="select-emoji-btn" onClick={() => onSelect({ type: 'emoji', icon: '🎉', background: '#FF0000' })}>
|
|
||||||
Select Emoji
|
|
||||||
</button>
|
|
||||||
<button data-testid="close-picker-btn" onClick={onClose}>
|
|
||||||
Close Picker
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock the plugins service to avoid React Query issues from TabSlider
|
// Mock the plugins service to avoid React Query issues from TabSlider
|
||||||
vi.mock('@/service/use-plugins', () => ({
|
vi.mock('@/service/use-plugins', () => ({
|
||||||
useInstalledPluginList: () => ({
|
useInstalledPluginList: () => ({
|
||||||
@ -695,9 +670,8 @@ describe('MCPModal', () => {
|
|||||||
if (appIconContainer) {
|
if (appIconContainer) {
|
||||||
fireEvent.click(appIconContainer)
|
fireEvent.click(appIconContainer)
|
||||||
|
|
||||||
// The mocked AppIconPicker should now be visible
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('app-icon-picker'))!.toBeInTheDocument()
|
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -712,16 +686,14 @@ describe('MCPModal', () => {
|
|||||||
fireEvent.click(appIconContainer)
|
fireEvent.click(appIconContainer)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('app-icon-picker'))!.toBeInTheDocument()
|
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Click the select emoji button
|
fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' }))
|
||||||
const selectBtn = screen.getByTestId('select-emoji-btn')
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||||
fireEvent.click(selectBtn)
|
|
||||||
|
|
||||||
// The picker should be closed
|
|
||||||
await waitFor(() => {
|
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)
|
fireEvent.click(appIconContainer)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('app-icon-picker'))!.toBeInTheDocument()
|
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Click the close button
|
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ }))
|
||||||
const closeBtn = screen.getByTestId('close-picker-btn')
|
|
||||||
fireEvent.click(closeBtn)
|
|
||||||
|
|
||||||
// The picker should be closed
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -117,12 +117,6 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
|
|||||||
|
|
||||||
const handleIconSelect = (payload: AppIconSelection) => {
|
const handleIconSelect = (payload: AppIconSelection) => {
|
||||||
actions.setAppIcon(payload)
|
actions.setAppIcon(payload)
|
||||||
actions.setShowAppIconPicker(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleIconClose = () => {
|
|
||||||
actions.resetIcon()
|
|
||||||
actions.setShowAppIconPicker(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSubmitDisabled = !state.name || !state.url || !state.serverIdentifier || state.isFetchingIcon
|
const isSubmitDisabled = !state.name || !state.url || !state.serverIdentifier || state.isFetchingIcon
|
||||||
@ -260,8 +254,12 @@ const MCPModalContent: FC<MCPModalContentProps> = ({
|
|||||||
|
|
||||||
{state.showAppIconPicker && (
|
{state.showAppIconPicker && (
|
||||||
<AppIconPicker
|
<AppIconPicker
|
||||||
|
open={state.showAppIconPicker}
|
||||||
|
initialEmoji={state.appIcon.type === 'emoji'
|
||||||
|
? { icon: state.appIcon.icon, background: state.appIcon.background }
|
||||||
|
: undefined}
|
||||||
|
onOpenChange={actions.setShowAppIconPicker}
|
||||||
onSelect={handleIconSelect}
|
onSelect={handleIconSelect}
|
||||||
onClose={handleIconClose}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ vi.mock('@/next/navigation', () => ({
|
|||||||
}),
|
}),
|
||||||
usePathname: () => '/app/workflow-app-id',
|
usePathname: () => '/app/workflow-app-id',
|
||||||
useSearchParams: () => new URLSearchParams(),
|
useSearchParams: () => new URLSearchParams(),
|
||||||
|
useParams: () => ({}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock app context
|
// 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 }) => (
|
|
||||||
<div data-testid="emoji-picker">
|
|
||||||
<button data-testid="select-emoji" onClick={() => onSelect('🚀', '#f0f0f0')}>Select Emoji</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock AppIcon - simplified for testing
|
// Mock AppIcon - simplified for testing
|
||||||
vi.mock('@/app/components/base/app-icon', () => ({
|
vi.mock('@/app/components/base/app-icon', () => ({
|
||||||
default: ({ onClick, icon, background }: { onClick?: () => void, icon: string, background: string }) => (
|
default: ({ onClick, icon, background }: { onClick?: () => void, icon: string, background: string }) => (
|
||||||
@ -814,8 +806,9 @@ describe('WorkflowToolDrawer', () => {
|
|||||||
await user.click(iconButton)
|
await user.click(iconButton)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// Assert
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('emoji-picker'))!.toBeInTheDocument()
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update emoji on selection', async () => {
|
it('should update emoji on selection', async () => {
|
||||||
@ -834,14 +827,19 @@ describe('WorkflowToolDrawer', () => {
|
|||||||
const iconButton = screen.getByTestId('app-icon')
|
const iconButton = screen.getByTestId('app-icon')
|
||||||
await user.click(iconButton)
|
await user.click(iconButton)
|
||||||
|
|
||||||
// Select emoji
|
await waitFor(() => {
|
||||||
await user.click(screen.getByTestId('select-emoji'))
|
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||||
await user.click(screen.getByRole('button', { name: 'app.iconPicker.ok' }))
|
})
|
||||||
|
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
|
// Assert
|
||||||
const updatedIcon = screen.getByTestId('app-icon')
|
const updatedIcon = screen.getByTestId('app-icon')
|
||||||
expect(updatedIcon)!.toHaveAttribute('data-icon', '🚀')
|
expect(updatedIcon)!.toHaveAttribute('data-icon', '🔧')
|
||||||
expect(updatedIcon)!.toHaveAttribute('data-background', '#f0f0f0')
|
expect(updatedIcon)!.toHaveAttribute('data-background', '#E4FBCC')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should close emoji picker on close button', async () => {
|
it('should close emoji picker on close button', async () => {
|
||||||
@ -859,43 +857,15 @@ describe('WorkflowToolDrawer', () => {
|
|||||||
const iconButton = screen.getByTestId('app-icon')
|
const iconButton = screen.getByTestId('app-icon')
|
||||||
await user.click(iconButton)
|
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' }))
|
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||||
|
|
||||||
// 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()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update labels when label selector changes', async () => {
|
it('should update labels when label selector changes', async () => {
|
||||||
|
|||||||
@ -4,14 +4,6 @@ import userEvent from '@testing-library/user-event'
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { WorkflowToolDrawer } from '../index'
|
import { WorkflowToolDrawer } from '../index'
|
||||||
|
|
||||||
vi.mock('@/app/components/base/emoji-picker/Inner', () => ({
|
|
||||||
default: ({ onSelect }: { onSelect: (icon: string, background: string) => void }) => (
|
|
||||||
<div data-testid="emoji-picker">
|
|
||||||
<button data-testid="select-emoji" onClick={() => onSelect('🚀', '#000000')}>Emoji</button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/base/app-icon', () => ({
|
vi.mock('@/app/components/base/app-icon', () => ({
|
||||||
default: ({ onClick, icon }: { onClick?: () => void, icon: string }) => (
|
default: ({ onClick, icon }: { onClick?: () => void, icon: string }) => (
|
||||||
<button data-testid="app-icon" onClick={onClick}>{icon}</button>
|
<button data-testid="app-icon" onClick={onClick}>{icon}</button>
|
||||||
@ -98,14 +90,20 @@ describe('WorkflowToolDrawer', () => {
|
|||||||
await user.type(screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder'), 'Created Tool')
|
await user.type(screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder'), 'Created Tool')
|
||||||
await user.click(screen.getByTestId('append-label'))
|
await user.click(screen.getByTestId('append-label'))
|
||||||
await user.click(screen.getByTestId('app-icon'))
|
await user.click(screen.getByTestId('app-icon'))
|
||||||
await user.click(screen.getByTestId('select-emoji'))
|
await waitFor(() => {
|
||||||
await user.click(screen.getByRole('button', { name: 'app.iconPicker.ok' }))
|
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' }))
|
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||||
|
|
||||||
expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({
|
expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
workflow_app_id: 'workflow-app-1',
|
workflow_app_id: 'workflow-app-1',
|
||||||
label: 'Created Tool',
|
label: 'Created Tool',
|
||||||
icon: { content: '🚀', background: '#000000' },
|
icon: { content: '🔧', background: '#E4FBCC' },
|
||||||
labels: ['label1', 'new-label'],
|
labels: ['label1', 'new-label'],
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import type { DrawerRootProps } from '@langgenius/dify-ui/drawer'
|
|||||||
import type { Emoji, WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types'
|
import type { Emoji, WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerBackdrop,
|
DrawerBackdrop,
|
||||||
@ -21,8 +20,7 @@ import * as React from 'react'
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import Divider from '@/app/components/base/divider'
|
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||||
import EmojiPickerInner from '@/app/components/base/emoji-picker/Inner'
|
|
||||||
import { Infotip } from '@/app/components/base/infotip'
|
import { Infotip } from '@/app/components/base/infotip'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Textarea from '@/app/components/base/textarea'
|
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<string>()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open disablePointerDismissal>
|
|
||||||
<DialogContent
|
|
||||||
backdropProps={{ forceRender: true }}
|
|
||||||
className="flex max-h-[552px] w-[480px]! flex-col overflow-hidden rounded-xl border-[0.5px] border-divider-subtle p-0! shadow-xl"
|
|
||||||
>
|
|
||||||
<DialogTitle className="sr-only">
|
|
||||||
{t('iconPicker.emoji', { ns: 'app' })}
|
|
||||||
</DialogTitle>
|
|
||||||
<EmojiPickerInner
|
|
||||||
className="pt-3"
|
|
||||||
onSelect={(emoji, background) => {
|
|
||||||
setSelectedEmoji(emoji)
|
|
||||||
setSelectedBackground(background)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Divider className="mt-3 mb-0" />
|
|
||||||
<div className="flex w-full items-center justify-center gap-2 p-3">
|
|
||||||
<Button className="w-full" onClick={onClose}>
|
|
||||||
{t('iconPicker.cancel', { ns: 'app' })}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={selectedEmoji === '' || !selectedBackground}
|
|
||||||
variant="primary"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => onSelect(selectedEmoji, selectedBackground!)}
|
|
||||||
>
|
|
||||||
{t('iconPicker.ok', { ns: 'app' })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WorkflowToolDrawer({
|
export function WorkflowToolDrawer({
|
||||||
isAdd,
|
isAdd,
|
||||||
payload,
|
payload,
|
||||||
@ -449,17 +402,19 @@ export function WorkflowToolDrawer({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</WorkflowToolDrawerFrame>
|
</WorkflowToolDrawerFrame>
|
||||||
{showEmojiPicker && (
|
<AppIconPicker
|
||||||
<WorkflowToolEmojiPicker
|
open={showEmojiPicker}
|
||||||
onSelect={(icon, icon_background) => {
|
enableImageUpload={false}
|
||||||
setEmoji({ content: icon, background: icon_background })
|
initialEmoji={{
|
||||||
setShowEmojiPicker(false)
|
icon: emoji.content,
|
||||||
}}
|
background: emoji.background,
|
||||||
onClose={() => {
|
}}
|
||||||
setShowEmojiPicker(false)
|
onOpenChange={setShowEmojiPicker}
|
||||||
}}
|
onSelect={(payload) => {
|
||||||
/>
|
if (payload.type === 'emoji')
|
||||||
)}
|
setEmoji({ content: payload.icon, background: payload.background })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{confirmModalOpen && (
|
{confirmModalOpen && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
show={confirmModalOpen}
|
show={confirmModalOpen}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user