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,
|
fetchTagList,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock ahooks to avoid timer-related issues in tests
|
|
||||||
vi.mock('ahooks', () => {
|
vi.mock('ahooks', () => {
|
||||||
return {
|
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) => {
|
useMount: (fn: () => void) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fn()
|
fn()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
}, [])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -228,7 +216,6 @@ describe('TagFilter', () => {
|
||||||
const searchInput = screen.getByRole('textbox')
|
const searchInput = screen.getByRole('textbox')
|
||||||
await user.type(searchInput, 'Front')
|
await user.type(searchInput, 'Front')
|
||||||
|
|
||||||
// With debounce mocked to be synchronous, results should be immediate
|
|
||||||
expect(screen.getByText('Frontend')).toBeInTheDocument()
|
expect(screen.getByText('Frontend')).toBeInTheDocument()
|
||||||
expect(screen.queryByText('Backend')).not.toBeInTheDocument()
|
expect(screen.queryByText('Backend')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('API Design')).not.toBeInTheDocument()
|
expect(screen.queryByText('API Design')).not.toBeInTheDocument()
|
||||||
|
|
@ -257,22 +244,14 @@ describe('TagFilter', () => {
|
||||||
const searchInput = screen.getByRole('textbox')
|
const searchInput = screen.getByRole('textbox')
|
||||||
await user.type(searchInput, 'Front')
|
await user.type(searchInput, 'Front')
|
||||||
|
|
||||||
// Wait for the debounced search to filter
|
expect(screen.queryByText('Backend')).not.toBeInTheDocument()
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByText('Backend')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Clear the search using the Input's clear button
|
|
||||||
const clearButton = screen.getByTestId('input-clear')
|
const clearButton = screen.getByTestId('input-clear')
|
||||||
await user.click(clearButton)
|
await user.click(clearButton)
|
||||||
|
|
||||||
// The input value should be cleared
|
|
||||||
expect(searchInput).toHaveValue('')
|
expect(searchInput).toHaveValue('')
|
||||||
|
|
||||||
// After the clear + microtask re-render, all app tags should be visible again
|
expect(screen.getByText('Backend')).toBeInTheDocument()
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Backend')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
expect(screen.getByText('Frontend')).toBeInTheDocument()
|
expect(screen.getByText('Frontend')).toBeInTheDocument()
|
||||||
expect(screen.getByText('API Design')).toBeInTheDocument()
|
expect(screen.getByText('API Design')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
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 { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
Popover,
|
||||||
PortalToFollowElemContent,
|
PopoverContent,
|
||||||
PortalToFollowElemTrigger,
|
PopoverTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/ui/popover'
|
||||||
import { fetchTagList } from '@/service/tag'
|
import { fetchTagList } from '@/service/tag'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
|
||||||
|
|
@ -33,18 +33,10 @@ const TagFilter: FC<TagFilterProps> = ({
|
||||||
const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal)
|
const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal)
|
||||||
|
|
||||||
const [keywords, setKeywords] = useState('')
|
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(() => {
|
const filteredTagList = useMemo(() => {
|
||||||
return tagList.filter(tag => tag.type === type && tag.name.includes(searchKeywords))
|
return tagList.filter(tag => tag.type === type && tag.name.includes(keywords))
|
||||||
}, [type, tagList, searchKeywords])
|
}, [type, tagList, keywords])
|
||||||
|
|
||||||
const currentTag = useMemo(() => {
|
const currentTag = useMemo(() => {
|
||||||
return tagList.find(tag => tag.id === value[0])
|
return tagList.find(tag => tag.id === value[0])
|
||||||
|
|
@ -64,61 +56,61 @@ const TagFilter: FC<TagFilterProps> = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PortalToFollowElem
|
<Popover
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
placement="bottom-start"
|
|
||||||
offset={4}
|
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<PortalToFollowElemTrigger
|
<PopoverTrigger
|
||||||
onClick={() => setOpen(v => !v)}
|
render={(
|
||||||
className="block"
|
<button
|
||||||
>
|
type="button"
|
||||||
<div className={cn(
|
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',
|
'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',
|
||||||
!open && !!value.length && 'shadow-xs',
|
!!value.length && 'pr-6 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 && (
|
|
||||||
<div className="p-[1px]">
|
<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>
|
</div>
|
||||||
)}
|
<div className="min-w-0 truncate text-[13px] leading-[18px] text-text-secondary">
|
||||||
{!!value.length && (
|
{!value.length && t('tag.placeholder', { ns: 'common' })}
|
||||||
<div
|
{!!value.length && currentTag?.name}
|
||||||
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>
|
</div>
|
||||||
)}
|
{value.length > 1 && (
|
||||||
</div>
|
<div className="shrink-0 text-xs font-medium leading-[18px] text-text-tertiary">{`+${value.length - 1}`}</div>
|
||||||
</PortalToFollowElemTrigger>
|
)}
|
||||||
<PortalToFollowElemContent className="z-[1002]">
|
{!value.length && (
|
||||||
<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="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">
|
<div className="p-2">
|
||||||
<Input
|
<Input
|
||||||
showLeftIcon
|
showLeftIcon
|
||||||
showClearIcon
|
showClearIcon
|
||||||
value={keywords}
|
value={keywords}
|
||||||
onChange={e => handleKeywordsChange(e.target.value)}
|
onChange={e => setKeywords(e.target.value)}
|
||||||
onClear={() => handleKeywordsChange('')}
|
onClear={() => setKeywords('')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-72 overflow-auto p-1">
|
<div className="max-h-72 overflow-auto p-1">
|
||||||
|
|
@ -155,9 +147,9 @@ const TagFilter: FC<TagFilterProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PortalToFollowElemContent>
|
</PopoverContent>
|
||||||
</div>
|
</div>
|
||||||
</PortalToFollowElem>
|
</Popover>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import TTSParamsPanel from './tts-params-panel'
|
||||||
|
|
||||||
export type ModelParameterModalProps = {
|
export type ModelParameterModalProps = {
|
||||||
popupClassName?: string
|
popupClassName?: string
|
||||||
portalToFollowElemContentClassName?: string
|
|
||||||
isAdvancedMode: boolean
|
isAdvancedMode: boolean
|
||||||
value: any
|
value: any
|
||||||
setModel: (model: any) => void
|
setModel: (model: any) => void
|
||||||
|
|
@ -44,7 +43,6 @@ export type ModelParameterModalProps = {
|
||||||
|
|
||||||
const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||||
popupClassName,
|
popupClassName,
|
||||||
portalToFollowElemContentClassName,
|
|
||||||
isAdvancedMode,
|
isAdvancedMode,
|
||||||
value,
|
value,
|
||||||
setModel,
|
setModel,
|
||||||
|
|
@ -230,7 +228,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
placement={isInWorkflow ? 'left' : 'bottom-end'}
|
placement={isInWorkflow ? 'left' : 'bottom-end'}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
className={portalToFollowElemContentClassName}
|
|
||||||
popupClassName={cn(popupClassName, 'w-[389px] rounded-2xl')}
|
popupClassName={cn(popupClassName, 'w-[389px] rounded-2xl')}
|
||||||
>
|
>
|
||||||
<div className="max-h-[420px] overflow-y-auto p-4 pt-3">
|
<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/index.tsx',
|
||||||
'app/components/base/select/pure.tsx',
|
'app/components/base/select/pure.tsx',
|
||||||
'app/components/base/sort/index.tsx',
|
'app/components/base/sort/index.tsx',
|
||||||
'app/components/base/tag-management/filter.tsx',
|
|
||||||
'app/components/base/theme-selector.tsx',
|
'app/components/base/theme-selector.tsx',
|
||||||
'app/components/base/tooltip/index.tsx',
|
'app/components/base/tooltip/index.tsx',
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue