mirror of https://github.com/langgenius/dify.git
refactor: migrate tag filter overlay and remove dead z-index override prop (#33791)
This commit is contained in:
parent
40eacf8f32
commit
a0135e9e38
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue