refactor: migrate tag filter overlay and remove dead z-index override prop (#33791)

This commit is contained in:
yyh 2026-03-20 11:15:22 +08:00 committed by GitHub
parent 40eacf8f32
commit a0135e9e38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 54 additions and 87 deletions

View File

@ -14,23 +14,11 @@ vi.mock('@/service/tag', () => ({
fetchTagList,
}))
// Mock ahooks to avoid timer-related issues in tests
vi.mock('ahooks', () => {
return {
useDebounceFn: (fn: (...args: unknown[]) => void) => {
const ref = React.useRef(fn)
ref.current = fn
const stableRun = React.useRef((...args: unknown[]) => {
// Schedule to run after current event handler finishes,
// allowing React to process pending state updates first
Promise.resolve().then(() => ref.current(...args))
})
return { run: stableRun.current }
},
useMount: (fn: () => void) => {
React.useEffect(() => {
fn()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
},
}
@ -228,7 +216,6 @@ describe('TagFilter', () => {
const searchInput = screen.getByRole('textbox')
await user.type(searchInput, 'Front')
// With debounce mocked to be synchronous, results should be immediate
expect(screen.getByText('Frontend')).toBeInTheDocument()
expect(screen.queryByText('Backend')).not.toBeInTheDocument()
expect(screen.queryByText('API Design')).not.toBeInTheDocument()
@ -257,22 +244,14 @@ describe('TagFilter', () => {
const searchInput = screen.getByRole('textbox')
await user.type(searchInput, 'Front')
// Wait for the debounced search to filter
await waitFor(() => {
expect(screen.queryByText('Backend')).not.toBeInTheDocument()
})
expect(screen.queryByText('Backend')).not.toBeInTheDocument()
// Clear the search using the Input's clear button
const clearButton = screen.getByTestId('input-clear')
await user.click(clearButton)
// The input value should be cleared
expect(searchInput).toHaveValue('')
// After the clear + microtask re-render, all app tags should be visible again
await waitFor(() => {
expect(screen.getByText('Backend')).toBeInTheDocument()
})
expect(screen.getByText('Backend')).toBeInTheDocument()
expect(screen.getByText('Frontend')).toBeInTheDocument()
expect(screen.getByText('API Design')).toBeInTheDocument()
})

View File

@ -1,15 +1,15 @@
import type { FC } from 'react'
import type { Tag } from '@/app/components/base/tag-management/constant'
import { useDebounceFn, useMount } from 'ahooks'
import { useMount } from 'ahooks'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
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 { fetchTagList } from '@/service/tag'
import { cn } from '@/utils/classnames'
@ -33,18 +33,10 @@ const TagFilter: FC<TagFilterProps> = ({
const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal)
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
}, { wait: 500 })
const handleKeywordsChange = (value: string) => {
setKeywords(value)
handleSearch()
}
const filteredTagList = useMemo(() => {
return tagList.filter(tag => tag.type === type && tag.name.includes(searchKeywords))
}, [type, tagList, searchKeywords])
return tagList.filter(tag => tag.type === type && tag.name.includes(keywords))
}, [type, tagList, keywords])
const currentTag = useMemo(() => {
return tagList.find(tag => tag.id === value[0])
@ -64,61 +56,61 @@ const TagFilter: FC<TagFilterProps> = ({
})
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',
!open && !!value.length && 'shadow-xs',
open && !!value.length && 'shadow-xs',
)}
>
<div className="p-[1px]">
<Tag01 className="h-3.5 w-3.5 text-text-tertiary" data-testid="tag-filter-trigger-icon" />
</div>
<div className="text-[13px] leading-[18px] text-text-secondary">
{!value.length && t('tag.placeholder', { ns: 'common' })}
{!!value.length && currentTag?.name}
</div>
{value.length > 1 && (
<div className="text-xs font-medium leading-[18px] text-text-tertiary">{`+${value.length - 1}`}</div>
)}
{!value.length && (
<PopoverTrigger
render={(
<button
type="button"
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',
!!value.length && 'pr-6 shadow-xs',
)}
>
<div className="p-[1px]">
<span className="i-ri-arrow-down-s-line h-3.5 w-3.5 text-text-tertiary" data-testid="tag-filter-arrow-down-icon" />
<Tag01 className="h-3.5 w-3.5 text-text-tertiary" data-testid="tag-filter-trigger-icon" />
</div>
)}
{!!value.length && (
<div
className="group/clear cursor-pointer p-[1px]"
onClick={(e) => {
e.stopPropagation()
onChange([])
}}
data-testid="tag-filter-clear-button"
>
<span className="i-custom-vender-solid-general-x-circle h-3.5 w-3.5 text-text-tertiary group-hover/clear:text-text-secondary" />
<div className="min-w-0 truncate text-[13px] leading-[18px] text-text-secondary">
{!value.length && t('tag.placeholder', { ns: 'common' })}
{!!value.length && currentTag?.name}
</div>
)}
</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]">
{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]">
<span className="i-ri-arrow-down-s-line h-3.5 w-3.5 text-text-tertiary" data-testid="tag-filter-arrow-down-icon" />
</div>
)}
</button>
)}
/>
{!!value.length && (
<button
type="button"
className="group/clear absolute right-2 top-1/2 -translate-y-1/2 p-[1px]"
onClick={() => onChange([])}
data-testid="tag-filter-clear-button"
>
<span className="i-custom-vender-solid-general-x-circle 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="max-h-72 overflow-auto p-1">
@ -155,9 +147,9 @@ const TagFilter: FC<TagFilterProps> = ({
</div>
</div>
</div>
</PortalToFollowElemContent>
</PopoverContent>
</div>
</PortalToFollowElem>
</Popover>
)
}

View File

@ -31,7 +31,6 @@ import TTSParamsPanel from './tts-params-panel'
export type ModelParameterModalProps = {
popupClassName?: string
portalToFollowElemContentClassName?: string
isAdvancedMode: boolean
value: any
setModel: (model: any) => void
@ -44,7 +43,6 @@ export type ModelParameterModalProps = {
const ModelParameterModal: FC<ModelParameterModalProps> = ({
popupClassName,
portalToFollowElemContentClassName,
isAdvancedMode,
value,
setModel,
@ -230,7 +228,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
<PopoverContent
placement={isInWorkflow ? 'left' : 'bottom-end'}
sideOffset={4}
className={portalToFollowElemContentClassName}
popupClassName={cn(popupClassName, 'w-[389px] rounded-2xl')}
>
<div className="max-h-[420px] overflow-y-auto p-4 pt-3">

View File

@ -116,7 +116,6 @@ export const OVERLAY_MIGRATION_LEGACY_BASE_FILES = [
'app/components/base/select/index.tsx',
'app/components/base/select/pure.tsx',
'app/components/base/sort/index.tsx',
'app/components/base/tag-management/filter.tsx',
'app/components/base/theme-selector.tsx',
'app/components/base/tooltip/index.tsx',
]