mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
fix(web): align workspace switcher states and semantics (#41713)
This commit is contained in:
parent
8f7cca3ae2
commit
642ad2fffe
@ -194,6 +194,27 @@ describe('WorkspaceCard', () => {
|
||||
expect(screen.getByRole('button', { name: workspaceMenuAccessibleName })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps full workspace names on the final interactive title owner', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspaceCard()
|
||||
|
||||
const trigger = screen.getByRole('button', { name: workspaceMenuAccessibleName })
|
||||
expect(trigger).toHaveAttribute('title', 'Solar Studio')
|
||||
expect(within(trigger).getByText('Solar Studio')).not.toHaveAttribute('title')
|
||||
|
||||
await user.click(trigger)
|
||||
|
||||
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
|
||||
const workspaceItem = within(panel).getByRole('button', {
|
||||
name: 'Evan Workspace',
|
||||
})
|
||||
expect(workspaceItem).toHaveAttribute('title', 'Evan Workspace')
|
||||
expect(within(workspaceItem).getByText('Evan Workspace')).not.toHaveAttribute('title')
|
||||
expect(
|
||||
within(panel).getByRole('button', { name: 'common.mainNav.workspace.settings' }),
|
||||
).toHaveAttribute('title', 'common.mainNav.workspace.settings')
|
||||
})
|
||||
|
||||
it('includes the visible workspace plan in the menu trigger accessible name', () => {
|
||||
renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } })
|
||||
|
||||
@ -308,8 +329,12 @@ describe('WorkspaceCard', () => {
|
||||
expect(
|
||||
within(panel).getByRole('button', { name: 'common.mainNav.workspace.sort.openMenu' }),
|
||||
).toBeDisabled()
|
||||
expect(within(panel).getByRole('button', { name: 'common.operation.search' })).toBeDisabled()
|
||||
expect(within(panel).getByRole('button', { name: 'common.operation.search' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(panel.querySelector('[aria-busy="true"]')).toBeInTheDocument()
|
||||
expect(within(panel).getByRole('status', { name: 'common.loading' })).toBeInTheDocument()
|
||||
expect(within(panel).queryByRole('button', { name: 'Evan Workspace' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -390,32 +415,79 @@ describe('WorkspaceCard', () => {
|
||||
).toBeInTheDocument()
|
||||
const workspaceItem = within(panel).getByRole('button', { name: 'Evan Workspace' })
|
||||
expect(workspaceItem).toBeInTheDocument()
|
||||
expect(workspaceItem.parentElement).toHaveClass('max-h-[240px]', 'overflow-y-auto')
|
||||
const workspaceList = within(panel).getByRole('list', {
|
||||
name: 'common.userProfile.workspace',
|
||||
})
|
||||
expect(within(workspaceList).getAllByRole('listitem')).toHaveLength(2)
|
||||
expect(workspaceList.parentElement).toHaveClass('max-h-[240px]', 'overflow-y-auto')
|
||||
})
|
||||
|
||||
it('filters workspace switcher options from the search action', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspaceCard()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.search' }))
|
||||
await user.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
|
||||
const searchTrigger = await screen.findByRole('button', { name: 'common.operation.search' })
|
||||
expect(searchTrigger).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
await user.click(searchTrigger)
|
||||
|
||||
expect(screen.getByText('common.userProfile.workspace')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'common.mainNav.workspace.sort.openMenu' }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.search' })).toHaveClass(
|
||||
'bg-state-base-hover',
|
||||
)
|
||||
expect(searchTrigger).toHaveAttribute('aria-expanded', 'true')
|
||||
const controlledPanelId = searchTrigger.getAttribute('aria-controls')
|
||||
expect(controlledPanelId).toBeTruthy()
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('common.mainNav.workspace.searchPlaceholder'), {
|
||||
target: { value: 'evan' },
|
||||
})
|
||||
const searchInput = screen.getByPlaceholderText('common.mainNav.workspace.searchPlaceholder')
|
||||
expect(document.getElementById(controlledPanelId!)).toContainElement(searchInput)
|
||||
expect(searchInput).toHaveFocus()
|
||||
await user.type(searchInput, 'evan')
|
||||
|
||||
const panel = screen.getByRole('dialog', { name: 'Solar Studio' })
|
||||
expect(within(panel).getByRole('button', { name: 'Evan Workspace' })).toBeInTheDocument()
|
||||
expect(within(panel).queryByRole('button', { name: 'Solar Studio' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('announces an empty workspace search result', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspaceCard()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
|
||||
await user.click(await screen.findByRole('button', { name: 'common.operation.search' }))
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('common.mainNav.workspace.searchPlaceholder'),
|
||||
'missing',
|
||||
)
|
||||
|
||||
const panel = screen.getByRole('dialog', { name: 'Solar Studio' })
|
||||
expect(within(panel).getByRole('status')).toHaveTextContent(
|
||||
'common.mainNav.workspace.noResults',
|
||||
)
|
||||
expect(
|
||||
within(panel).queryByRole('list', { name: 'common.userProfile.workspace' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('exposes only the current workspace as current', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspaceCard()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
|
||||
|
||||
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
|
||||
const workspaceList = within(panel).getByRole('list', {
|
||||
name: 'common.userProfile.workspace',
|
||||
})
|
||||
expect(
|
||||
within(workspaceList).getByRole('button', { name: 'Solar Studio', current: true }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
within(workspaceList).getByRole('button', { name: 'Evan Workspace' }),
|
||||
).not.toHaveAttribute('aria-current')
|
||||
})
|
||||
|
||||
it('sorts workspaces by last opened and can sort by created time', async () => {
|
||||
mockWorkspaces = [
|
||||
{
|
||||
@ -451,7 +523,10 @@ describe('WorkspaceCard', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
|
||||
|
||||
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
|
||||
const defaultWorkspaceOptions = within(panel)
|
||||
const workspaceList = within(panel).getByRole('list', {
|
||||
name: 'common.userProfile.workspace',
|
||||
})
|
||||
const defaultWorkspaceOptions = within(workspaceList)
|
||||
.getAllByRole('button')
|
||||
.map((item) => item.getAttribute('title'))
|
||||
.filter(Boolean)
|
||||
@ -476,7 +551,7 @@ describe('WorkspaceCard', () => {
|
||||
screen.getByRole('menuitemradio', { name: 'common.mainNav.workspace.sort.createdTime' }),
|
||||
)
|
||||
|
||||
const createdTimeWorkspaceOptions = within(panel)
|
||||
const createdTimeWorkspaceOptions = within(workspaceList)
|
||||
.getAllByRole('button')
|
||||
.map((item) => item.getAttribute('title'))
|
||||
.filter(Boolean)
|
||||
@ -488,6 +563,39 @@ describe('WorkspaceCard', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('closes the nested sort menu before the workspace popover on Escape', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspaceCard()
|
||||
|
||||
const workspaceTrigger = screen.getByRole('button', { name: workspaceMenuAccessibleName })
|
||||
await user.click(workspaceTrigger)
|
||||
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
|
||||
const sortTrigger = within(panel).getByRole('button', {
|
||||
name: 'common.mainNav.workspace.sort.openMenu',
|
||||
})
|
||||
await user.click(sortTrigger)
|
||||
expect(
|
||||
await screen.findByRole('menuitemradio', {
|
||||
name: 'common.mainNav.workspace.sort.lastOpened',
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
expect(
|
||||
screen.queryByRole('menuitemradio', {
|
||||
name: 'common.mainNav.workspace.sort.lastOpened',
|
||||
}),
|
||||
).not.toBeInTheDocument()
|
||||
expect(panel).toBeInTheDocument()
|
||||
expect(sortTrigger).toHaveFocus()
|
||||
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
await waitFor(() => expect(panel).not.toBeInTheDocument())
|
||||
expect(workspaceTrigger).toHaveFocus()
|
||||
})
|
||||
|
||||
it('opens account settings from workspace menu actions', async () => {
|
||||
renderWorkspaceCard()
|
||||
|
||||
@ -527,6 +635,20 @@ describe('WorkspaceCard', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('closes the popover without switching when the current workspace is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspaceCard()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
|
||||
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
|
||||
await user.click(within(panel).getByRole('button', { name: 'Solar Studio', current: true }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: 'Solar Studio' })).not.toBeInTheDocument(),
|
||||
)
|
||||
expect(mockSwitchWorkspace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps workspace settings visible for dataset operators without member management permission', async () => {
|
||||
mockCurrentWorkspaceQuery({
|
||||
...currentWorkspaceValue,
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import type { TenantListItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import { page, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { WorkspaceSwitcher } from '../workspace-switcher'
|
||||
|
||||
const workspaces: TenantListItemResponse[] = [
|
||||
{
|
||||
id: 'workspace-current',
|
||||
name: 'Solar Studio',
|
||||
plan: 'sandbox',
|
||||
status: 'normal',
|
||||
created_at: 2,
|
||||
current: true,
|
||||
},
|
||||
{
|
||||
id: 'workspace-other',
|
||||
name: 'Solar Studio',
|
||||
plan: 'team',
|
||||
status: 'normal',
|
||||
created_at: 1,
|
||||
current: false,
|
||||
},
|
||||
]
|
||||
|
||||
describe('WorkspaceSwitcher visual states', () => {
|
||||
it('keeps current, hover, and focus-visible as independent states', async () => {
|
||||
// Chromium owns the rendered hover/focus-visible output that happy-dom cannot reproduce.
|
||||
const screen = await render(
|
||||
<WorkspaceSwitcher workspaces={workspaces} isPending={false} onSwitchWorkspace={vi.fn()} />,
|
||||
)
|
||||
const workspaceList = screen.getByRole('list', { name: 'common.userProfile.workspace' })
|
||||
const workspaceItems = workspaceList.getByRole('listitem')
|
||||
const current = workspaceItems.nth(0).getByRole('button')
|
||||
const other = workspaceItems.nth(1).getByRole('button')
|
||||
const captureList = () => page.screenshot({ element: workspaceList, save: false })
|
||||
|
||||
const currentResting = await page.screenshot({ element: current, save: false })
|
||||
const otherResting = await page.screenshot({ element: other, save: false })
|
||||
expect(currentResting).not.toBe(otherResting)
|
||||
|
||||
const resting = await captureList()
|
||||
|
||||
await other.hover()
|
||||
const hover = await captureList()
|
||||
expect(hover).not.toBe(resting)
|
||||
|
||||
await userEvent.unhover(other)
|
||||
await current.hover()
|
||||
const currentHover = await captureList()
|
||||
expect(currentHover).not.toBe(resting)
|
||||
expect(currentHover).not.toBe(hover)
|
||||
|
||||
await userEvent.unhover(current)
|
||||
await userEvent.tab()
|
||||
await userEvent.tab()
|
||||
await userEvent.tab()
|
||||
expect(current.element()).toHaveFocus()
|
||||
const focusVisible = await captureList()
|
||||
expect(focusVisible).not.toBe(resting)
|
||||
expect(focusVisible).not.toBe(currentHover)
|
||||
})
|
||||
})
|
||||
@ -126,10 +126,7 @@ function WorkspaceCardTrigger({
|
||||
</span>
|
||||
<div className="min-w-0 grow">
|
||||
<div className="flex min-w-0 items-center gap-1 pr-0.5">
|
||||
<span
|
||||
className="max-w-30 min-w-0 shrink truncate system-sm-medium text-text-primary"
|
||||
title={name}
|
||||
>
|
||||
<span className="max-w-30 min-w-0 shrink truncate system-sm-medium text-text-primary">
|
||||
{name}
|
||||
</span>
|
||||
{showStatus && <span className="flex shrink-0 items-center">{status}</span>}
|
||||
@ -183,8 +180,8 @@ function WorkspaceMenuHeader({
|
||||
name: string
|
||||
status: ReactNode
|
||||
showInviteMembers: boolean
|
||||
settingsLabel: ReactNode
|
||||
inviteMembersLabel: ReactNode
|
||||
settingsLabel: string
|
||||
inviteMembersLabel: string
|
||||
onOpenSettings: () => void
|
||||
onInviteMembers: () => void
|
||||
}) {
|
||||
@ -205,6 +202,7 @@ function WorkspaceMenuHeader({
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
title={settingsLabel}
|
||||
className="flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
|
||||
onClick={onOpenSettings}
|
||||
>
|
||||
@ -218,6 +216,7 @@ function WorkspaceMenuHeader({
|
||||
{showInviteMembers && (
|
||||
<button
|
||||
type="button"
|
||||
title={inviteMembersLabel}
|
||||
className="flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
|
||||
onClick={onInviteMembers}
|
||||
>
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
|
||||
export function WorkspaceMenuItemContent({
|
||||
icon,
|
||||
label,
|
||||
trailing,
|
||||
trailingClassName,
|
||||
}: {
|
||||
icon: ReactNode
|
||||
label: ReactNode
|
||||
trailing?: ReactNode
|
||||
trailingClassName?: string
|
||||
}) {
|
||||
const labelTitle = typeof label === 'string' ? label : undefined
|
||||
const showTrailing = trailing !== undefined && trailing !== null
|
||||
|
||||
return (
|
||||
@ -20,14 +22,15 @@ export function WorkspaceMenuItemContent({
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-left system-md-regular text-text-secondary"
|
||||
title={labelTitle}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate text-left system-md-regular text-text-secondary">
|
||||
{label}
|
||||
</span>
|
||||
{showTrailing && (
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center">{trailing}</span>
|
||||
<span
|
||||
className={cn('flex h-4 w-4 shrink-0 items-center justify-center', trailingClassName)}
|
||||
>
|
||||
{trailing}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { TenantListItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -10,14 +11,13 @@ import {
|
||||
DropdownMenuRadioItemIndicator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { useId, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SearchInput } from '@/app/components/base/search-input'
|
||||
import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar'
|
||||
import { WorkspaceMenuItemContent } from './workspace-menu-content'
|
||||
|
||||
const workspaceSwitchActionButtonClassName =
|
||||
'flex shrink-0 items-center justify-center rounded-md p-0.5 text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled'
|
||||
const workspaceSwitchActionIconWrapClassName = 'flex size-5 shrink-0 items-center justify-center'
|
||||
const workspaceSwitchActionIconClassName = 'size-3.5 shrink-0'
|
||||
const workspaceSwitchListClassName = 'max-h-[240px] overflow-y-auto overscroll-contain scroll-py-1'
|
||||
@ -31,19 +31,20 @@ const getWorkspaceLastOpenedAt = (workspace: TenantListItemResponse) =>
|
||||
|
||||
function WorkspaceSwitchControls({
|
||||
disabled,
|
||||
labelId,
|
||||
searchText,
|
||||
sort,
|
||||
onSearchTextChange,
|
||||
onSortChange,
|
||||
}: {
|
||||
disabled: boolean
|
||||
labelId: string
|
||||
searchText: string
|
||||
sort: WorkspaceSort
|
||||
onSearchTextChange: (value: string) => void
|
||||
onSortChange: (value: WorkspaceSort) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [searchVisible, setSearchVisible] = useState(false)
|
||||
const [sortMenuOpen, setSortMenuOpen] = useState(false)
|
||||
const sortMenuLabel = t(($) => $[workspaceSwitchI18nKey('mainNav.workspace.sort.openMenu')], {
|
||||
ns: 'common',
|
||||
@ -64,26 +65,28 @@ function WorkspaceSwitchControls({
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapsible>
|
||||
<div className="flex items-start gap-0.5 py-1 pr-2 pl-3">
|
||||
<div className="flex min-w-0 flex-1 items-center justify-center py-1">
|
||||
<span className="min-w-0 flex-1 truncate system-xs-medium-uppercase text-text-tertiary">
|
||||
<span
|
||||
id={labelId}
|
||||
className="min-w-0 flex-1 truncate system-xs-medium-uppercase text-text-tertiary"
|
||||
>
|
||||
{t(($) => $['userProfile.workspace'], { ns: 'common' })}
|
||||
</span>
|
||||
</div>
|
||||
<DropdownMenu open={sortMenuOpen} onOpenChange={setSortMenuOpen}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={sortMenuLabel}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
workspaceSwitchActionButtonClassName,
|
||||
'data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
|
||||
<span className={cn('i-ri-sort-desc', workspaceSwitchActionIconClassName)} />
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
className="data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary"
|
||||
render={
|
||||
<IconButton aria-label={sortMenuLabel}>
|
||||
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
|
||||
<span className={cn('i-ri-sort-desc', workspaceSwitchActionIconClassName)} />
|
||||
</span>
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
@ -113,22 +116,19 @@ function WorkspaceSwitchControls({
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(($) => $['operation.search'], { ns: 'common' })}
|
||||
<CollapsibleTrigger
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
workspaceSwitchActionButtonClassName,
|
||||
searchVisible && 'bg-state-base-hover text-text-secondary',
|
||||
)}
|
||||
onClick={() => setSearchVisible((visible) => !visible)}
|
||||
>
|
||||
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
|
||||
<span className={cn('i-ri-search-line', workspaceSwitchActionIconClassName)} />
|
||||
</span>
|
||||
</button>
|
||||
className="size-6 min-h-0 w-6 justify-center gap-0 rounded-md p-0.5 data-panel-open:bg-state-base-hover data-panel-open:text-text-secondary"
|
||||
render={
|
||||
<IconButton aria-label={t(($) => $['operation.search'], { ns: 'common' })}>
|
||||
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
|
||||
<span className={cn('i-ri-search-line', workspaceSwitchActionIconClassName)} />
|
||||
</span>
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{searchVisible && (
|
||||
<CollapsiblePanel>
|
||||
<div className="px-2 pb-2">
|
||||
<SearchInput
|
||||
value={searchText}
|
||||
@ -140,8 +140,8 @@ function WorkspaceSwitchControls({
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</CollapsiblePanel>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
@ -156,6 +156,8 @@ export function WorkspaceSwitcher({
|
||||
isPending,
|
||||
onSwitchWorkspace,
|
||||
}: WorkspaceSwitcherProps) {
|
||||
const { t } = useTranslation()
|
||||
const workspaceListLabelId = useId()
|
||||
const [workspaceSearchText, setWorkspaceSearchText] = useState('')
|
||||
const [workspaceSort, setWorkspaceSort] = useState<WorkspaceSort>('lastOpened')
|
||||
const displayedWorkspaces = useMemo(() => {
|
||||
@ -183,6 +185,7 @@ export function WorkspaceSwitcher({
|
||||
<div className="p-1 pb-2">
|
||||
<WorkspaceSwitchControls
|
||||
disabled={isPending}
|
||||
labelId={workspaceListLabelId}
|
||||
searchText={workspaceSearchText}
|
||||
sort={workspaceSort}
|
||||
onSearchTextChange={setWorkspaceSearchText}
|
||||
@ -190,39 +193,54 @@ export function WorkspaceSwitcher({
|
||||
/>
|
||||
<div aria-busy={isPending} className={workspaceSwitchListClassName}>
|
||||
{isPending ? (
|
||||
<div aria-hidden className="flex h-8 items-center justify-center">
|
||||
<span className="i-ri-loader-2-line size-4 animate-spin text-text-tertiary motion-reduce:animate-none" />
|
||||
<div
|
||||
role="status"
|
||||
aria-label={t(($) => $.loading, { ns: 'common' })}
|
||||
className="flex h-8 items-center justify-center"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-ri-loader-2-line size-4 animate-spin text-text-tertiary motion-reduce:animate-none"
|
||||
/>
|
||||
</div>
|
||||
) : displayedWorkspaces.length === 0 ? (
|
||||
<div
|
||||
role="status"
|
||||
className="flex h-8 items-center px-3 system-xs-regular text-text-tertiary"
|
||||
>
|
||||
{t(($) => $[workspaceSwitchI18nKey('mainNav.workspace.noResults')], {
|
||||
ns: 'common',
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
displayedWorkspaces.map((workspace) => {
|
||||
const workspaceName = getWorkspaceName(workspace)
|
||||
<ul aria-labelledby={workspaceListLabelId}>
|
||||
{displayedWorkspaces.map((workspace) => {
|
||||
const workspaceName = getWorkspaceName(workspace)
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={workspace.id}
|
||||
aria-current={workspace.current ? 'true' : undefined}
|
||||
title={workspaceName}
|
||||
className={cn(
|
||||
'flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
|
||||
workspace.current && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
onSwitchWorkspace(workspace.id)
|
||||
}}
|
||||
>
|
||||
<WorkspaceMenuItemContent
|
||||
icon={<WorkspaceAvatar name={workspaceName} size="xs" />}
|
||||
label={workspaceName}
|
||||
trailing={
|
||||
workspace.current ? (
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<li key={workspace.id}>
|
||||
<button
|
||||
type="button"
|
||||
aria-current={workspace.current ? 'true' : undefined}
|
||||
title={workspaceName}
|
||||
className="group flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
|
||||
onClick={() => {
|
||||
onSwitchWorkspace(workspace.id)
|
||||
}}
|
||||
>
|
||||
<WorkspaceMenuItemContent
|
||||
icon={<WorkspaceAvatar name={workspaceName} size="xs" />}
|
||||
label={workspaceName}
|
||||
trailingClassName="hidden group-aria-current:flex"
|
||||
trailing={
|
||||
<span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" />
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} رصيد",
|
||||
"mainNav.workspace.creditsUnit": "أرصدة",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "لم يتم العثور على مساحات عمل",
|
||||
"mainNav.workspace.openMenu": "فتح قائمة مساحة العمل",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} Credits",
|
||||
"mainNav.workspace.creditsUnit": "Credits",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Keine Arbeitsbereiche gefunden",
|
||||
"mainNav.workspace.openMenu": "Workspace-Menü öffnen",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} credits",
|
||||
"mainNav.workspace.creditsUnit": "credits",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "No workspaces found",
|
||||
"mainNav.workspace.openMenu": "Open workspace menu",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} créditos",
|
||||
"mainNav.workspace.creditsUnit": "créditos",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "No se encontraron espacios de trabajo",
|
||||
"mainNav.workspace.openMenu": "Abrir menú del espacio de trabajo",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} اعتبار",
|
||||
"mainNav.workspace.creditsUnit": "اعتبار",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "هیچ فضای کاری یافت نشد",
|
||||
"mainNav.workspace.openMenu": "باز کردن منوی فضای کاری",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} crédits",
|
||||
"mainNav.workspace.creditsUnit": "crédits",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Aucun espace de travail trouvé",
|
||||
"mainNav.workspace.openMenu": "Ouvrir le menu de l’espace de travail",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} क्रेडिट",
|
||||
"mainNav.workspace.creditsUnit": "क्रेडिट",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "कोई वर्कस्पेस नहीं मिला",
|
||||
"mainNav.workspace.openMenu": "वर्कस्पेस मेनू खोलें",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} kredit",
|
||||
"mainNav.workspace.creditsUnit": "kredit",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Tidak ada workspace yang ditemukan",
|
||||
"mainNav.workspace.openMenu": "Buka menu workspace",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} crediti",
|
||||
"mainNav.workspace.creditsUnit": "crediti",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Nessun workspace trovato",
|
||||
"mainNav.workspace.openMenu": "Apri menu workspace",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} クレジット",
|
||||
"mainNav.workspace.creditsUnit": "クレジット",
|
||||
"mainNav.workspace.inviteMembers": "メンバーを招待",
|
||||
"mainNav.workspace.noResults": "ワークスペースが見つかりません",
|
||||
"mainNav.workspace.openMenu": "ワークスペースメニューを開く",
|
||||
"mainNav.workspace.searchPlaceholder": "ワークスペースを検索",
|
||||
"mainNav.workspace.settings": "ワークスペース設定",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} 크레딧",
|
||||
"mainNav.workspace.creditsUnit": "크레딧",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "워크스페이스를 찾을 수 없습니다",
|
||||
"mainNav.workspace.openMenu": "워크스페이스 메뉴 열기",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} ເຄຣດິດ",
|
||||
"mainNav.workspace.creditsUnit": "ເຄຣດິດ",
|
||||
"mainNav.workspace.inviteMembers": "ເຊີນສະມາຊິກ",
|
||||
"mainNav.workspace.noResults": "ບໍ່ພົບພື້ນທີ່ເຮັດວຽກ",
|
||||
"mainNav.workspace.openMenu": "ເປີດເມນູພື້ນທີ່ເຮັດວຽກ",
|
||||
"mainNav.workspace.searchPlaceholder": "ຄົ້ນຫາພື້ນທີ່ເຮັດວຽກ",
|
||||
"mainNav.workspace.settings": "ການຕັ້ງຄ່າພື້ນທີ່ເຮັດວຽກ",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} credits",
|
||||
"mainNav.workspace.creditsUnit": "credits",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Geen werkruimtes gevonden",
|
||||
"mainNav.workspace.openMenu": "Werkruimtemenu openen",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} kredytów",
|
||||
"mainNav.workspace.creditsUnit": "kredyty",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Nie znaleziono obszarów roboczych",
|
||||
"mainNav.workspace.openMenu": "Otwórz menu obszaru roboczego",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} créditos",
|
||||
"mainNav.workspace.creditsUnit": "créditos",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Nenhum workspace encontrado",
|
||||
"mainNav.workspace.openMenu": "Abrir menu do workspace",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} credite",
|
||||
"mainNav.workspace.creditsUnit": "credite",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Nu s-au găsit spații de lucru",
|
||||
"mainNav.workspace.openMenu": "Deschide meniul spațiului de lucru",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} кредитов",
|
||||
"mainNav.workspace.creditsUnit": "кредиты",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Рабочие пространства не найдены",
|
||||
"mainNav.workspace.openMenu": "Открыть меню рабочей области",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} kreditov",
|
||||
"mainNav.workspace.creditsUnit": "krediti",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Ni najdenih delovnih prostorov",
|
||||
"mainNav.workspace.openMenu": "Odpri meni delovnega prostora",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} เครดิต",
|
||||
"mainNav.workspace.creditsUnit": "เครดิต",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "ไม่พบพื้นที่ทำงาน",
|
||||
"mainNav.workspace.openMenu": "เปิดเมนูพื้นที่ทำงาน",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} kredi",
|
||||
"mainNav.workspace.creditsUnit": "kredi",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Çalışma alanı bulunamadı",
|
||||
"mainNav.workspace.openMenu": "Çalışma alanı menüsünü aç",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} кредитів",
|
||||
"mainNav.workspace.creditsUnit": "кредити",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Робочі простори не знайдено",
|
||||
"mainNav.workspace.openMenu": "Відкрити меню робочого простору",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} tín dụng",
|
||||
"mainNav.workspace.creditsUnit": "tín dụng",
|
||||
"mainNav.workspace.inviteMembers": "Invite members",
|
||||
"mainNav.workspace.noResults": "Không tìm thấy không gian làm việc",
|
||||
"mainNav.workspace.openMenu": "Mở menu workspace",
|
||||
"mainNav.workspace.searchPlaceholder": "Search workspace",
|
||||
"mainNav.workspace.settings": "Workspace settings",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} 消息额度",
|
||||
"mainNav.workspace.creditsUnit": "消息额度",
|
||||
"mainNav.workspace.inviteMembers": "邀请成员",
|
||||
"mainNav.workspace.noResults": "未找到工作空间",
|
||||
"mainNav.workspace.openMenu": "打开工作空间菜单",
|
||||
"mainNav.workspace.searchPlaceholder": "搜索工作空间",
|
||||
"mainNav.workspace.settings": "工作空间设置",
|
||||
|
||||
@ -211,6 +211,7 @@
|
||||
"mainNav.workspace.credits": "{{count}} 點額度",
|
||||
"mainNav.workspace.creditsUnit": "點額度",
|
||||
"mainNav.workspace.inviteMembers": "邀請成員",
|
||||
"mainNav.workspace.noResults": "找不到工作區",
|
||||
"mainNav.workspace.openMenu": "開啟工作區選單",
|
||||
"mainNav.workspace.searchPlaceholder": "搜尋工作區",
|
||||
"mainNav.workspace.settings": "工作區設定",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user