Compare commits
3 Commits
test/real-
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
987a0e70a6 | ||
|
|
60ed66607e | ||
|
|
4929a67885 |
@@ -2821,17 +2821,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx_a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/declarations.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 11
|
||||
|
||||
@@ -12,6 +12,16 @@ const mockAccounts = [
|
||||
{ id: '3', name: 'Bob Wilson', email: 'bob@example.com', avatar_url: '' },
|
||||
]
|
||||
|
||||
const getTrigger = () =>
|
||||
screen.getByRole('button', {
|
||||
name: 'common.members.transferModal.transferPlaceholder',
|
||||
})
|
||||
|
||||
const getMemberButtons = () =>
|
||||
screen.getAllByRole('button', {
|
||||
name: /@example\.com/,
|
||||
})
|
||||
|
||||
describe('MemberSelector', () => {
|
||||
const mockOnSelect = vi.fn()
|
||||
|
||||
@@ -24,22 +34,25 @@ describe('MemberSelector', () => {
|
||||
|
||||
it('should render placeholder when no value is selected', () => {
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
expect(screen.getByText(/members\.transferModal\.transferPlaceholder/i)).toBeInTheDocument()
|
||||
expect(getTrigger()).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
it('should render selected member info', () => {
|
||||
render(<MemberSelector value="1" onSelect={mockOnSelect} />)
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
||||
expect(screen.getByText('john@example.com')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /John Doe.*john@example\.com/,
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open dropdown and show filtered list on click', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MemberSelector onSelect={mockOnSelect} exclude={['1']} />)
|
||||
|
||||
await user.click(screen.getByTestId('member-selector-trigger'))
|
||||
await user.click(getTrigger())
|
||||
|
||||
const items = screen.getAllByTestId('member-selector-item')
|
||||
const items = getMemberButtons()
|
||||
expect(items).toHaveLength(2) // Jane and Bob (John excluded)
|
||||
expect(screen.queryByText('John Doe')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
|
||||
@@ -49,10 +62,14 @@ describe('MemberSelector', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
await user.click(screen.getByTestId('member-selector-trigger'))
|
||||
await user.type(screen.getByRole('textbox', { name: 'common.operation.search' }), 'Jane')
|
||||
await user.click(getTrigger())
|
||||
const searchInput = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||
expect(searchInput).toHaveAttribute('name', 'query')
|
||||
expect(searchInput).toHaveAttribute('autocomplete', 'off')
|
||||
expect(searchInput).toHaveAttribute('enterkeyhint', 'search')
|
||||
await user.type(searchInput, 'Jane')
|
||||
|
||||
const items = screen.getAllByTestId('member-selector-item')
|
||||
const items = getMemberButtons()
|
||||
expect(items).toHaveLength(1)
|
||||
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Bob Wilson')).not.toBeInTheDocument()
|
||||
@@ -62,13 +79,13 @@ describe('MemberSelector', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
await user.click(screen.getByTestId('member-selector-trigger'))
|
||||
await user.click(screen.getByText('Jane Smith'))
|
||||
await user.click(getTrigger())
|
||||
await user.click(screen.getByRole('button', { name: /Jane Smith.*jane@example\.com/ }))
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('2')
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('textbox', { name: 'common.operation.search' }),
|
||||
screen.queryByRole('searchbox', { name: 'common.operation.search' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -77,10 +94,10 @@ describe('MemberSelector', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
await user.click(screen.getByTestId('member-selector-trigger'))
|
||||
await user.type(screen.getByRole('textbox', { name: 'common.operation.search' }), 'john@')
|
||||
await user.click(getTrigger())
|
||||
await user.type(screen.getByRole('searchbox', { name: 'common.operation.search' }), 'john@')
|
||||
|
||||
const items = screen.getAllByTestId('member-selector-item')
|
||||
const items = getMemberButtons()
|
||||
expect(items).toHaveLength(1)
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument()
|
||||
@@ -89,7 +106,7 @@ describe('MemberSelector', () => {
|
||||
it('should show placeholder when value does not match any account', () => {
|
||||
render(<MemberSelector value="nonexistent-id" onSelect={mockOnSelect} />)
|
||||
|
||||
expect(screen.getByText(/members\.transferModal\.transferPlaceholder/i)).toBeInTheDocument()
|
||||
expect(getTrigger()).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle missing data gracefully', () => {
|
||||
@@ -97,7 +114,7 @@ describe('MemberSelector', () => {
|
||||
typeof useMembers
|
||||
>)
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
expect(screen.getByText(/members\.transferModal\.transferPlaceholder/i)).toBeInTheDocument()
|
||||
expect(getTrigger()).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should filter by email when account name is empty', async () => {
|
||||
@@ -112,34 +129,35 @@ describe('MemberSelector', () => {
|
||||
} as unknown as ReturnType<typeof useMembers>)
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
await user.click(screen.getByTestId('member-selector-trigger'))
|
||||
await user.type(screen.getByRole('textbox', { name: 'common.operation.search' }), 'noname@')
|
||||
await user.click(getTrigger())
|
||||
await user.type(screen.getByRole('searchbox', { name: 'common.operation.search' }), 'noname@')
|
||||
|
||||
const items = screen.getAllByTestId('member-selector-item')
|
||||
const items = getMemberButtons()
|
||||
expect(items).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should apply hover background class when dropdown is open', async () => {
|
||||
it('should expose the expanded state while the dropdown is open', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
const trigger = screen.getByTestId('member-selector-trigger')
|
||||
const trigger = getTrigger()
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
await user.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open')
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
it('should not match account when neither name nor email contains search value', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
await user.click(screen.getByTestId('member-selector-trigger'))
|
||||
await user.click(getTrigger())
|
||||
await user.type(
|
||||
screen.getByRole('textbox', { name: 'common.operation.search' }),
|
||||
screen.getByRole('searchbox', { name: 'common.operation.search' }),
|
||||
'xyz-no-match-xyz',
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('member-selector-item')).not.toBeInTheDocument()
|
||||
expect(screen.queryAllByRole('button', { name: /@example\.com/ })).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should fall back to empty string for account with undefined email when searching', async () => {
|
||||
@@ -153,10 +171,42 @@ describe('MemberSelector', () => {
|
||||
} as unknown as ReturnType<typeof useMembers>)
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
await user.click(screen.getByTestId('member-selector-trigger'))
|
||||
await user.type(screen.getByRole('textbox', { name: 'common.operation.search' }), 'john')
|
||||
await user.click(getTrigger())
|
||||
await user.type(screen.getByRole('searchbox', { name: 'common.operation.search' }), 'john')
|
||||
|
||||
const items = screen.getAllByTestId('member-selector-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(screen.getByRole('button', { name: /John/ })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['Enter', '{Enter}'],
|
||||
['Space', ' '],
|
||||
])('should open the dropdown with the %s key', async (_, key) => {
|
||||
const user = userEvent.setup()
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
const trigger = getTrigger()
|
||||
trigger.focus()
|
||||
await user.keyboard(key)
|
||||
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(getMemberButtons()).toHaveLength(3)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['Enter', '{Enter}'],
|
||||
['Space', ' '],
|
||||
])('should select a member with the %s key', async (_, key) => {
|
||||
const user = userEvent.setup()
|
||||
render(<MemberSelector onSelect={mockOnSelect} />)
|
||||
|
||||
await user.click(getTrigger())
|
||||
const jane = screen.getByRole('button', { name: /Jane Smith.*jane@example\.com/ })
|
||||
jane.focus()
|
||||
await user.keyboard(key)
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('2')
|
||||
expect(
|
||||
screen.queryByRole('searchbox', { name: 'common.operation.search' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
|
||||
type Props = Readonly<{
|
||||
@@ -14,7 +12,7 @@ type Props = Readonly<{
|
||||
exclude?: string[]
|
||||
}>
|
||||
|
||||
const MemberSelector: FC<Props> = ({ value, onSelect, exclude = [] }) => {
|
||||
function MemberSelector({ value, onSelect, exclude = [] }: Props) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
@@ -46,28 +44,33 @@ const MemberSelector: FC<Props> = ({ value, onSelect, exclude = [] }) => {
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<div
|
||||
data-testid="member-selector-trigger"
|
||||
className="group flex cursor-pointer items-center gap-1.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt data-popup-open:bg-state-base-hover-alt"
|
||||
>
|
||||
{!currentValue && (
|
||||
<div className="grow p-1 system-sm-regular text-components-input-text-placeholder">
|
||||
{t(($) => $['members.transferModal.transferPlaceholder'], { ns: 'common' })}
|
||||
</div>
|
||||
)}
|
||||
{currentValue && (
|
||||
<>
|
||||
<Avatar avatar={currentValue.avatar_url} size="sm" name={currentValue.name} />
|
||||
<div className="grow truncate system-sm-medium text-text-secondary">
|
||||
{currentValue.name}
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-quaternary">{currentValue.email}</div>
|
||||
</>
|
||||
)}
|
||||
<div className="i-ri-arrow-down-s-line size-4 text-text-quaternary group-hover:text-text-secondary group-data-popup-open:text-text-secondary" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="group flex cursor-pointer items-center gap-1.5 rounded-lg bg-components-input-bg-normal px-2 py-1 text-left outline-hidden hover:bg-state-base-hover-alt focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover-alt"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
>
|
||||
{!currentValue && (
|
||||
<span className="grow p-1 system-sm-regular text-components-input-text-placeholder">
|
||||
{t(($) => $['members.transferModal.transferPlaceholder'], { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
{currentValue && (
|
||||
<>
|
||||
<span aria-hidden>
|
||||
<Avatar avatar={currentValue.avatar_url} size="sm" name={currentValue.name} />
|
||||
</span>
|
||||
<span className="grow truncate system-sm-medium text-text-secondary">
|
||||
{currentValue.name}
|
||||
</span>
|
||||
<span className="system-xs-regular text-text-quaternary">{currentValue.email}</span>
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-ri-arrow-down-s-line size-4 text-text-quaternary group-hover:text-text-secondary group-data-popup-open:text-text-secondary"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
placement="bottom"
|
||||
sideOffset={4}
|
||||
@@ -75,30 +78,42 @@ const MemberSelector: FC<Props> = ({ value, onSelect, exclude = [] }) => {
|
||||
>
|
||||
<div className="min-w-93 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs">
|
||||
<div className="p-2 pb-1">
|
||||
<Input
|
||||
showLeftIcon
|
||||
aria-label={t(($) => $['operation.search'], { ns: 'common' })}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
/>
|
||||
<div className="relative w-full">
|
||||
<span
|
||||
aria-hidden
|
||||
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"
|
||||
/>
|
||||
<Input
|
||||
type="search"
|
||||
name="query"
|
||||
autoComplete="off"
|
||||
enterKeyHint="search"
|
||||
aria-label={t(($) => $['operation.search'], { ns: 'common' })}
|
||||
className="pl-6.5 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none"
|
||||
value={searchValue}
|
||||
onValueChange={setSearchValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
{filteredList.map((account) => (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
key={account.id}
|
||||
data-testid="member-selector-item"
|
||||
className="flex cursor-pointer items-center gap-2 rounded-lg py-1 pr-3 pl-2 hover:bg-state-base-hover"
|
||||
className="flex w-full cursor-pointer items-center gap-2 rounded-lg py-1 pr-3 pl-2 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
onClick={() => {
|
||||
onSelect(account.id)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Avatar avatar={account.avatar_url} size="sm" name={account.name} />
|
||||
<div className="grow truncate system-sm-medium text-text-secondary">
|
||||
<span aria-hidden>
|
||||
<Avatar avatar={account.avatar_url} size="sm" name={account.name} />
|
||||
</span>
|
||||
<span className="grow truncate system-sm-medium text-text-secondary">
|
||||
{account.name}
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-quaternary">{account.email}</div>
|
||||
</div>
|
||||
</span>
|
||||
<span className="system-xs-regular text-text-quaternary">{account.email}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user