mirror of
https://github.com/langgenius/dify.git
synced 2026-02-28 12:25:14 +00:00
feat(web): add hover clear button for provider search (#32707)
Signed-off-by: -LAN- <laipz8200@outlook.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
@@ -43,7 +43,7 @@ describe('Input component', () => {
|
||||
|
||||
it('shows left icon when showLeftIcon is true', () => {
|
||||
render(<Input showLeftIcon />)
|
||||
const searchIcon = document.querySelector('svg')
|
||||
const searchIcon = document.querySelector('.i-ri-search-line')
|
||||
expect(searchIcon).toBeInTheDocument()
|
||||
const input = screen.getByPlaceholderText('Search')
|
||||
expect(input).toHaveClass('pl-[26px]')
|
||||
@@ -51,7 +51,7 @@ describe('Input component', () => {
|
||||
|
||||
it('shows clear icon when showClearIcon is true and has value', () => {
|
||||
render(<Input showClearIcon value="test" />)
|
||||
const clearIcon = document.querySelector('.group svg')
|
||||
const clearIcon = document.querySelector('.i-ri-close-circle-fill')
|
||||
expect(clearIcon).toBeInTheDocument()
|
||||
const input = screen.getByDisplayValue('test')
|
||||
expect(input).toHaveClass('pr-[26px]')
|
||||
@@ -59,21 +59,21 @@ describe('Input component', () => {
|
||||
|
||||
it('does not show clear icon when disabled, even with value', () => {
|
||||
render(<Input showClearIcon value="test" disabled />)
|
||||
const clearIcon = document.querySelector('.group svg')
|
||||
const clearIcon = document.querySelector('.i-ri-close-circle-fill')
|
||||
expect(clearIcon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onClear when clear icon is clicked', () => {
|
||||
const onClear = vi.fn()
|
||||
render(<Input showClearIcon value="test" onClear={onClear} />)
|
||||
const clearIconContainer = document.querySelector('.group')
|
||||
const clearIconContainer = screen.getByTestId('input-clear')
|
||||
fireEvent.click(clearIconContainer!)
|
||||
expect(onClear).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('shows warning icon when destructive is true', () => {
|
||||
render(<Input destructive />)
|
||||
const warningIcon = document.querySelector('svg')
|
||||
const warningIcon = document.querySelector('.i-ri-error-warning-line')
|
||||
expect(warningIcon).toBeInTheDocument()
|
||||
const input = screen.getByPlaceholderText('Please input')
|
||||
expect(input).toHaveClass('border-components-input-border-destructive')
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type { ChangeEventHandler, CSSProperties, FocusEventHandler } from 'react'
|
||||
import { RiCloseCircleFill, RiErrorWarningLine, RiSearchLine } from '@remixicon/react'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
@@ -13,8 +12,8 @@ export const inputVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
regular: 'px-3 radius-md system-sm-regular',
|
||||
large: 'px-4 radius-lg system-md-regular',
|
||||
regular: 'px-3 system-sm-regular radius-md',
|
||||
large: 'px-4 system-md-regular radius-lg',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -83,7 +82,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
|
||||
}
|
||||
return (
|
||||
<div className={cn('relative w-full', wrapperClassName)}>
|
||||
{showLeftIcon && <RiSearchLine className={cn('absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-components-input-text-placeholder')} />}
|
||||
{showLeftIcon && <span className={cn('i-ri-search-line absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-components-input-text-placeholder')} />}
|
||||
<input
|
||||
ref={ref}
|
||||
style={styleCss}
|
||||
@@ -115,11 +114,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
|
||||
onClick={onClear}
|
||||
data-testid="input-clear"
|
||||
>
|
||||
<RiCloseCircleFill className="h-3.5 w-3.5 cursor-pointer text-text-quaternary group-hover:text-text-tertiary" />
|
||||
<span className="i-ri-close-circle-fill h-3.5 w-3.5 cursor-pointer text-text-quaternary group-hover:text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
{destructive && (
|
||||
<RiErrorWarningLine className="absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-text-destructive-secondary" />
|
||||
<span className="i-ri-error-warning-line absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-text-destructive-secondary" />
|
||||
)}
|
||||
{showCopyIcon && (
|
||||
<div className={cn('group absolute right-0 top-1/2 -translate-y-1/2 cursor-pointer')}>
|
||||
@@ -131,7 +130,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
|
||||
)}
|
||||
{
|
||||
unit && (
|
||||
<div className="system-sm-regular absolute right-2 top-1/2 -translate-y-1/2 text-text-tertiary">
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 text-text-tertiary system-sm-regular">
|
||||
{unit}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -334,10 +334,10 @@ describe('FileList', () => {
|
||||
it('should call resetKeywords prop when clear button is clicked', () => {
|
||||
const mockResetKeywords = vi.fn()
|
||||
const props = createDefaultProps({ resetKeywords: mockResetKeywords, keywords: 'to-reset' })
|
||||
const { container } = render(<FileList {...props} />)
|
||||
render(<FileList {...props} />)
|
||||
|
||||
// Act - Click the clear icon div (it contains RiCloseCircleFill icon)
|
||||
const clearButton = container.querySelector('[class*="cursor-pointer"] svg[class*="h-3.5"]')?.parentElement
|
||||
const clearButton = screen.getByTestId('input-clear')
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
fireEvent.click(clearButton!)
|
||||
|
||||
@@ -346,12 +346,12 @@ describe('FileList', () => {
|
||||
|
||||
it('should reset inputValue to empty string when clear is clicked', () => {
|
||||
const props = createDefaultProps({ keywords: 'to-be-reset' })
|
||||
const { container } = render(<FileList {...props} />)
|
||||
render(<FileList {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
|
||||
fireEvent.change(input, { target: { value: 'some-search' } })
|
||||
|
||||
// Act - Find and click the clear icon
|
||||
const clearButton = container.querySelector('[class*="cursor-pointer"] svg[class*="h-3.5"]')?.parentElement
|
||||
const clearButton = screen.getByTestId('input-clear')
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
fireEvent.click(clearButton!)
|
||||
|
||||
|
||||
@@ -93,8 +93,8 @@ describe('Header', () => {
|
||||
|
||||
const { container } = render(<Header {...props} />)
|
||||
|
||||
// Assert - Input should have search icon (RiSearchLine is rendered as svg)
|
||||
const searchIcon = container.querySelector('svg.h-4.w-4')
|
||||
// Assert - Input should have search icon class
|
||||
const searchIcon = container.querySelector('.i-ri-search-line.h-4.w-4')
|
||||
expect(searchIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -313,10 +313,10 @@ describe('Header', () => {
|
||||
inputValue: 'to-clear',
|
||||
handleResetKeywords: mockHandleResetKeywords,
|
||||
})
|
||||
const { container } = render(<Header {...props} />)
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Act - Find and click the clear icon container
|
||||
const clearButton = container.querySelector('[class*="cursor-pointer"] svg[class*="h-3.5"]')?.parentElement
|
||||
const clearButton = screen.getByTestId('input-clear')
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
fireEvent.click(clearButton!)
|
||||
|
||||
@@ -325,19 +325,19 @@ describe('Header', () => {
|
||||
|
||||
it('should not show clear icon when inputValue is empty', () => {
|
||||
const props = createDefaultProps({ inputValue: '' })
|
||||
const { container } = render(<Header {...props} />)
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Act & Assert - Clear icon should not be visible
|
||||
const clearIcon = container.querySelector('[class*="cursor-pointer"] svg[class*="h-3.5"]')
|
||||
const clearIcon = screen.queryByTestId('input-clear')
|
||||
expect(clearIcon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show clear icon when inputValue is not empty', () => {
|
||||
const props = createDefaultProps({ inputValue: 'some-value' })
|
||||
const { container } = render(<Header {...props} />)
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Act & Assert - Clear icon should be visible
|
||||
const clearIcon = container.querySelector('[class*="cursor-pointer"] svg[class*="h-3.5"]')
|
||||
const clearIcon = screen.getByTestId('input-clear')
|
||||
expect(clearIcon).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -570,13 +570,12 @@ describe('Header', () => {
|
||||
inputValue: 'to-clear',
|
||||
handleResetKeywords: mockHandleResetKeywords,
|
||||
})
|
||||
const { container, rerender } = render(<Header {...props} />)
|
||||
const { rerender } = render(<Header {...props} />)
|
||||
|
||||
// Act - Click clear, rerender, click again
|
||||
const clearButton = container.querySelector('[class*="cursor-pointer"] svg[class*="h-3.5"]')?.parentElement
|
||||
fireEvent.click(clearButton!)
|
||||
fireEvent.click(screen.getByTestId('input-clear'))
|
||||
rerender(<Header {...props} />)
|
||||
fireEvent.click(clearButton!)
|
||||
fireEvent.click(screen.getByTestId('input-clear'))
|
||||
|
||||
expect(mockHandleResetKeywords).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
'use client'
|
||||
import type { AccountSettingTab } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
RiBrain2Fill,
|
||||
RiBrain2Line,
|
||||
RiCloseLine,
|
||||
RiColorFilterFill,
|
||||
RiColorFilterLine,
|
||||
RiDatabase2Fill,
|
||||
RiDatabase2Line,
|
||||
RiGroup2Fill,
|
||||
RiGroup2Line,
|
||||
RiMoneyDollarCircleFill,
|
||||
RiMoneyDollarCircleLine,
|
||||
RiPuzzle2Fill,
|
||||
RiPuzzle2Line,
|
||||
RiTranslate2,
|
||||
} from '@remixicon/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import BillingPage from '@/app/components/billing/billing-page'
|
||||
import CustomPage from '@/app/components/custom/custom-page'
|
||||
import {
|
||||
@@ -76,14 +60,14 @@ export default function AccountSetting({
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
name: t('settings.provider', { ns: 'common' }),
|
||||
icon: <RiBrain2Line className={iconClassName} />,
|
||||
activeIcon: <RiBrain2Fill className={iconClassName} />,
|
||||
icon: <span className={cn('i-ri-brain-2-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-brain-2-fill', iconClassName)} />,
|
||||
},
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.MEMBERS,
|
||||
name: t('settings.members', { ns: 'common' }),
|
||||
icon: <RiGroup2Line className={iconClassName} />,
|
||||
activeIcon: <RiGroup2Fill className={iconClassName} />,
|
||||
icon: <span className={cn('i-ri-group-2-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-group-2-fill', iconClassName)} />,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -92,8 +76,8 @@ export default function AccountSetting({
|
||||
key: ACCOUNT_SETTING_TAB.BILLING,
|
||||
name: t('settings.billing', { ns: 'common' }),
|
||||
description: t('plansCommon.receiptInfo', { ns: 'billing' }),
|
||||
icon: <RiMoneyDollarCircleLine className={iconClassName} />,
|
||||
activeIcon: <RiMoneyDollarCircleFill className={iconClassName} />,
|
||||
icon: <span className={cn('i-ri-money-dollar-circle-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-money-dollar-circle-fill', iconClassName)} />,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,14 +85,14 @@ export default function AccountSetting({
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
name: t('settings.dataSource', { ns: 'common' }),
|
||||
icon: <RiDatabase2Line className={iconClassName} />,
|
||||
activeIcon: <RiDatabase2Fill className={iconClassName} />,
|
||||
icon: <span className={cn('i-ri-database-2-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-database-2-fill', iconClassName)} />,
|
||||
},
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION,
|
||||
name: t('settings.apiBasedExtension', { ns: 'common' }),
|
||||
icon: <RiPuzzle2Line className={iconClassName} />,
|
||||
activeIcon: <RiPuzzle2Fill className={iconClassName} />,
|
||||
icon: <span className={cn('i-ri-puzzle-2-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-puzzle-2-fill', iconClassName)} />,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -116,8 +100,8 @@ export default function AccountSetting({
|
||||
items.push({
|
||||
key: ACCOUNT_SETTING_TAB.CUSTOM,
|
||||
name: t('custom', { ns: 'custom' }),
|
||||
icon: <RiColorFilterLine className={iconClassName} />,
|
||||
activeIcon: <RiColorFilterFill className={iconClassName} />,
|
||||
icon: <span className={cn('i-ri-color-filter-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-color-filter-fill', iconClassName)} />,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,8 +124,8 @@ export default function AccountSetting({
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.LANGUAGE,
|
||||
name: t('settings.language', { ns: 'common' }),
|
||||
icon: <RiTranslate2 className={iconClassName} />,
|
||||
activeIcon: <RiTranslate2 className={iconClassName} />,
|
||||
icon: <span className={cn('i-ri-translate-2', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-translate-2', iconClassName)} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -171,13 +155,13 @@ export default function AccountSetting({
|
||||
>
|
||||
<div className="mx-auto flex h-[100vh] max-w-[1048px]">
|
||||
<div className="flex w-[44px] flex-col border-r border-divider-burn pl-4 pr-6 sm:w-[224px]">
|
||||
<div className="title-2xl-semi-bold mb-8 mt-6 px-3 py-2 text-text-primary">{t('userProfile.settings', { ns: 'common' })}</div>
|
||||
<div className="mb-8 mt-6 px-3 py-2 text-text-primary title-2xl-semi-bold">{t('userProfile.settings', { ns: 'common' })}</div>
|
||||
<div className="w-full">
|
||||
{
|
||||
menuItems.map(menuItem => (
|
||||
<div key={menuItem.key} className="mb-2">
|
||||
{!isCurrentWorkspaceDatasetOperator && (
|
||||
<div className="system-xs-medium-uppercase mb-0.5 py-2 pb-1 pl-3 text-text-tertiary">{menuItem.name}</div>
|
||||
<div className="mb-0.5 py-2 pb-1 pl-3 text-text-tertiary system-xs-medium-uppercase">{menuItem.name}</div>
|
||||
)}
|
||||
<div>
|
||||
{
|
||||
@@ -186,7 +170,7 @@ export default function AccountSetting({
|
||||
key={item.key}
|
||||
className={cn(
|
||||
'mb-0.5 flex h-[37px] cursor-pointer items-center rounded-lg p-1 pl-3 text-sm',
|
||||
activeMenu === item.key ? 'system-sm-semibold bg-state-base-active text-components-menu-item-text-active' : 'system-sm-medium text-components-menu-item-text',
|
||||
activeMenu === item.key ? 'bg-state-base-active text-components-menu-item-text-active system-sm-semibold' : 'text-components-menu-item-text system-sm-medium',
|
||||
)}
|
||||
title={item.name}
|
||||
onClick={() => {
|
||||
@@ -213,25 +197,23 @@ export default function AccountSetting({
|
||||
className="px-2"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
<span className="i-ri-close-line h-5 w-5" />
|
||||
</Button>
|
||||
<div className="system-2xs-medium-uppercase mt-1 text-text-tertiary">ESC</div>
|
||||
<div className="mt-1 text-text-tertiary system-2xs-medium-uppercase">ESC</div>
|
||||
</div>
|
||||
<div ref={scrollRef} className="w-full overflow-y-auto bg-components-panel-bg pb-4">
|
||||
<div className={cn('sticky top-0 z-20 mx-8 mb-[18px] flex items-center bg-components-panel-bg pb-2 pt-[27px]', scrolled && 'border-b border-divider-regular')}>
|
||||
<div className="title-2xl-semi-bold shrink-0 text-text-primary">
|
||||
<div className="shrink-0 text-text-primary title-2xl-semi-bold">
|
||||
{activeItem?.name}
|
||||
{activeItem?.description && (
|
||||
<div className="system-sm-regular mt-1 text-text-tertiary">{activeItem?.description}</div>
|
||||
<div className="mt-1 text-text-tertiary system-sm-regular">{activeItem?.description}</div>
|
||||
)}
|
||||
</div>
|
||||
{activeItem?.key === 'provider' && (
|
||||
<div className="flex grow justify-end">
|
||||
<Input
|
||||
showLeftIcon
|
||||
wrapperClassName="!w-[200px]"
|
||||
className="!h-8 !text-[13px]"
|
||||
onChange={e => setSearchValue(e.target.value)}
|
||||
<SearchInput
|
||||
className="w-[200px]"
|
||||
onChange={setSearchValue}
|
||||
value={searchValue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2052,9 +2052,6 @@
|
||||
"app/components/base/input/index.tsx": {
|
||||
"react-refresh/only-export-components": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/base/linked-apps-panel/index.tsx": {
|
||||
@@ -3994,9 +3991,6 @@
|
||||
"app/components/header/account-setting/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"app/components/header/account-setting/key-validator/declarations.ts": {
|
||||
|
||||
Reference in New Issue
Block a user