mirror of
https://github.com/langgenius/dify.git
synced 2026-03-21 22:38:26 +08:00
refactor: migrate high-risk overlay follow-up selectors (#33795)
Signed-off-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import AppTypeSelector, { AppTypeIcon, AppTypeLabel } from './index'
|
||||
@ -14,7 +14,7 @@ describe('AppTypeSelector', () => {
|
||||
render(<AppTypeSelector value={[]} onChange={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('app.typeSelector.all')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('app.typeSelector.workflow')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -39,24 +39,27 @@ describe('AppTypeSelector', () => {
|
||||
|
||||
// Covers opening/closing the dropdown and selection updates.
|
||||
describe('User interactions', () => {
|
||||
it('should toggle option list when clicking the trigger', () => {
|
||||
it('should close option list when clicking outside', () => {
|
||||
render(<AppTypeSelector value={[]} onChange={vi.fn()} />)
|
||||
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('list')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('app.typeSelector.all'))
|
||||
expect(screen.getByRole('tooltip')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.typeSelector.all' }))
|
||||
expect(screen.getByRole('list')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('app.typeSelector.all'))
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
||||
fireEvent.pointerDown(document.body)
|
||||
fireEvent.click(document.body)
|
||||
return waitFor(() => {
|
||||
expect(screen.queryByRole('list')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onChange with added type when selecting an unselected item', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<AppTypeSelector value={[]} onChange={onChange} />)
|
||||
|
||||
fireEvent.click(screen.getByText('app.typeSelector.all'))
|
||||
fireEvent.click(within(screen.getByRole('tooltip')).getByText('app.typeSelector.workflow'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.typeSelector.all' }))
|
||||
fireEvent.click(within(screen.getByRole('list')).getByRole('button', { name: 'app.typeSelector.workflow' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([AppModeEnum.WORKFLOW])
|
||||
})
|
||||
@ -65,8 +68,8 @@ describe('AppTypeSelector', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<AppTypeSelector value={[AppModeEnum.WORKFLOW]} onChange={onChange} />)
|
||||
|
||||
fireEvent.click(screen.getByText('app.typeSelector.workflow'))
|
||||
fireEvent.click(within(screen.getByRole('tooltip')).getByText('app.typeSelector.workflow'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.typeSelector.workflow' }))
|
||||
fireEvent.click(within(screen.getByRole('list')).getByRole('button', { name: 'app.typeSelector.workflow' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
@ -75,8 +78,8 @@ describe('AppTypeSelector', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<AppTypeSelector value={[AppModeEnum.CHAT]} onChange={onChange} />)
|
||||
|
||||
fireEvent.click(screen.getByText('app.typeSelector.chatbot'))
|
||||
fireEvent.click(within(screen.getByRole('tooltip')).getByText('app.typeSelector.agent'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.typeSelector.chatbot' }))
|
||||
fireEvent.click(within(screen.getByRole('list')).getByRole('button', { name: 'app.typeSelector.agent' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT])
|
||||
})
|
||||
@ -88,7 +91,7 @@ describe('AppTypeSelector', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.clear' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('app.typeSelector.workflow')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,13 +4,12 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/app/components/base/ui/popover'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Checkbox from '../../base/checkbox'
|
||||
|
||||
export type AppSelectorProps = {
|
||||
value: Array<AppModeEnum>
|
||||
@ -22,43 +21,43 @@ const allTypes: AppModeEnum[] = [AppModeEnum.WORKFLOW, AppModeEnum.ADVANCED_CHAT
|
||||
const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const triggerLabel = value.length === 0
|
||||
? t('typeSelector.all', { ns: 'app' })
|
||||
: value.map(type => getAppTypeLabel(type, t)).join(', ')
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
>
|
||||
<div className="relative">
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className="block"
|
||||
>
|
||||
<div className={cn(
|
||||
'flex cursor-pointer items-center justify-between space-x-1 rounded-md px-2 hover:bg-state-base-hover',
|
||||
<PopoverTrigger
|
||||
aria-label={triggerLabel}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center justify-between rounded-md px-2 hover:bg-state-base-hover',
|
||||
value.length > 0 && 'pr-7',
|
||||
)}
|
||||
>
|
||||
<AppTypeSelectTrigger values={value} />
|
||||
</PopoverTrigger>
|
||||
{value.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
className="group absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2"
|
||||
onClick={() => onChange([])}
|
||||
>
|
||||
<AppTypeSelectTrigger values={value} />
|
||||
{value && value.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
className="group h-4 w-4"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange([])
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill
|
||||
className="h-3.5 w-3.5 text-text-quaternary group-hover:text-text-tertiary"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[1002]">
|
||||
<ul className="relative w-[240px] rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]">
|
||||
<RiCloseCircleFill
|
||||
className="h-3.5 w-3.5 text-text-quaternary group-hover:text-text-tertiary"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[240px] rounded-xl border border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]"
|
||||
>
|
||||
<ul className="relative w-full p-1">
|
||||
{allTypes.map(mode => (
|
||||
<AppTypeSelectorItem
|
||||
key={mode}
|
||||
@ -73,9 +72,9 @@ const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</PortalToFollowElemContent>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@ -173,33 +172,54 @@ type AppTypeSelectorItemProps = {
|
||||
}
|
||||
function AppTypeSelectorItem({ checked, type, onClick }: AppTypeSelectorItemProps) {
|
||||
return (
|
||||
<li className="flex cursor-pointer items-center space-x-2 rounded-lg py-1 pl-2 pr-1 hover:bg-state-base-hover" onClick={onClick}>
|
||||
<Checkbox checked={checked} />
|
||||
<AppTypeIcon type={type} />
|
||||
<div className="grow p-1 pl-0">
|
||||
<AppTypeLabel type={type} className="system-sm-medium text-components-menu-item-text" />
|
||||
</div>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center space-x-2 rounded-lg py-1 pl-2 pr-1 text-left hover:bg-state-base-hover"
|
||||
aria-pressed={checked}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'flex h-4 w-4 shrink-0 items-center justify-center rounded-[4px] shadow-xs shadow-shadow-shadow-3',
|
||||
checked
|
||||
? 'bg-components-checkbox-bg text-components-checkbox-icon'
|
||||
: 'border border-components-checkbox-border bg-components-checkbox-bg-unchecked',
|
||||
)}
|
||||
>
|
||||
{checked && <span className="i-ri-check-line h-3 w-3" />}
|
||||
</span>
|
||||
<AppTypeIcon type={type} />
|
||||
<div className="grow p-1 pl-0">
|
||||
<AppTypeLabel type={type} className="system-sm-medium text-components-menu-item-text" />
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function getAppTypeLabel(type: AppModeEnum, t: ReturnType<typeof useTranslation>['t']) {
|
||||
if (type === AppModeEnum.CHAT)
|
||||
return t('typeSelector.chatbot', { ns: 'app' })
|
||||
if (type === AppModeEnum.AGENT_CHAT)
|
||||
return t('typeSelector.agent', { ns: 'app' })
|
||||
if (type === AppModeEnum.COMPLETION)
|
||||
return t('typeSelector.completion', { ns: 'app' })
|
||||
if (type === AppModeEnum.ADVANCED_CHAT)
|
||||
return t('typeSelector.advanced', { ns: 'app' })
|
||||
if (type === AppModeEnum.WORKFLOW)
|
||||
return t('typeSelector.workflow', { ns: 'app' })
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
type AppTypeLabelProps = {
|
||||
type: AppModeEnum
|
||||
className?: string
|
||||
}
|
||||
export function AppTypeLabel({ type, className }: AppTypeLabelProps) {
|
||||
const { t } = useTranslation()
|
||||
let label = ''
|
||||
if (type === AppModeEnum.CHAT)
|
||||
label = t('typeSelector.chatbot', { ns: 'app' })
|
||||
if (type === AppModeEnum.AGENT_CHAT)
|
||||
label = t('typeSelector.agent', { ns: 'app' })
|
||||
if (type === AppModeEnum.COMPLETION)
|
||||
label = t('typeSelector.completion', { ns: 'app' })
|
||||
if (type === AppModeEnum.ADVANCED_CHAT)
|
||||
label = t('typeSelector.advanced', { ns: 'app' })
|
||||
if (type === AppModeEnum.WORKFLOW)
|
||||
label = t('typeSelector.workflow', { ns: 'app' })
|
||||
|
||||
return <span className={className}>{label}</span>
|
||||
return <span className={className}>{getAppTypeLabel(type, t)}</span>
|
||||
}
|
||||
|
||||
@ -13,12 +13,20 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
const { mockToastNotify } = vi.hoisted(() => ({
|
||||
mockToastNotify: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
default: Object.assign(actual.default, {
|
||||
notify: mockToastNotify,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const mockCreateEmptyDataset = vi.fn()
|
||||
const mockInvalidDatasetList = vi.fn()
|
||||
|
||||
@ -37,6 +45,8 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
describe('CreateCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockToastNotify.mockReset()
|
||||
mockToastNotify.mockImplementation(() => ({ clear: vi.fn() }))
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import type { PipelineTemplate } from '@/models/pipeline'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import EditPipelineInfo from '../edit-pipeline-info'
|
||||
|
||||
@ -16,12 +14,21 @@ vi.mock('@/service/use-pipeline', () => ({
|
||||
useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
toast: {
|
||||
...actual.toast,
|
||||
add: mockToastAdd,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
@ -88,6 +95,7 @@ describe('EditPipelineInfo', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockToastAdd.mockReset()
|
||||
_mockOnSelect = undefined
|
||||
_mockOnClose = undefined
|
||||
})
|
||||
@ -235,9 +243,9 @@ describe('EditPipelineInfo', () => {
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'Please enter a name for the Knowledge Base.',
|
||||
title: 'datasetPipeline.editPipelineInfoNameRequired',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { PipelineTemplate } from '@/models/pipeline'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import TemplateCard from '../index'
|
||||
|
||||
@ -15,12 +14,21 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
toast: {
|
||||
...actual.toast,
|
||||
add: mockToastAdd,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock download utilities
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadBlob: vi.fn(),
|
||||
@ -174,6 +182,7 @@ describe('TemplateCard', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockToastAdd.mockReset()
|
||||
mockIsExporting = false
|
||||
_capturedOnConfirm = undefined
|
||||
_capturedOnCancel = undefined
|
||||
@ -228,9 +237,9 @@ describe('TemplateCard', () => {
|
||||
fireEvent.click(chooseButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: expect.any(String),
|
||||
title: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -291,9 +300,9 @@ describe('TemplateCard', () => {
|
||||
fireEvent.click(chooseButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: expect.any(String),
|
||||
title: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -309,9 +318,9 @@ describe('TemplateCard', () => {
|
||||
fireEvent.click(chooseButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: expect.any(String),
|
||||
title: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -458,9 +467,9 @@ describe('TemplateCard', () => {
|
||||
fireEvent.click(exportButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: expect.any(String),
|
||||
title: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -476,9 +485,9 @@ describe('TemplateCard', () => {
|
||||
fireEvent.click(exportButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: expect.any(String),
|
||||
title: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -32,16 +32,21 @@ vi.mock('@/service/base', () => ({
|
||||
ssePost: mockSsePost,
|
||||
}))
|
||||
|
||||
// Mock Toast.notify - static method that manipulates DOM, needs mocking to verify calls
|
||||
const { mockToastNotify } = vi.hoisted(() => ({
|
||||
mockToastNotify: vi.fn(),
|
||||
// Mock toast.add because the component reports errors through the UI toast manager.
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: mockToastNotify,
|
||||
},
|
||||
}))
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
toast: {
|
||||
...actual.toast,
|
||||
add: mockToastAdd,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock useGetDataSourceAuth - API service hook requires mocking
|
||||
const { mockUseGetDataSourceAuth } = vi.hoisted(() => ({
|
||||
@ -192,6 +197,7 @@ const createDefaultProps = (overrides?: Partial<OnlineDocumentsProps>): OnlineDo
|
||||
describe('OnlineDocuments', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockToastAdd.mockReset()
|
||||
|
||||
// Reset store state
|
||||
mockStoreState.documentsData = []
|
||||
@ -509,9 +515,9 @@ describe('OnlineDocuments', () => {
|
||||
render(<OnlineDocuments {...props} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'Something went wrong',
|
||||
title: 'Something went wrong',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -774,9 +780,9 @@ describe('OnlineDocuments', () => {
|
||||
render(<OnlineDocuments {...props} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'API Error Message',
|
||||
title: 'API Error Message',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1094,9 +1100,9 @@ describe('OnlineDocuments', () => {
|
||||
render(<OnlineDocuments {...props} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'Failed to fetch documents',
|
||||
title: 'Failed to fetch documents',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -45,15 +45,20 @@ vi.mock('@/service/use-datasource', () => ({
|
||||
useGetDataSourceAuth: mockUseGetDataSourceAuth,
|
||||
}))
|
||||
|
||||
const { mockToastNotify } = vi.hoisted(() => ({
|
||||
mockToastNotify: vi.fn(),
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: mockToastNotify,
|
||||
},
|
||||
}))
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
toast: {
|
||||
...actual.toast,
|
||||
add: mockToastAdd,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Note: zustand/react/shallow useShallow is imported directly (simple utility function)
|
||||
|
||||
@ -231,6 +236,7 @@ const resetMockStoreState = () => {
|
||||
describe('OnlineDrive', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockToastAdd.mockReset()
|
||||
|
||||
// Reset store state
|
||||
resetMockStoreState()
|
||||
@ -541,9 +547,9 @@ describe('OnlineDrive', () => {
|
||||
render(<OnlineDrive {...props} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
title: errorMessage,
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -915,9 +921,9 @@ describe('OnlineDrive', () => {
|
||||
render(<OnlineDrive {...props} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
title: errorMessage,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,13 +1,26 @@
|
||||
import type { MockInstance } from 'vitest'
|
||||
import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { CrawlStep } from '@/models/datasets'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import Options from '../index'
|
||||
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
toast: {
|
||||
...actual.toast,
|
||||
add: mockToastAdd,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock useInitialData and useConfigurations hooks
|
||||
const { mockUseInitialData, mockUseConfigurations } = vi.hoisted(() => ({
|
||||
mockUseInitialData: vi.fn(),
|
||||
@ -116,13 +129,9 @@ const createDefaultProps = (overrides?: Partial<OptionsProps>): OptionsProps =>
|
||||
})
|
||||
|
||||
describe('Options', () => {
|
||||
let toastNotifySpy: MockInstance
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Spy on Toast.notify instead of mocking the entire module
|
||||
toastNotifySpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
mockToastAdd.mockReset()
|
||||
|
||||
// Reset mock form values
|
||||
Object.keys(mockFormValues).forEach(key => delete mockFormValues[key])
|
||||
@ -132,10 +141,6 @@ describe('Options', () => {
|
||||
mockUseConfigurations.mockReturnValue([createMockConfiguration()])
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
toastNotifySpy.mockRestore()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const props = createDefaultProps()
|
||||
@ -638,7 +643,7 @@ describe('Options', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert - Toast should be called with error message
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith(
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
}),
|
||||
@ -660,10 +665,10 @@ describe('Options', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert - Toast message should contain field path
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith(
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
message: expect.stringContaining('email_address'),
|
||||
title: expect.stringContaining('email_address'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -714,8 +719,8 @@ describe('Options', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert - Toast should be called once (only first error)
|
||||
expect(toastNotifySpy).toHaveBeenCalledTimes(1)
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith(
|
||||
expect(mockToastAdd).toHaveBeenCalledTimes(1)
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
}),
|
||||
@ -738,7 +743,7 @@ describe('Options', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert - No toast error, onSubmit called
|
||||
expect(toastNotifySpy).not.toHaveBeenCalled()
|
||||
expect(mockToastAdd).not.toHaveBeenCalled()
|
||||
expect(mockOnSubmit).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -835,7 +840,7 @@ describe('Options', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalled()
|
||||
expect(toastNotifySpy).not.toHaveBeenCalled()
|
||||
expect(mockToastAdd).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fail validation with invalid data', () => {
|
||||
@ -854,7 +859,7 @@ describe('Options', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(mockOnSubmit).not.toHaveBeenCalled()
|
||||
expect(toastNotifySpy).toHaveBeenCalled()
|
||||
expect(mockToastAdd).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error toast message when validation fails', () => {
|
||||
@ -871,10 +876,10 @@ describe('Options', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith(
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
message: expect.any(String),
|
||||
title: expect.any(String),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,13 +1,24 @@
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import OnlineDocumentPreview from '../online-document-preview'
|
||||
|
||||
// Uses global react-i18next mock from web/vitest.setup.ts
|
||||
|
||||
// Spy on Toast.notify
|
||||
const toastNotifySpy = vi.spyOn(Toast, 'notify')
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
toast: {
|
||||
...actual.toast,
|
||||
add: mockToastAdd,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock dataset-detail context - needs mock to control return values
|
||||
const mockPipelineId = vi.fn()
|
||||
@ -56,6 +67,7 @@ const defaultProps = {
|
||||
describe('OnlineDocumentPreview', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockToastAdd.mockReset()
|
||||
mockPipelineId.mockReturnValue('pipeline-123')
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
@ -258,9 +270,9 @@ describe('OnlineDocumentPreview', () => {
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
title: errorMessage,
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -276,9 +288,9 @@ describe('OnlineDocumentPreview', () => {
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'Network Error',
|
||||
title: 'Network Error',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,13 +3,24 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import * as z from 'zod'
|
||||
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Actions from '../actions'
|
||||
import Form from '../form'
|
||||
import Header from '../header'
|
||||
|
||||
// Spy on Toast.notify for validation tests
|
||||
const toastNotifySpy = vi.spyOn(Toast, 'notify')
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
toast: {
|
||||
...actual.toast,
|
||||
add: mockToastAdd,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Test Data Factory Functions
|
||||
|
||||
@ -335,7 +346,7 @@ describe('Form', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
toastNotifySpy.mockClear()
|
||||
mockToastAdd.mockReset()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@ -444,9 +455,9 @@ describe('Form', () => {
|
||||
|
||||
// Assert - validation error should be shown
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: '"field1" is required',
|
||||
title: '"field1" is required',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -566,9 +577,9 @@ describe('Form', () => {
|
||||
fireEvent.submit(form)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: '"field1" is required',
|
||||
title: '"field1" is required',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -583,7 +594,7 @@ describe('Form', () => {
|
||||
|
||||
// Assert - wait a bit and verify onSubmit was not called
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalled()
|
||||
expect(mockToastAdd).toHaveBeenCalled()
|
||||
})
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -2,10 +2,23 @@ import type { BaseConfiguration } from '@/app/components/base/form/form-scenario
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
import Form from '../form'
|
||||
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
|
||||
return {
|
||||
...actual,
|
||||
toast: {
|
||||
...actual.toast,
|
||||
add: mockToastAdd,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock the Header component (sibling component, not a base component)
|
||||
vi.mock('../header', () => ({
|
||||
default: ({ onReset, resetDisabled, onPreview, previewDisabled }: {
|
||||
@ -44,7 +57,7 @@ const defaultProps = {
|
||||
describe('Form (process-documents)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
mockToastAdd.mockReset()
|
||||
})
|
||||
|
||||
// Verify basic rendering of form structure
|
||||
@ -106,8 +119,11 @@ describe('Form (process-documents)', () => {
|
||||
fireEvent.submit(form)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
title: '"name" Name is required',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -121,7 +137,7 @@ describe('Form (process-documents)', () => {
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalled()
|
||||
})
|
||||
expect(Toast.notify).not.toHaveBeenCalled()
|
||||
expect(mockToastAdd).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Import component after mocks
|
||||
@ -17,44 +18,73 @@ vi.mock('@/i18n-config/language', () => ({
|
||||
],
|
||||
}))
|
||||
|
||||
// Mock PortalSelect component
|
||||
vi.mock('@/app/components/base/select', () => ({
|
||||
PortalSelect: ({
|
||||
const MockSelectContext = React.createContext<{
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
}>({
|
||||
value: '',
|
||||
onValueChange: () => {},
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/ui/select', () => ({
|
||||
Select: ({
|
||||
value,
|
||||
items,
|
||||
onSelect,
|
||||
triggerClassName,
|
||||
popupClassName,
|
||||
popupInnerClassName,
|
||||
onValueChange,
|
||||
children,
|
||||
}: {
|
||||
value: string
|
||||
items: Array<{ value: string, name: string }>
|
||||
onSelect: (item: { value: string }) => void
|
||||
triggerClassName?: string
|
||||
popupClassName?: string
|
||||
popupInnerClassName?: string
|
||||
onValueChange: (value: string) => void
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<div
|
||||
data-testid="portal-select"
|
||||
data-value={value}
|
||||
data-trigger-class={triggerClassName}
|
||||
data-popup-class={popupClassName}
|
||||
data-popup-inner-class={popupInnerClassName}
|
||||
>
|
||||
<span data-testid="selected-value">{value}</span>
|
||||
<div data-testid="items-container">
|
||||
{items.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
data-testid={`select-item-${item.value}`}
|
||||
onClick={() => onSelect({ value: item.value })}
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<MockSelectContext.Provider value={{ value, onValueChange }}>
|
||||
<div data-testid="select-root">{children}</div>
|
||||
</MockSelectContext.Provider>
|
||||
),
|
||||
SelectTrigger: ({
|
||||
children,
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: {
|
||||
'children': React.ReactNode
|
||||
'className'?: string
|
||||
'data-testid'?: string
|
||||
}) => (
|
||||
<button data-testid={testId ?? 'select-trigger'} data-class={className}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SelectValue: () => {
|
||||
const { value } = React.useContext(MockSelectContext)
|
||||
return <span data-testid="selected-value">{value}</span>
|
||||
},
|
||||
SelectContent: ({
|
||||
children,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupClassName?: string
|
||||
}) => (
|
||||
<div data-testid="select-content" data-popup-class={popupClassName}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectItem: ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
value: string
|
||||
}) => {
|
||||
const { onValueChange } = React.useContext(MockSelectContext)
|
||||
return (
|
||||
<button
|
||||
data-testid={`select-item-${value}`}
|
||||
onClick={() => onValueChange(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
@ -139,7 +169,7 @@ describe('TTSParamsPanel', () => {
|
||||
expect(screen.getByText('appDebug.voice.voiceSettings.voice')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render two PortalSelect components', () => {
|
||||
it('should render two Select components', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
@ -147,7 +177,7 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
const selects = screen.getAllByTestId('select-root')
|
||||
expect(selects).toHaveLength(2)
|
||||
})
|
||||
|
||||
@ -159,8 +189,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', 'zh-Hans')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0]).toHaveTextContent('zh-Hans')
|
||||
})
|
||||
|
||||
it('should render voice select with correct value', () => {
|
||||
@ -171,8 +201,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', 'echo')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1]).toHaveTextContent('echo')
|
||||
})
|
||||
|
||||
it('should only show supported languages in language select', () => {
|
||||
@ -205,7 +235,7 @@ describe('TTSParamsPanel', () => {
|
||||
|
||||
// ==================== Props Testing ====================
|
||||
describe('Props', () => {
|
||||
it('should apply trigger className to PortalSelect', () => {
|
||||
it('should apply trigger className to SelectTrigger', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
@ -213,12 +243,11 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-trigger-class', 'h-8')
|
||||
expect(selects[1]).toHaveAttribute('data-trigger-class', 'h-8')
|
||||
expect(screen.getByTestId('tts-language-select-trigger')).toHaveAttribute('data-class', 'w-full')
|
||||
expect(screen.getByTestId('tts-voice-select-trigger')).toHaveAttribute('data-class', 'w-full')
|
||||
})
|
||||
|
||||
it('should apply popup className to PortalSelect', () => {
|
||||
it('should apply popup className to SelectContent', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
@ -226,22 +255,9 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-popup-class', 'z-[1000]')
|
||||
expect(selects[1]).toHaveAttribute('data-popup-class', 'z-[1000]')
|
||||
})
|
||||
|
||||
it('should apply popup inner className to PortalSelect', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-popup-inner-class', 'w-[354px]')
|
||||
expect(selects[1]).toHaveAttribute('data-popup-inner-class', 'w-[354px]')
|
||||
const contents = screen.getAllByTestId('select-content')
|
||||
expect(contents[0]).toHaveAttribute('data-popup-class', 'w-[354px]')
|
||||
expect(contents[1]).toHaveAttribute('data-popup-class', 'w-[354px]')
|
||||
})
|
||||
})
|
||||
|
||||
@ -411,10 +427,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert - no voice items (except language items)
|
||||
const voiceSelects = screen.getAllByTestId('portal-select')
|
||||
// Second select is voice select, should have no voice items in items-container
|
||||
const voiceItemsContainer = voiceSelects[1].querySelector('[data-testid="items-container"]')
|
||||
expect(voiceItemsContainer?.children).toHaveLength(0)
|
||||
expect(screen.getAllByTestId('select-content')[1].children).toHaveLength(0)
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle currentModel with single voice', () => {
|
||||
@ -443,8 +457,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', '')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0]).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle empty voice value', () => {
|
||||
@ -455,8 +469,8 @@ describe('TTSParamsPanel', () => {
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', '')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1]).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle many voices', () => {
|
||||
@ -514,14 +528,14 @@ describe('TTSParamsPanel', () => {
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[0]).toHaveAttribute('data-value', 'en-US')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0]).toHaveTextContent('en-US')
|
||||
|
||||
rerender(<TTSParamsPanel {...props} language="zh-Hans" />)
|
||||
|
||||
// Assert
|
||||
const updatedSelects = screen.getAllByTestId('portal-select')
|
||||
expect(updatedSelects[0]).toHaveAttribute('data-value', 'zh-Hans')
|
||||
const updatedValues = screen.getAllByTestId('selected-value')
|
||||
expect(updatedValues[0]).toHaveTextContent('zh-Hans')
|
||||
})
|
||||
|
||||
it('should update when voice prop changes', () => {
|
||||
@ -530,14 +544,14 @@ describe('TTSParamsPanel', () => {
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
const selects = screen.getAllByTestId('portal-select')
|
||||
expect(selects[1]).toHaveAttribute('data-value', 'alloy')
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1]).toHaveTextContent('alloy')
|
||||
|
||||
rerender(<TTSParamsPanel {...props} voice="echo" />)
|
||||
|
||||
// Assert
|
||||
const updatedSelects = screen.getAllByTestId('portal-select')
|
||||
expect(updatedSelects[1]).toHaveAttribute('data-value', 'echo')
|
||||
const updatedValues = screen.getAllByTestId('selected-value')
|
||||
expect(updatedValues[1]).toHaveTextContent('echo')
|
||||
})
|
||||
|
||||
it('should update voice list when currentModel changes', () => {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
currentModel: any
|
||||
@ -12,6 +11,8 @@ type Props = {
|
||||
onChange: (language: string, voice: string) => void
|
||||
}
|
||||
|
||||
const supportedLanguages = languages.filter(item => item.supported)
|
||||
|
||||
const TTSParamsPanel = ({
|
||||
currentModel,
|
||||
language,
|
||||
@ -19,11 +20,11 @@ const TTSParamsPanel = ({
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const voiceList = useMemo(() => {
|
||||
const voiceList = useMemo<Array<{ label: string, value: string }>>(() => {
|
||||
if (!currentModel)
|
||||
return []
|
||||
return currentModel.model_properties.voices.map((item: { mode: any }) => ({
|
||||
...item,
|
||||
return currentModel.model_properties.voices.map((item: { mode: string, name: string }) => ({
|
||||
label: item.name,
|
||||
value: item.mode,
|
||||
}))
|
||||
}, [currentModel])
|
||||
@ -39,27 +40,57 @@ const TTSParamsPanel = ({
|
||||
<div className="system-sm-semibold mb-1 flex items-center py-1 text-text-secondary">
|
||||
{t('voice.voiceSettings.language', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<PortalSelect
|
||||
triggerClassName="h-8"
|
||||
popupClassName={cn('z-[1000]')}
|
||||
popupInnerClassName={cn('w-[354px]')}
|
||||
<Select
|
||||
value={language}
|
||||
items={languages.filter(item => item.supported)}
|
||||
onSelect={item => setLanguage(item.value as string)}
|
||||
/>
|
||||
onValueChange={(value) => {
|
||||
if (value == null)
|
||||
return
|
||||
setLanguage(value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
data-testid="tts-language-select-trigger"
|
||||
aria-label={t('voice.voiceSettings.language', { ns: 'appDebug' })}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-[354px]">
|
||||
{supportedLanguages.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="system-sm-semibold mb-1 flex items-center py-1 text-text-secondary">
|
||||
{t('voice.voiceSettings.voice', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<PortalSelect
|
||||
triggerClassName="h-8"
|
||||
popupClassName={cn('z-[1000]')}
|
||||
popupInnerClassName={cn('w-[354px]')}
|
||||
<Select
|
||||
value={voice}
|
||||
items={voiceList}
|
||||
onSelect={item => setVoice(item.value as string)}
|
||||
/>
|
||||
onValueChange={(value) => {
|
||||
if (value == null)
|
||||
return
|
||||
setVoice(value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
data-testid="tts-voice-select-trigger"
|
||||
aria-label={t('voice.voiceSettings.voice', { ns: 'appDebug' })}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-[354px]">
|
||||
{voiceList.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1333,12 +1333,9 @@ describe('CommonCreateModal', () => {
|
||||
mockVerifyCredentials.mockImplementation((params, { onSuccess }) => {
|
||||
onSuccess()
|
||||
})
|
||||
const builder = createMockSubscriptionBuilder()
|
||||
|
||||
render(<CommonCreateModal {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateBuilder).toHaveBeenCalled()
|
||||
})
|
||||
render(<CommonCreateModal {...defaultProps} builder={builder} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-confirm'))
|
||||
|
||||
|
||||
@ -18,32 +18,11 @@ vi.mock('@/app/components/plugins/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useDebounceFn to store the function and allow manual triggering
|
||||
let debouncedFn: (() => void) | null = null
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounceFn: (fn: () => void) => {
|
||||
debouncedFn = fn
|
||||
return {
|
||||
run: () => {
|
||||
// Schedule to run after React state updates
|
||||
setTimeout(() => debouncedFn?.(), 0)
|
||||
},
|
||||
cancel: vi.fn(),
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
describe('LabelFilter', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
debouncedFn = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
// Rendering Tests
|
||||
@ -81,36 +60,23 @@ describe('LabelFilter', () => {
|
||||
|
||||
const trigger = screen.getByText('common.tag.placeholder')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(trigger)
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(trigger))
|
||||
|
||||
mockTags.forEach((tag) => {
|
||||
expect(screen.getByText(tag.label)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should close dropdown when trigger is clicked again', async () => {
|
||||
it('should render search input when dropdown is open', async () => {
|
||||
render(<LabelFilter value={[]} onChange={mockOnChange} />)
|
||||
|
||||
const trigger = screen.getByText('common.tag.placeholder')
|
||||
const trigger = screen.getByText('common.tag.placeholder').closest('button')
|
||||
expect(trigger).toBeInTheDocument()
|
||||
|
||||
// Open
|
||||
await act(async () => {
|
||||
fireEvent.click(trigger)
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(trigger!))
|
||||
|
||||
expect(screen.getByText('Agent')).toBeInTheDocument()
|
||||
|
||||
// Close
|
||||
await act(async () => {
|
||||
fireEvent.click(trigger)
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -119,17 +85,11 @@ describe('LabelFilter', () => {
|
||||
it('should call onChange with selected label when clicking a label', async () => {
|
||||
render(<LabelFilter value={[]} onChange={mockOnChange} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('common.tag.placeholder'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(screen.getByText('common.tag.placeholder')))
|
||||
|
||||
expect(screen.getByText('Agent')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Agent'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(screen.getByText('Agent')))
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['agent'])
|
||||
})
|
||||
@ -137,10 +97,7 @@ describe('LabelFilter', () => {
|
||||
it('should remove label from selection when clicking already selected label', async () => {
|
||||
render(<LabelFilter value={['agent']} onChange={mockOnChange} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Agent'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(screen.getByText('Agent')))
|
||||
|
||||
// Find the label item in the dropdown list
|
||||
const labelItems = screen.getAllByText('Agent')
|
||||
@ -149,7 +106,6 @@ describe('LabelFilter', () => {
|
||||
await act(async () => {
|
||||
if (dropdownItem)
|
||||
fireEvent.click(dropdownItem)
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith([])
|
||||
@ -158,17 +114,11 @@ describe('LabelFilter', () => {
|
||||
it('should add label to existing selection', async () => {
|
||||
render(<LabelFilter value={['agent']} onChange={mockOnChange} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Agent'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(screen.getByText('Agent')))
|
||||
|
||||
expect(screen.getByText('RAG')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('RAG'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(screen.getByText('RAG')))
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['agent', 'rag'])
|
||||
})
|
||||
@ -179,8 +129,7 @@ describe('LabelFilter', () => {
|
||||
it('should clear all selections when clear button is clicked', async () => {
|
||||
render(<LabelFilter value={['agent', 'rag']} onChange={mockOnChange} />)
|
||||
|
||||
// Find and click the clear button (XCircle icon's parent)
|
||||
const clearButton = document.querySelector('.group\\/clear')
|
||||
const clearButton = screen.getByRole('button', { name: 'common.operation.clear' })
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(clearButton!)
|
||||
@ -203,21 +152,16 @@ describe('LabelFilter', () => {
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('common.tag.placeholder'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
// Filter by 'rag' which only matches 'rag' name
|
||||
fireEvent.change(searchInput, { target: { value: 'rag' } })
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
// Only RAG should be visible (rag contains 'rag')
|
||||
expect(screen.getByTitle('RAG')).toBeInTheDocument()
|
||||
// Agent should not be in the dropdown list (agent doesn't contain 'rag')
|
||||
expect(screen.queryByTitle('Agent')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -226,7 +170,6 @@ describe('LabelFilter', () => {
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('common.tag.placeholder'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
@ -234,7 +177,6 @@ describe('LabelFilter', () => {
|
||||
await act(async () => {
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
fireEvent.change(searchInput, { target: { value: 'nonexistent' } })
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
expect(screen.getByText('common.tag.noTag')).toBeInTheDocument()
|
||||
@ -245,26 +187,21 @@ describe('LabelFilter', () => {
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('common.tag.placeholder'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
// First filter to show only RAG
|
||||
fireEvent.change(searchInput, { target: { value: 'rag' } })
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
expect(screen.getByTitle('RAG')).toBeInTheDocument()
|
||||
expect(screen.queryByTitle('Agent')).not.toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
// Clear the input
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
fireEvent.change(searchInput, { target: { value: '' } })
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
|
||||
// All labels should be visible again
|
||||
@ -310,17 +247,11 @@ describe('LabelFilter', () => {
|
||||
it('should call onChange with updated array', async () => {
|
||||
render(<LabelFilter value={[]} onChange={mockOnChange} />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('common.tag.placeholder'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(screen.getByText('common.tag.placeholder')))
|
||||
|
||||
expect(screen.getByText('Agent')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Agent'))
|
||||
vi.advanceTimersByTime(10)
|
||||
})
|
||||
await act(async () => fireEvent.click(screen.getByText('Agent')))
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['agent'])
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import type { Label } from '@/app/components/tools/labels/constant'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
@ -9,10 +8,10 @@ import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Input from '@/app/components/base/input'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/app/components/base/ui/popover'
|
||||
import { useTags } from '@/app/components/plugins/hooks'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@ -30,18 +29,10 @@ const LabelFilter: FC<LabelFilterProps> = ({
|
||||
const { tags: labelList } = useTags()
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const filteredLabelList = useMemo(() => {
|
||||
return labelList.filter(label => label.name.includes(searchKeywords))
|
||||
}, [labelList, searchKeywords])
|
||||
return labelList.filter(label => label.name.includes(keywords))
|
||||
}, [labelList, keywords])
|
||||
|
||||
const currentLabel = useMemo(() => {
|
||||
return labelList.find(label => label.name === value[0])
|
||||
@ -55,72 +46,70 @@ const LabelFilter: FC<LabelFilterProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
>
|
||||
<div className="relative">
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className="block"
|
||||
>
|
||||
<div className={cn(
|
||||
'flex h-8 cursor-pointer select-none items-center gap-1 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 hover:bg-components-input-bg-hover',
|
||||
!open && !!value.length && 'shadow-xs',
|
||||
open && !!value.length && 'shadow-xs',
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
'flex h-8 cursor-pointer select-none items-center gap-1 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 text-left hover:bg-components-input-bg-hover',
|
||||
!!value.length && 'pr-6 shadow-xs',
|
||||
)}
|
||||
>
|
||||
<div className="p-[1px]">
|
||||
<Tag01 className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
</div>
|
||||
<div className="text-[13px] leading-[18px] text-text-tertiary">
|
||||
{!value.length && t('tag.placeholder', { ns: 'common' })}
|
||||
{!!value.length && currentLabel?.label}
|
||||
</div>
|
||||
{value.length > 1 && (
|
||||
<div className="text-xs font-medium leading-[18px] text-text-tertiary">{`+${value.length - 1}`}</div>
|
||||
)}
|
||||
{!value.length && (
|
||||
<div className="p-[1px]">
|
||||
<RiArrowDownSLine className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
{!!value.length && (
|
||||
<div
|
||||
className="group/clear cursor-pointer p-[1px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange([])
|
||||
}}
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5 text-text-tertiary group-hover/clear:text-text-secondary" />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="p-[1px]">
|
||||
<Tag01 className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[1002]">
|
||||
<div className="relative w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]">
|
||||
<div className="min-w-0 truncate text-[13px] leading-[18px] text-text-tertiary">
|
||||
{!value.length && t('tag.placeholder', { ns: 'common' })}
|
||||
{!!value.length && currentLabel?.label}
|
||||
</div>
|
||||
{value.length > 1 && (
|
||||
<div className="shrink-0 text-xs font-medium leading-[18px] text-text-tertiary">{`+${value.length - 1}`}</div>
|
||||
)}
|
||||
{!value.length && (
|
||||
<div className="shrink-0 p-[1px]">
|
||||
<RiArrowDownSLine className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
{!!value.length && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
className="group/clear absolute right-2 top-1/2 -translate-y-1/2 p-[1px]"
|
||||
data-testid="label-filter-clear-button"
|
||||
onClick={() => onChange([])}
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5 text-text-tertiary group-hover/clear:text-text-secondary" />
|
||||
</button>
|
||||
)}
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="p-2">
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
onChange={e => setKeywords(e.target.value)}
|
||||
onClear={() => setKeywords('')}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
{filteredLabelList.map(label => (
|
||||
<div
|
||||
<button
|
||||
key={label.name}
|
||||
className="flex cursor-pointer select-none items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover"
|
||||
type="button"
|
||||
className="flex w-full select-none items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 text-left hover:bg-state-base-hover"
|
||||
onClick={() => selectLabel(label)}
|
||||
>
|
||||
<div title={label.label} className="grow truncate text-sm leading-5 text-text-secondary">{label.label}</div>
|
||||
{value.includes(label.name) && <Check className="h-4 w-4 shrink-0 text-text-accent" />}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{!filteredLabelList.length && (
|
||||
<div className="flex flex-col items-center gap-1 p-3">
|
||||
@ -130,9 +119,9 @@ const LabelFilter: FC<LabelFilterProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
</Popover>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@ -1325,9 +1325,6 @@
|
||||
}
|
||||
},
|
||||
"app/components/app/type-selector/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
}
|
||||
@ -5211,14 +5208,11 @@
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx": {
|
||||
@ -5975,14 +5969,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/tools/labels/filter.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/no-unnecessary-whitespace": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/tools/labels/selector.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
Reference in New Issue
Block a user