mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
refactor(web): align search input with dify ui (#37101)
This commit is contained in:
parent
e40b30d746
commit
23cd129802
@ -660,9 +660,6 @@
|
|||||||
"web/app/components/apps/list.tsx": {
|
"web/app/components/apps/list.tsx": {
|
||||||
"no-restricted-globals": {
|
"no-restricted-globals": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/apps/new-app-card.tsx": {
|
"web/app/components/apps/new-app-card.tsx": {
|
||||||
@ -1744,9 +1741,6 @@
|
|||||||
"web/app/components/base/search-input/index.stories.tsx": {
|
"web/app/components/base/search-input/index.stories.tsx": {
|
||||||
"no-console": {
|
"no-console": {
|
||||||
"count": 3
|
"count": 3
|
||||||
},
|
|
||||||
"ts/no-explicit-any": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/base/svg-gallery/index.tsx": {
|
"web/app/components/base/svg-gallery/index.tsx": {
|
||||||
|
|||||||
@ -203,9 +203,9 @@ vi.mock('../app-card', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../new-app-card', () => ({
|
vi.mock('../new-app-card', () => ({
|
||||||
default: React.forwardRef((_props: unknown, _ref: React.ForwardedRef<unknown>) => {
|
default: (_props: { ref?: React.Ref<unknown> }) => {
|
||||||
return React.createElement('div', { 'data-testid': 'new-app-card', 'role': 'button' }, 'New App Card')
|
return React.createElement('div', { 'data-testid': 'new-app-card', 'role': 'button' }, 'New App Card')
|
||||||
}),
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../empty', () => ({
|
vi.mock('../empty', () => ({
|
||||||
@ -293,7 +293,7 @@ describe('List', () => {
|
|||||||
|
|
||||||
it('should render search input', () => {
|
it('should render search input', () => {
|
||||||
renderList()
|
renderList()
|
||||||
expect(screen.getByRole('textbox'))!.toBeInTheDocument()
|
expect(screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' }))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render tag filter', () => {
|
it('should render tag filter', () => {
|
||||||
@ -360,13 +360,13 @@ describe('List', () => {
|
|||||||
describe('Search Functionality', () => {
|
describe('Search Functionality', () => {
|
||||||
it('should render search input field', () => {
|
it('should render search input field', () => {
|
||||||
renderList()
|
renderList()
|
||||||
expect(screen.getByRole('textbox'))!.toBeInTheDocument()
|
expect(screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' }))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle search input change', () => {
|
it('should handle search input change', () => {
|
||||||
renderList()
|
renderList()
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' })
|
||||||
fireEvent.change(input, { target: { value: 'test search' } })
|
fireEvent.change(input, { target: { value: 'test search' } })
|
||||||
|
|
||||||
expect(mockSetKeywords).toHaveBeenCalledWith('test search')
|
expect(mockSetKeywords).toHaveBeenCalledWith('test search')
|
||||||
@ -377,10 +377,7 @@ describe('List', () => {
|
|||||||
|
|
||||||
renderList()
|
renderList()
|
||||||
|
|
||||||
const clearButton = document.querySelector('.group')
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.clear' }))
|
||||||
expect(clearButton)!.toBeInTheDocument()
|
|
||||||
if (clearButton)
|
|
||||||
fireEvent.click(clearButton)
|
|
||||||
|
|
||||||
expect(mockSetKeywords).toHaveBeenCalledWith('')
|
expect(mockSetKeywords).toHaveBeenCalledWith('')
|
||||||
})
|
})
|
||||||
@ -505,7 +502,7 @@ describe('List', () => {
|
|||||||
it('should render with all filter options visible', () => {
|
it('should render with all filter options visible', () => {
|
||||||
renderList()
|
renderList()
|
||||||
|
|
||||||
expect(screen.getByRole('textbox'))!.toBeInTheDocument()
|
expect(screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' }))!.toBeInTheDocument()
|
||||||
expect(screen.getByText('common.tag.placeholder'))!.toBeInTheDocument()
|
expect(screen.getByText('common.tag.placeholder'))!.toBeInTheDocument()
|
||||||
expect(screen.getByText('app.showMyCreatedAppsOnly'))!.toBeInTheDocument()
|
expect(screen.getByText('app.showMyCreatedAppsOnly'))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import type { AppListQuery } from '@/contract/console/apps'
|
import type { AppListQuery } from '@/contract/console/apps'
|
||||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
@ -8,7 +7,7 @@ import { keepPreviousData, useInfiniteQuery, useSuspenseQuery } from '@tanstack/
|
|||||||
import { useDebounce } from 'ahooks'
|
import { useDebounce } from 'ahooks'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Input from '@/app/components/base/input'
|
import { SearchInput } from '@/app/components/base/search-input'
|
||||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
@ -39,9 +38,9 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
|
|||||||
type Props = {
|
type Props = {
|
||||||
controlRefreshList?: number
|
controlRefreshList?: number
|
||||||
}
|
}
|
||||||
const List: FC<Props> = ({
|
function List({
|
||||||
controlRefreshList = 0,
|
controlRefreshList = 0,
|
||||||
}) => {
|
}: Props) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
|
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
|
||||||
@ -224,13 +223,12 @@ const List: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<TagFilter type="app" value={tagIDs} onChange={setTagIDs} onOpenTagManagement={() => setShowTagManagementModal(true)} />
|
<TagFilter type="app" value={tagIDs} onChange={setTagIDs} onOpenTagManagement={() => setShowTagManagementModal(true)} />
|
||||||
<Input
|
<SearchInput
|
||||||
showLeftIcon
|
className="w-52"
|
||||||
showClearIcon
|
|
||||||
wrapperClassName="w-[200px]"
|
|
||||||
value={keywords}
|
value={keywords}
|
||||||
onChange={e => setKeywords(e.target.value)}
|
onValueChange={setKeywords}
|
||||||
onClear={() => setKeywords('')}
|
placeholder={t('operation.search', { ns: 'common' })}
|
||||||
|
aria-label={t('gotoAnything.actions.searchApplications', { ns: 'app' })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { CheckboxList } from '..'
|
|||||||
|
|
||||||
describe('checkbox list component', () => {
|
describe('checkbox list component', () => {
|
||||||
const selectAllName = 'common.operation.selectAll'
|
const selectAllName = 'common.operation.selectAll'
|
||||||
|
const getSearchInput = () => screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||||
const options = [
|
const options = [
|
||||||
{ label: 'Option 1', value: 'option1' },
|
{ label: 'Option 1', value: 'option1' },
|
||||||
{ label: 'Option 2', value: 'option2' },
|
{ label: 'Option 2', value: 'option2' },
|
||||||
@ -30,7 +31,7 @@ describe('checkbox list component', () => {
|
|||||||
it('filters options by label', async () => {
|
it('filters options by label', async () => {
|
||||||
render(<CheckboxList options={options} />)
|
render(<CheckboxList options={options} />)
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = getSearchInput()
|
||||||
await userEvent.type(input, 'app')
|
await userEvent.type(input, 'app')
|
||||||
|
|
||||||
expect(screen.getByText('Apple'))!.toBeInTheDocument()
|
expect(screen.getByText('Apple'))!.toBeInTheDocument()
|
||||||
@ -116,7 +117,7 @@ describe('checkbox list component', () => {
|
|||||||
|
|
||||||
it('hides select-all checkbox when searching', async () => {
|
it('hides select-all checkbox when searching', async () => {
|
||||||
render(<CheckboxList options={options} />)
|
render(<CheckboxList options={options} />)
|
||||||
await userEvent.type(screen.getByRole('textbox'), 'app')
|
await userEvent.type(getSearchInput(), 'app')
|
||||||
expect(screen.queryByRole('checkbox', { name: selectAllName })).not.toBeInTheDocument()
|
expect(screen.queryByRole('checkbox', { name: selectAllName })).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -182,7 +183,7 @@ describe('checkbox list component', () => {
|
|||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = getSearchInput()
|
||||||
await userEvent.type(input, 'ban')
|
await userEvent.type(input, 'ban')
|
||||||
await userEvent.click(screen.getByText('common.operation.resetKeywords'))
|
await userEvent.click(screen.getByText('common.operation.resetKeywords'))
|
||||||
expect(input)!.toHaveValue('')
|
expect(input)!.toHaveValue('')
|
||||||
@ -293,7 +294,7 @@ describe('checkbox list component', () => {
|
|||||||
it('filters options correctly when searching', async () => {
|
it('filters options correctly when searching', async () => {
|
||||||
render(<CheckboxList options={options} />)
|
render(<CheckboxList options={options} />)
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = getSearchInput()
|
||||||
await userEvent.type(input, 'option')
|
await userEvent.type(input, 'option')
|
||||||
|
|
||||||
expect(screen.getByText('Option 1'))!.toBeInTheDocument()
|
expect(screen.getByText('Option 1'))!.toBeInTheDocument()
|
||||||
@ -305,7 +306,7 @@ describe('checkbox list component', () => {
|
|||||||
it('shows no data message when no options match search', async () => {
|
it('shows no data message when no options match search', async () => {
|
||||||
render(<CheckboxList options={options} />)
|
render(<CheckboxList options={options} />)
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = getSearchInput()
|
||||||
await userEvent.type(input, 'xyz')
|
await userEvent.type(input, 'xyz')
|
||||||
|
|
||||||
expect(screen.getByText(/common.operation.noSearchResults/i))!.toBeInTheDocument()
|
expect(screen.getByText(/common.operation.noSearchResults/i))!.toBeInTheDocument()
|
||||||
@ -372,7 +373,7 @@ describe('checkbox list component', () => {
|
|||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = getSearchInput()
|
||||||
await userEvent.type(input, 'opt')
|
await userEvent.type(input, 'opt')
|
||||||
|
|
||||||
expect(screen.getByText(/operation.searchCount/i))!.toBeInTheDocument()
|
expect(screen.getByText(/operation.searchCount/i))!.toBeInTheDocument()
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Badge from '@/app/components/base/badge'
|
import Badge from '@/app/components/base/badge'
|
||||||
import SearchInput from '@/app/components/base/search-input'
|
import { SearchInput } from '@/app/components/base/search-input'
|
||||||
import SearchMenu from '@/assets/search-menu.svg'
|
import SearchMenu from '@/assets/search-menu.svg'
|
||||||
|
|
||||||
type CheckboxListOption = {
|
type CheckboxListOption = {
|
||||||
@ -134,7 +134,7 @@ export const CheckboxList = ({
|
|||||||
{showSearch && (
|
{showSearch && (
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={setSearchQuery}
|
onValueChange={setSearchQuery}
|
||||||
placeholder={t('placeholder.search', { ns: 'common' })}
|
placeholder={t('placeholder.search', { ns: 'common' })}
|
||||||
className="w-40"
|
className="w-40"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,23 +1,31 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import SearchInput from '..'
|
import { useState } from 'react'
|
||||||
|
import { SearchInput } from '..'
|
||||||
|
|
||||||
describe('SearchInput', () => {
|
describe('SearchInput', () => {
|
||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
it('renders correctly with default props', () => {
|
it('renders correctly with default props', () => {
|
||||||
render(<SearchInput value="" onChange={() => {}} />)
|
render(<SearchInput value="" onValueChange={() => {}} />)
|
||||||
const input = screen.getByPlaceholderText('common.operation.search')
|
const input = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||||
expect(input).toBeInTheDocument()
|
expect(input).toBeInTheDocument()
|
||||||
expect(input).toHaveValue('')
|
expect(input).toHaveValue('')
|
||||||
|
expect(input).toHaveAttribute('name', 'query')
|
||||||
|
expect(input).toHaveAttribute('autocomplete', 'off')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders custom placeholder', () => {
|
it('renders custom placeholder', () => {
|
||||||
render(<SearchInput value="" onChange={() => {}} placeholder="Custom Placeholder" />)
|
render(<SearchInput value="" onValueChange={() => {}} placeholder="Custom Placeholder" />)
|
||||||
expect(screen.getByPlaceholderText('Custom Placeholder')).toBeInTheDocument()
|
expect(screen.getByRole('searchbox', { name: 'common.operation.search' })).toHaveAttribute('placeholder', 'Custom Placeholder')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses custom aria label', () => {
|
||||||
|
render(<SearchInput value="" onValueChange={() => {}} aria-label="Search providers" />)
|
||||||
|
expect(screen.getByRole('searchbox', { name: 'Search providers' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows clear button when value is present', () => {
|
it('shows clear button when value is present', () => {
|
||||||
const onChange = vi.fn()
|
const onValueChange = vi.fn()
|
||||||
render(<SearchInput value="has value" onChange={onChange} />)
|
render(<SearchInput value="has value" onValueChange={onValueChange} />)
|
||||||
|
|
||||||
const clearButton = screen.getByLabelText('common.operation.clear')
|
const clearButton = screen.getByLabelText('common.operation.clear')
|
||||||
expect(clearButton).toBeInTheDocument()
|
expect(clearButton).toBeInTheDocument()
|
||||||
@ -25,65 +33,113 @@ describe('SearchInput', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Interaction', () => {
|
describe('Interaction', () => {
|
||||||
it('calls onChange when typing', () => {
|
it('calls onValueChange when typing', () => {
|
||||||
const onChange = vi.fn()
|
const onValueChange = vi.fn()
|
||||||
render(<SearchInput value="" onChange={onChange} />)
|
render(<SearchInput value="" onValueChange={onValueChange} />)
|
||||||
const input = screen.getByPlaceholderText('common.operation.search')
|
const input = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||||
|
|
||||||
fireEvent.change(input, { target: { value: 'test' } })
|
fireEvent.change(input, { target: { value: 'test' } })
|
||||||
expect(onChange).toHaveBeenCalledWith('test')
|
expect(onValueChange).toHaveBeenCalledWith('test')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles composition events', () => {
|
it('handles composition events', () => {
|
||||||
const onChange = vi.fn()
|
const onValueChange = vi.fn()
|
||||||
render(<SearchInput value="initial" onChange={onChange} />)
|
render(<SearchInput value="initial" onValueChange={onValueChange} />)
|
||||||
const input = screen.getByPlaceholderText('common.operation.search')
|
const input = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||||
|
|
||||||
// Start composition
|
|
||||||
fireEvent.compositionStart(input)
|
fireEvent.compositionStart(input)
|
||||||
fireEvent.change(input, { target: { value: 'final' } })
|
fireEvent.change(input, { target: { value: 'final' } })
|
||||||
|
|
||||||
// While composing, onChange should NOT be called
|
expect(onValueChange).not.toHaveBeenCalled()
|
||||||
expect(onChange).not.toHaveBeenCalled()
|
|
||||||
expect(input).toHaveValue('final')
|
expect(input).toHaveValue('final')
|
||||||
|
|
||||||
// End composition
|
|
||||||
fireEvent.compositionEnd(input)
|
fireEvent.compositionEnd(input)
|
||||||
expect(onChange).toHaveBeenCalledTimes(1)
|
expect(onValueChange).toHaveBeenCalledTimes(1)
|
||||||
expect(onChange).toHaveBeenCalledWith('final')
|
expect(onValueChange).toHaveBeenCalledWith('final')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls onChange with empty string when clear button is clicked', () => {
|
it('does not keep stale composition commits after the next distinct change', () => {
|
||||||
const onChange = vi.fn()
|
const onValueChange = vi.fn()
|
||||||
render(<SearchInput value="has value" onChange={onChange} />)
|
|
||||||
|
function ControlledSearchInput() {
|
||||||
|
const [value, setValue] = useState('initial')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchInput
|
||||||
|
value={value}
|
||||||
|
onValueChange={(nextValue) => {
|
||||||
|
onValueChange(nextValue)
|
||||||
|
setValue(nextValue)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<ControlledSearchInput />)
|
||||||
|
const input = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||||
|
|
||||||
|
fireEvent.compositionStart(input)
|
||||||
|
fireEvent.change(input, { target: { value: 'final' } })
|
||||||
|
fireEvent.compositionEnd(input)
|
||||||
|
fireEvent.change(input, { target: { value: 'finalx' } })
|
||||||
|
fireEvent.change(input, { target: { value: 'final' } })
|
||||||
|
|
||||||
|
expect(onValueChange).toHaveBeenCalledTimes(3)
|
||||||
|
expect(onValueChange).toHaveBeenNthCalledWith(1, 'final')
|
||||||
|
expect(onValueChange).toHaveBeenNthCalledWith(2, 'finalx')
|
||||||
|
expect(onValueChange).toHaveBeenNthCalledWith(3, 'final')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears composition value without committing stale text', () => {
|
||||||
|
const onValueChange = vi.fn()
|
||||||
|
|
||||||
|
function ControlledSearchInput() {
|
||||||
|
const [value, setValue] = useState('initial')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchInput
|
||||||
|
value={value}
|
||||||
|
onValueChange={(nextValue) => {
|
||||||
|
onValueChange(nextValue)
|
||||||
|
setValue(nextValue)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<ControlledSearchInput />)
|
||||||
|
const input = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||||
|
|
||||||
|
fireEvent.compositionStart(input)
|
||||||
|
fireEvent.change(input, { target: { value: 'final' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.clear' }))
|
||||||
|
fireEvent.compositionEnd(input)
|
||||||
|
|
||||||
|
expect(input).toHaveValue('')
|
||||||
|
expect(onValueChange).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onValueChange).toHaveBeenCalledWith('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onValueChange with empty string when clear button is clicked', () => {
|
||||||
|
const onValueChange = vi.fn()
|
||||||
|
render(<SearchInput value="has value" onValueChange={onValueChange} />)
|
||||||
|
|
||||||
const clearButton = screen.getByLabelText('common.operation.clear')
|
const clearButton = screen.getByLabelText('common.operation.clear')
|
||||||
fireEvent.click(clearButton)
|
fireEvent.click(clearButton)
|
||||||
expect(onChange).toHaveBeenCalledWith('')
|
expect(onValueChange).toHaveBeenCalledWith('')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates focus state on focus/blur', () => {
|
it('uses dify-ui input spacing for the search adornment', () => {
|
||||||
const { container } = render(<SearchInput value="" onChange={() => {}} />)
|
render(<SearchInput value="" onValueChange={() => {}} />)
|
||||||
const wrapper = container.firstChild as HTMLElement
|
const input = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||||
const input = screen.getByPlaceholderText('common.operation.search')
|
expect(input).toHaveClass('ps-7')
|
||||||
|
expect(input).not.toHaveClass('h-[18px]')
|
||||||
fireEvent.focus(input)
|
|
||||||
expect(wrapper).toHaveClass(/bg-components-input-bg-active/)
|
|
||||||
|
|
||||||
fireEvent.blur(input)
|
|
||||||
expect(wrapper).not.toHaveClass(/bg-components-input-bg-active/)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Style', () => {
|
describe('Style', () => {
|
||||||
it('applies white style', () => {
|
|
||||||
const { container } = render(<SearchInput value="" onChange={() => {}} white />)
|
|
||||||
const wrapper = container.firstChild as HTMLElement
|
|
||||||
expect(wrapper).toHaveClass('bg-white!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('applies custom className', () => {
|
it('applies custom className', () => {
|
||||||
const { container } = render(<SearchInput value="" onChange={() => {}} className="custom-test" />)
|
const { container } = render(<SearchInput value="" onValueChange={() => {}} className="custom-test" />)
|
||||||
const wrapper = container.firstChild as HTMLElement
|
const wrapper = container.firstChild as HTMLElement
|
||||||
expect(wrapper).toHaveClass('custom-test')
|
expect(wrapper).toHaveClass('custom-test')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||||
|
import type { ComponentProps } from 'react'
|
||||||
import { Kbd } from '@langgenius/dify-ui/kbd'
|
import { Kbd } from '@langgenius/dify-ui/kbd'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import SearchInput from '.'
|
import { SearchInput } from '.'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Base/Data Entry/SearchInput',
|
title: 'Base/Data Entry/SearchInput',
|
||||||
@ -20,25 +21,21 @@ const meta = {
|
|||||||
control: 'text',
|
control: 'text',
|
||||||
description: 'Search input value',
|
description: 'Search input value',
|
||||||
},
|
},
|
||||||
onChange: {
|
onValueChange: {
|
||||||
action: 'changed',
|
action: 'value changed',
|
||||||
description: 'Change handler',
|
description: 'Value change handler',
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
control: 'text',
|
control: 'text',
|
||||||
description: 'Placeholder text',
|
description: 'Placeholder text',
|
||||||
},
|
},
|
||||||
white: {
|
|
||||||
control: 'boolean',
|
|
||||||
description: 'White background variant',
|
|
||||||
},
|
|
||||||
className: {
|
className: {
|
||||||
control: 'text',
|
control: 'text',
|
||||||
description: 'Additional CSS classes',
|
description: 'Additional CSS classes',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
onChange: (v) => {
|
onValueChange: (v: string) => {
|
||||||
console.log('Search value changed:', v)
|
console.log('Search value changed:', v)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -47,8 +44,7 @@ const meta = {
|
|||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
// Interactive demo wrapper
|
const SearchInputDemo = (args: ComponentProps<typeof SearchInput>) => {
|
||||||
const SearchInputDemo = (args: any) => {
|
|
||||||
const [value, setValue] = useState(args.value || '')
|
const [value, setValue] = useState(args.value || '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -56,7 +52,7 @@ const SearchInputDemo = (args: any) => {
|
|||||||
<SearchInput
|
<SearchInput
|
||||||
{...args}
|
{...args}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(v) => {
|
onValueChange={(v: string) => {
|
||||||
setValue(v)
|
setValue(v)
|
||||||
console.log('Search value changed:', v)
|
console.log('Search value changed:', v)
|
||||||
}}
|
}}
|
||||||
@ -77,31 +73,19 @@ export const Default: Story = {
|
|||||||
render: args => <SearchInputDemo {...args} />,
|
render: args => <SearchInputDemo {...args} />,
|
||||||
args: {
|
args: {
|
||||||
placeholder: 'Search...',
|
placeholder: 'Search...',
|
||||||
white: false,
|
|
||||||
value: '',
|
value: '',
|
||||||
onChange: (v) => {
|
onValueChange: (v: string) => {
|
||||||
console.log('Search value changed:', v)
|
console.log('Search value changed:', v)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// White variant
|
|
||||||
export const WhiteBackground: Story = {
|
|
||||||
render: args => <SearchInputDemo {...args} />,
|
|
||||||
args: {
|
|
||||||
placeholder: 'Search...',
|
|
||||||
white: true,
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// With initial value
|
// With initial value
|
||||||
export const WithInitialValue: Story = {
|
export const WithInitialValue: Story = {
|
||||||
render: args => <SearchInputDemo {...args} />,
|
render: args => <SearchInputDemo {...args} />,
|
||||||
args: {
|
args: {
|
||||||
value: 'Initial search query',
|
value: 'Initial search query',
|
||||||
placeholder: 'Search...',
|
placeholder: 'Search...',
|
||||||
white: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +94,6 @@ export const CustomPlaceholder: Story = {
|
|||||||
render: args => <SearchInputDemo {...args} />,
|
render: args => <SearchInputDemo {...args} />,
|
||||||
args: {
|
args: {
|
||||||
placeholder: 'Search documents, files, and more...',
|
placeholder: 'Search documents, files, and more...',
|
||||||
white: false,
|
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -138,7 +121,7 @@ const UserListSearchDemo = () => {
|
|||||||
<h3 className="mb-4 text-lg font-semibold">Team Members</h3>
|
<h3 className="mb-4 text-lg font-semibold">Team Members</h3>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={setSearchQuery}
|
onValueChange={setSearchQuery}
|
||||||
placeholder="Search by name, email, or role..."
|
placeholder="Search by name, email, or role..."
|
||||||
/>
|
/>
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
@ -212,9 +195,8 @@ const ProductSearchDemo = () => {
|
|||||||
<h3 className="mb-4 text-lg font-semibold">Product Catalog</h3>
|
<h3 className="mb-4 text-lg font-semibold">Product Catalog</h3>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={setSearchQuery}
|
onValueChange={setSearchQuery}
|
||||||
placeholder="Search products..."
|
placeholder="Search products..."
|
||||||
white
|
|
||||||
/>
|
/>
|
||||||
<div className="mt-4 grid grid-cols-2 gap-3">
|
<div className="mt-4 grid grid-cols-2 gap-3">
|
||||||
{filteredProducts.length > 0
|
{filteredProducts.length > 0
|
||||||
@ -272,10 +254,8 @@ const DocumentationSearchDemo = () => {
|
|||||||
<p className="mb-4 text-sm text-gray-600">Search our comprehensive guides and API references</p>
|
<p className="mb-4 text-sm text-gray-600">Search our comprehensive guides and API references</p>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={setSearchQuery}
|
onValueChange={setSearchQuery}
|
||||||
placeholder="Search documentation..."
|
placeholder="Search documentation..."
|
||||||
white
|
|
||||||
className="h-10!"
|
|
||||||
/>
|
/>
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
{filteredDocs.length > 0
|
{filteredDocs.length > 0
|
||||||
@ -338,10 +318,8 @@ const CommandPaletteDemo = () => {
|
|||||||
<div className="border-b border-gray-200 p-4">
|
<div className="border-b border-gray-200 p-4">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={setSearchQuery}
|
onValueChange={setSearchQuery}
|
||||||
placeholder="Type a command or search..."
|
placeholder="Type a command or search..."
|
||||||
white
|
|
||||||
className="h-10!"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[400px] overflow-y-auto">
|
<div className="max-h-[400px] overflow-y-auto">
|
||||||
@ -413,13 +391,13 @@ const LiveSearchWithCountDemo = () => {
|
|||||||
</div>
|
</div>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={setSearchQuery}
|
onValueChange={setSearchQuery}
|
||||||
placeholder="Search resources..."
|
placeholder="Search resources..."
|
||||||
/>
|
/>
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
{filteredItems.map((item, index) => (
|
{filteredItems.map(item => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={item}
|
||||||
className="cursor-pointer rounded-lg border border-gray-200 p-3 transition-colors hover:border-blue-300 hover:bg-blue-50"
|
className="cursor-pointer rounded-lg border border-gray-200 p-3 transition-colors hover:border-blue-300 hover:bg-blue-50"
|
||||||
>
|
>
|
||||||
<div className="text-sm font-medium">{item}</div>
|
<div className="text-sm font-medium">{item}</div>
|
||||||
@ -445,24 +423,22 @@ const SizeVariationsDemo = () => {
|
|||||||
<div style={{ width: '500px' }} className="space-y-4">
|
<div style={{ width: '500px' }} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-xs font-medium text-gray-600">Default Size</label>
|
<label className="mb-2 block text-xs font-medium text-gray-600">Default Size</label>
|
||||||
<SearchInput value={value1} onChange={setValue1} placeholder="Search..." />
|
<SearchInput value={value1} onValueChange={setValue1} placeholder="Search..." />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-xs font-medium text-gray-600">Medium Size</label>
|
<label className="mb-2 block text-xs font-medium text-gray-600">Medium Size</label>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={value2}
|
value={value2}
|
||||||
onChange={setValue2}
|
onValueChange={setValue2}
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
className="h-10!"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-xs font-medium text-gray-600">Large Size</label>
|
<label className="mb-2 block text-xs font-medium text-gray-600">Large Size</label>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={value3}
|
value={value3}
|
||||||
onChange={setValue3}
|
onValueChange={setValue3}
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
className="h-12!"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -480,6 +456,5 @@ export const Playground: Story = {
|
|||||||
args: {
|
args: {
|
||||||
value: '',
|
value: '',
|
||||||
placeholder: 'Search...',
|
placeholder: 'Search...',
|
||||||
white: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,86 +1,100 @@
|
|||||||
import type { FC } from 'react'
|
import type { ComponentProps } from 'react'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { RiCloseCircleFill, RiSearchLine } from '@remixicon/react'
|
import { Input } from '@langgenius/dify-ui/input'
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
type SearchInputProps = {
|
type SearchInputProps = {
|
||||||
|
value: string
|
||||||
|
onValueChange: (value: string) => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
className?: string
|
className?: string
|
||||||
value: string
|
} & Pick<ComponentProps<'input'>, 'aria-label'>
|
||||||
onChange: (v: string) => void
|
|
||||||
white?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchInput: FC<SearchInputProps> = ({
|
export function SearchInput({
|
||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onValueChange,
|
||||||
white,
|
'aria-label': ariaLabel,
|
||||||
}) => {
|
}: SearchInputProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [focus, setFocus] = useState<boolean>(false)
|
const isComposingRef = useRef<boolean>(false)
|
||||||
const isComposing = useRef<boolean>(false)
|
const compositionCommitRef = useRef<string | null>(null)
|
||||||
const [compositionValue, setCompositionValue] = useState<string>('')
|
const [compositionValue, setCompositionValue] = useState('')
|
||||||
|
const inputValue = isComposingRef.current ? compositionValue : value
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
isComposingRef.current = false
|
||||||
|
compositionCommitRef.current = null
|
||||||
|
setCompositionValue('')
|
||||||
|
onValueChange('')
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'group flex h-8 items-center overflow-hidden rounded-lg border-none bg-components-input-bg-normal px-2 hover:bg-components-input-bg-hover',
|
'relative',
|
||||||
focus && 'bg-components-input-bg-active!',
|
|
||||||
white && 'border-gray-300! bg-white! shadow-xs hover:border-gray-300! hover:bg-white!',
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none mr-1.5 flex size-4 shrink-0 items-center justify-center">
|
<span className="pointer-events-none absolute top-1/2 left-2 i-ri-search-line size-4 -translate-y-1/2 text-components-input-text-placeholder" aria-hidden="true" />
|
||||||
<RiSearchLine className="size-4 text-components-input-text-placeholder" aria-hidden="true" />
|
<Input
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="search"
|
||||||
name="query"
|
name="query"
|
||||||
|
aria-label={ariaLabel ?? t('operation.search', { ns: 'common' })}
|
||||||
className={cn(
|
className={cn(
|
||||||
'caret-#295EFF block h-[18px] grow appearance-none border-0 bg-transparent system-sm-regular text-components-input-text-filled outline-hidden placeholder:text-components-input-text-placeholder',
|
'ps-7',
|
||||||
white && 'bg-white! group-hover:bg-white! placeholder:text-gray-400! hover:bg-white!',
|
!!inputValue && 'pe-7',
|
||||||
|
'[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none',
|
||||||
)}
|
)}
|
||||||
placeholder={placeholder || t('operation.search', { ns: 'common' })!}
|
placeholder={placeholder ?? t('operation.search', { ns: 'common' })}
|
||||||
value={isComposing.current ? compositionValue : value}
|
value={inputValue}
|
||||||
onChange={(e) => {
|
onValueChange={(nextValue) => {
|
||||||
const newValue = e.target.value
|
if (isComposingRef.current) {
|
||||||
if (isComposing.current)
|
setCompositionValue(nextValue)
|
||||||
setCompositionValue(newValue)
|
return
|
||||||
else
|
}
|
||||||
onChange(newValue)
|
if (compositionCommitRef.current !== null) {
|
||||||
|
if (compositionCommitRef.current !== nextValue) {
|
||||||
|
compositionCommitRef.current = null
|
||||||
|
onValueChange(nextValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
compositionCommitRef.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onValueChange(nextValue)
|
||||||
}}
|
}}
|
||||||
onCompositionStart={() => {
|
onCompositionStart={() => {
|
||||||
isComposing.current = true
|
isComposingRef.current = true
|
||||||
|
compositionCommitRef.current = null
|
||||||
setCompositionValue(value)
|
setCompositionValue(value)
|
||||||
}}
|
}}
|
||||||
onCompositionEnd={(e) => {
|
onCompositionEnd={(e) => {
|
||||||
isComposing.current = false
|
if (!isComposingRef.current)
|
||||||
|
return
|
||||||
|
|
||||||
|
isComposingRef.current = false
|
||||||
setCompositionValue('')
|
setCompositionValue('')
|
||||||
onChange(e.currentTarget.value)
|
compositionCommitRef.current = e.currentTarget.value
|
||||||
|
onValueChange(e.currentTarget.value)
|
||||||
}}
|
}}
|
||||||
onFocus={() => setFocus(true)}
|
|
||||||
onBlur={() => setFocus(false)}
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
enterKeyHint="search"
|
||||||
/>
|
/>
|
||||||
{value && (
|
{!!inputValue && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t('operation.clear', { ns: 'common' })}
|
aria-label={t('operation.clear', { ns: 'common' })}
|
||||||
className="group/clear flex size-4 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0"
|
className="group/clear absolute top-1/2 right-2 flex size-4 -translate-y-1/2 cursor-pointer touch-manipulation items-center justify-center rounded-md border-none bg-transparent p-0 outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset"
|
||||||
onClick={() => {
|
onClick={handleClear}
|
||||||
onChange('')
|
|
||||||
inputRef.current?.focus()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RiCloseCircleFill className="size-4 text-text-quaternary group-hover/clear:text-text-tertiary" />
|
<span className="i-ri-close-circle-fill size-4 text-text-quaternary group-hover/clear:text-text-tertiary" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SearchInput
|
|
||||||
|
|||||||
@ -461,7 +461,7 @@ describe('AccountSetting', () => {
|
|||||||
renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.PROVIDER })
|
renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||||
fireEvent.change(input, { target: { value: 'test-search' } })
|
fireEvent.change(input, { target: { value: 'test-search' } })
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
|||||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import SearchInput from '@/app/components/base/search-input'
|
import { SearchInput } from '@/app/components/base/search-input'
|
||||||
import BillingPage from '@/app/components/billing/billing-page'
|
import BillingPage from '@/app/components/billing/billing-page'
|
||||||
import CustomPage from '@/app/components/custom/custom-page'
|
import CustomPage from '@/app/components/custom/custom-page'
|
||||||
import {
|
import {
|
||||||
@ -218,8 +218,8 @@ export default function AccountSetting({
|
|||||||
{activeItem?.key === ACCOUNT_SETTING_TAB.PROVIDER && (
|
{activeItem?.key === ACCOUNT_SETTING_TAB.PROVIDER && (
|
||||||
<div className="flex grow justify-end">
|
<div className="flex grow justify-end">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
className="w-[200px]"
|
className="w-52"
|
||||||
onChange={setSearchValue}
|
onValueChange={setSearchValue}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -42,20 +42,20 @@ vi.mock('@/app/components/plugins/install-plugin/base/use-get-icon', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/app/components/base/search-input', () => ({
|
vi.mock('@/app/components/base/search-input', () => ({
|
||||||
default: ({
|
SearchInput: ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onValueChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: {
|
}: {
|
||||||
value: string
|
value: string
|
||||||
onChange: (value: string) => void
|
onValueChange: (value: string) => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
className?: string
|
className?: string
|
||||||
}) => (
|
}) => (
|
||||||
<input
|
<input
|
||||||
aria-label={placeholder}
|
aria-label={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.value)}
|
onChange={e => onValueChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
@ -220,14 +220,14 @@ vi.mock('@langgenius/dify-ui/popover', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContext.Provider value={{ open, setOpen }}>
|
<PopoverContext value={{ open, setOpen }}>
|
||||||
{children}
|
{children}
|
||||||
</PopoverContext.Provider>
|
</PopoverContext>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopoverTrigger = ({ render }: { render: React.ReactNode }) => {
|
const PopoverTrigger = ({ render }: { render: React.ReactNode }) => {
|
||||||
const { open, setOpen } = React.useContext(PopoverContext)
|
const { open, setOpen } = React.use(PopoverContext)
|
||||||
return (
|
return (
|
||||||
<div data-testid="agent-strategy-trigger" onClick={() => setOpen(!open)}>
|
<div data-testid="agent-strategy-trigger" onClick={() => setOpen(!open)}>
|
||||||
{render}
|
{render}
|
||||||
@ -236,7 +236,7 @@ vi.mock('@langgenius/dify-ui/popover', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PopoverContent = ({ children }: { children: React.ReactNode }) => {
|
const PopoverContent = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { open } = React.useContext(PopoverContext)
|
const { open } = React.use(PopoverContext)
|
||||||
return open ? <div data-testid="agent-strategy-popover">{children}</div> : null
|
return open ? <div data-testid="agent-strategy-popover">{children}</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,11 +14,10 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@langgenius/dify-ui/tooltip'
|
} from '@langgenius/dify-ui/tooltip'
|
||||||
import { RiArrowDownSLine, RiErrorWarningFill } from '@remixicon/react'
|
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import SearchInput from '@/app/components/base/search-input'
|
import { SearchInput } from '@/app/components/base/search-input'
|
||||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||||
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
|
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
|
||||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
@ -45,7 +44,7 @@ const NotFoundWarn = (props: {
|
|||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={<div><RiErrorWarningFill className="size-4 text-text-destructive" /></div>}
|
render={<div><span className="i-ri-error-warning-fill size-4 text-text-destructive" aria-hidden="true" /></div>}
|
||||||
/>
|
/>
|
||||||
<TooltipContent className="w-[180px]">
|
<TooltipContent className="w-[180px]">
|
||||||
<div className="space-y-1 text-xs">
|
<div className="space-y-1 text-xs">
|
||||||
@ -204,7 +203,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
|||||||
description={t('nodes.agent.strategyNotFoundDesc', { ns: 'workflow' })}
|
description={t('nodes.agent.strategyNotFoundDesc', { ns: 'workflow' })}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: <RiArrowDownSLine className="size-4 text-text-tertiary" />}
|
: <span className="i-ri-arrow-down-s-line size-4 text-text-tertiary" aria-hidden="true" />}
|
||||||
{showSwitchVersion && value && (
|
{showSwitchVersion && value && (
|
||||||
<SwitchPluginVersion
|
<SwitchPluginVersion
|
||||||
uniqueIdentifier={value.plugin_unique_identifier}
|
uniqueIdentifier={value.plugin_unique_identifier}
|
||||||
@ -234,7 +233,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
|||||||
>
|
>
|
||||||
<div className="w-[388px] overflow-hidden rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow">
|
<div className="w-[388px] overflow-hidden rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow">
|
||||||
<header className="flex gap-1 p-2">
|
<header className="flex gap-1 p-2">
|
||||||
<SearchInput placeholder={t('nodes.agent.strategy.searchPlaceholder', { ns: 'workflow' })} value={query} onChange={setQuery} className="w-full" />
|
<SearchInput placeholder={t('nodes.agent.strategy.searchPlaceholder', { ns: 'workflow' })} value={query} onValueChange={setQuery} className="w-full" />
|
||||||
<ViewTypeSelect viewType={viewType} onChange={setViewType} />
|
<ViewTypeSelect viewType={viewType} onChange={setViewType} />
|
||||||
</header>
|
</header>
|
||||||
<main className="relative flex w-full flex-col overflow-hidden md:max-h-[300px] xl:max-h-[400px] 2xl:max-h-[564px]" ref={wrapElemRef}>
|
<main className="relative flex w-full flex-col overflow-hidden md:max-h-[300px] xl:max-h-[400px] 2xl:max-h-[564px]" ref={wrapElemRef}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user