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