fix: align workspace card plan ownership (#39616)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-07-27 13:01:02 +08:00 committed by GitHub
parent b6099d09ff
commit 28d174603f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 204 additions and 125 deletions

View File

@ -27,7 +27,6 @@ class WorkspaceService:
tenant_info: dict[str, object] = {
"id": tenant.id,
"name": tenant.name,
"plan": tenant.plan,
"status": tenant.status,
"created_at": tenant.created_at,
"trial_end_reason": None,
@ -44,6 +43,7 @@ class WorkspaceService:
tenant_info["role"] = tenant_account_join.role
feature = FeatureService.get_features(tenant.id, exclude_vector_space=True)
tenant_info["plan"] = feature.billing.subscription.plan if feature.billing.enabled else None
can_replace_logo = feature.can_replace_logo
if can_replace_logo and TenantService.has_roles(

View File

@ -24,7 +24,10 @@ class TestWorkspaceService:
patch("services.workspace_service.dify_config") as mock_dify_config,
):
# Setup default mock returns
mock_feature_service.get_features.return_value.can_replace_logo = True
feature = mock_feature_service.get_features.return_value
feature.can_replace_logo = True
feature.billing.enabled = True
feature.billing.subscription.plan = "professional"
mock_tenant_service.has_roles.return_value = True
mock_dify_config.FILES_URL = "https://example.com/files"
@ -112,7 +115,7 @@ class TestWorkspaceService:
assert result is not None
assert result["id"] == tenant.id
assert result["name"] == tenant.name
assert result["plan"] == tenant.plan
assert result["plan"] == "professional"
assert result["status"] == tenant.status
assert result["role"] == TenantAccountRole.OWNER
assert result["created_at"] == tenant.created_at
@ -159,7 +162,7 @@ class TestWorkspaceService:
assert result is not None
assert result["id"] == tenant.id
assert result["name"] == tenant.name
assert result["plan"] == tenant.plan
assert result["plan"] == "professional"
assert result["status"] == tenant.status
assert result["role"] == TenantAccountRole.OWNER
assert result["created_at"] == tenant.created_at
@ -214,7 +217,7 @@ class TestWorkspaceService:
assert result is not None
assert result["id"] == tenant.id
assert result["name"] == tenant.name
assert result["plan"] == tenant.plan
assert result["plan"] == "professional"
assert result["status"] == tenant.status
assert result["role"] == TenantAccountRole.NORMAL
assert result["created_at"] == tenant.created_at
@ -606,20 +609,23 @@ class TestWorkspaceService:
def test_get_tenant_info_should_not_include_cloud_fields_in_self_hosted(
self, db_session_with_containers: Session, mock_external_service_dependencies
):
"""next_credit_reset_date and trial_credits should NOT appear in SELF_HOSTED mode."""
"""Cloud-only billing data should not appear in SELF_HOSTED mode."""
fake = Faker()
account, tenant = self._create_test_account_and_tenant(
db_session_with_containers, mock_external_service_dependencies
)
mock_external_service_dependencies["dify_config"].DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
mock_external_service_dependencies["feature_service"].get_features.return_value.can_replace_logo = False
feature = mock_external_service_dependencies["feature_service"].get_features.return_value
feature.can_replace_logo = False
feature.billing.enabled = False
mock_external_service_dependencies["tenant_service"].has_roles.return_value = False
with patch("services.workspace_service.current_user", account):
result = WorkspaceService.get_tenant_info(tenant, db_session_with_containers)
assert result is not None
assert result["plan"] is None
assert "next_credit_reset_date" not in result
assert "trial_credits" not in result
assert "trial_credits_used" not in result

View File

@ -401,7 +401,7 @@ const consoleState: ConsoleStateFixture = {
currentWorkspace: {
id: 'workspace-1',
name: 'Solar Studio',
plan: Plan.sandbox,
plan: Plan.team,
status: 'normal',
created_at: 0,
role: 'owner',
@ -1125,16 +1125,13 @@ describe('MainNav', () => {
})
it('shows the upgrade shortcut for sandbox workspaces', () => {
mockWorkspaces = [
{
id: 'workspace-1',
name: 'Solar Studio',
mockConsoleState.current = {
...consoleState,
currentWorkspace: {
...consoleState.currentWorkspace,
plan: Plan.sandbox,
status: 'normal',
created_at: 0,
current: true,
},
]
}
renderMainNav()
@ -1143,13 +1140,13 @@ describe('MainNav', () => {
})
it('shows the view plan shortcut for paid workspaces', () => {
;(useProviderContext as Mock).mockReturnValue({
enableBilling: true,
isEducationAccount: false,
isEducationWorkspace: false,
isFetchedPlan: true,
plan: { type: Plan.team },
} as ProviderContextState)
mockConsoleState.current = {
...consoleState,
currentWorkspace: {
...consoleState.currentWorkspace,
plan: Plan.professional,
},
}
renderMainNav()

View File

@ -2,6 +2,7 @@ import type { ModalContextState } from '@/context/modal-context'
import type { ProviderContextState } from '@/context/provider-context'
import type { ICurrentWorkspace, IWorkspace } from '@/models/common'
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Plan } from '@/app/components/billing/type'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
@ -15,13 +16,17 @@ import {
} from '@/test/console/query-data'
import { WorkspaceCard } from '../workspace-card'
const { mockSwitchWorkspace, mockCurrentWorkspaceQueryKey, mockWorkspacesQueryKey } = vi.hoisted(
() => ({
mockSwitchWorkspace: vi.fn(),
mockCurrentWorkspaceQueryKey: ['console', 'workspaces', 'current', 'post'] as const,
mockWorkspacesQueryKey: ['console', 'workspaces', 'get'] as const,
}),
)
const {
mockFetchWorkspaces,
mockSwitchWorkspace,
mockCurrentWorkspaceQueryKey,
mockWorkspacesQueryKey,
} = vi.hoisted(() => ({
mockFetchWorkspaces: vi.fn(),
mockSwitchWorkspace: vi.fn(),
mockCurrentWorkspaceQueryKey: ['console', 'workspaces', 'current', 'post'] as const,
mockWorkspacesQueryKey: ['console', 'workspaces', 'get'] as const,
}))
const mockConsoleState = vi.hoisted(() => ({
current: {
workspacePermissionKeys: [] as string[],
@ -64,9 +69,10 @@ vi.mock('@/service/client', async (importOriginal) => {
},
get: {
queryKey: () => mockWorkspacesQueryKey,
queryOptions: () => ({
queryOptions: (options?: object) => ({
queryKey: mockWorkspacesQueryKey,
queryFn: () => new Promise(() => {}),
queryFn: mockFetchWorkspaces,
...options,
}),
},
switch: {
@ -163,6 +169,7 @@ describe('WorkspaceCard', () => {
current: false,
},
]
mockFetchWorkspaces.mockResolvedValue({ workspaces: mockWorkspaces })
mockSwitchWorkspace.mockReturnValue(new Promise(() => {}))
mockCurrentWorkspaceQuery()
vi.mocked(useProviderContext).mockReturnValue({
@ -210,16 +217,68 @@ describe('WorkspaceCard', () => {
expect(screen.queryByText('Evan Workspace')).not.toBeInTheDocument()
})
it('renders a skeleton while the workspaces query has no data', () => {
it('renders the current workspace before loading the workspace list', async () => {
const user = userEvent.setup()
renderWorkspaceCard({ seedWorkspaces: false })
expect(
screen.queryByRole('button', { name: 'common.mainNav.workspace.openMenu' }),
).not.toBeInTheDocument()
expect(screen.queryByText('Solar Studio')).not.toBeInTheDocument()
screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' }),
).toBeInTheDocument()
expect(screen.getByText('Solar Studio')).toBeInTheDocument()
expect(mockFetchWorkspaces).not.toHaveBeenCalled()
await user.click(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' }))
expect(await screen.findByRole('dialog', { name: 'Solar Studio' })).toBeInTheDocument()
await waitFor(() => expect(mockFetchWorkspaces).toHaveBeenCalledOnce())
expect(await screen.findByRole('button', { name: 'Evan Workspace' })).toBeInTheDocument()
})
it('uses the workspaces query current item for billing plan UI', () => {
it('prefetches the workspace list when the trigger is hovered', async () => {
const user = userEvent.setup()
renderWorkspaceCard({ seedWorkspaces: false })
const trigger = screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' })
await user.hover(trigger)
await waitFor(() => expect(mockFetchWorkspaces).toHaveBeenCalledOnce())
expect(screen.queryByRole('dialog', { name: 'Solar Studio' })).not.toBeInTheDocument()
await user.click(trigger)
expect(await screen.findByRole('button', { name: 'Evan Workspace' })).toBeInTheDocument()
expect(mockFetchWorkspaces).toHaveBeenCalledOnce()
})
it('prefetches the workspace list when the trigger receives keyboard focus', async () => {
const user = userEvent.setup()
renderWorkspaceCard({ seedWorkspaces: false })
await user.tab()
expect(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' })).toHaveFocus()
await waitFor(() => expect(mockFetchWorkspaces).toHaveBeenCalledOnce())
expect(screen.queryByRole('dialog', { name: 'Solar Studio' })).not.toBeInTheDocument()
})
it('keeps workspace controls visible and disabled while the workspace list is loading', async () => {
const user = userEvent.setup()
mockFetchWorkspaces.mockReturnValue(new Promise(() => {}))
renderWorkspaceCard({ seedWorkspaces: false })
await user.click(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' }))
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
expect(within(panel).getByText('common.userProfile.workspace')).toBeInTheDocument()
expect(
within(panel).getByRole('button', { name: 'common.mainNav.workspace.sort.openMenu' }),
).toBeDisabled()
expect(within(panel).getByRole('button', { name: 'common.operation.search' })).toBeDisabled()
expect(panel.querySelector('[aria-busy="true"]')).toBeInTheDocument()
expect(within(panel).queryByRole('button', { name: 'Evan Workspace' })).not.toBeInTheDocument()
})
it('uses the current workspace query for billing plan UI', () => {
mockCurrentWorkspaceQuery({
...currentWorkspaceValue,
plan: Plan.team,
@ -234,23 +293,17 @@ describe('WorkspaceCard', () => {
renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } })
expect(screen.getByText(Plan.sandbox)).toBeInTheDocument()
expect(screen.getByText('billing.upgradeBtn.encourageShort')).toBeInTheDocument()
expect(screen.queryByText(Plan.team)).not.toBeInTheDocument()
expect(screen.queryByText('billing.upgradeBtn.plain')).not.toBeInTheDocument()
expect(screen.getByText(Plan.team)).toBeInTheDocument()
expect(screen.getByText('billing.upgradeBtn.plain')).toBeInTheDocument()
expect(screen.queryByText(Plan.sandbox)).not.toBeInTheDocument()
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
})
it('uses the original paid plan badge for paid workspaces', () => {
mockWorkspaces = [
{
id: 'workspace-1',
name: 'Solar Studio',
plan: Plan.team,
status: 'normal',
created_at: 0,
current: true,
},
]
mockCurrentWorkspaceQuery({
...currentWorkspaceValue,
plan: Plan.team,
})
vi.mocked(useProviderContext).mockReturnValue({
enableBilling: true,
isEducationAccount: false,
@ -265,6 +318,10 @@ describe('WorkspaceCard', () => {
})
it('shows the Enterprise license status independently of the Cloud billing state', () => {
mockCurrentWorkspaceQuery({
...currentWorkspaceValue,
plan: '',
})
vi.mocked(useProviderContext).mockReturnValue({
enableBilling: true,
isEducationAccount: false,

View File

@ -1,10 +1,11 @@
'use client'
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -89,6 +90,7 @@ function WorkspaceCardTrigger({
showPlanAction,
planActionLabel,
creditsHref,
onPrefetchWorkspaces,
onPlanClick,
}: {
open: boolean
@ -99,6 +101,7 @@ function WorkspaceCardTrigger({
showPlanAction: boolean
planActionLabel: string
creditsHref: string
onPrefetchWorkspaces: () => void
onPlanClick: () => void
}) {
const { t } = useTranslation()
@ -111,6 +114,8 @@ function WorkspaceCardTrigger({
<PopoverTrigger
aria-label={t(($) => $['mainNav.workspace.openMenu'], { ns: 'common' })}
title={name}
onMouseEnter={onPrefetchWorkspaces}
onFocus={onPrefetchWorkspaces}
className={cn(
'flex w-full items-center gap-1.5 py-1.5 pr-3 pl-1.5 text-left transition-colors hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid focus-visible:outline-hidden',
showCloudBilling ? 'rounded-t-xl' : 'rounded-xl',
@ -221,21 +226,21 @@ function WorkspaceMenuHeader({
)
}
const selectCurrentWorkspaceCardData = (workspace: {
id: string
name?: string | null
role?: string | null
trial_credits?: number | null
trial_credits_used?: number | null
}) => ({
type CurrentWorkspaceCardSource = Pick<
PostWorkspacesCurrentResponse,
'id' | 'name' | 'plan' | 'trial_credits' | 'trial_credits_used'
>
const selectCurrentWorkspaceCardData = (workspace: CurrentWorkspaceCardSource) => ({
id: workspace.id,
name: workspace.name,
role: workspace.role,
plan: workspace.plan,
credits: getRemainingCredits(workspace.trial_credits ?? 0, workspace.trial_credits_used ?? 0),
})
export function WorkspaceCard() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const { data: deploymentEdition } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: ({ deployment_edition }) => deployment_edition,
@ -245,27 +250,24 @@ export function WorkspaceCard() {
select: selectCurrentWorkspaceCardData,
}),
)
const workspacesQuery = useQuery(consoleQuery.workspaces.get.queryOptions())
const [open, setOpen] = useState(false)
const workspacesQueryOptions = consoleQuery.workspaces.get.queryOptions()
const workspacesQuery = useQuery({
...workspacesQueryOptions,
enabled: open,
})
const switchWorkspaceMutation = useMutation(consoleQuery.workspaces.switch.post.mutationOptions())
const currentWorkspace = currentWorkspaceQuery.data
const workspacesData = workspacesQuery.data
const workspaces = workspacesData?.workspaces
const currentWorkspaceInList = workspaces?.find((workspace) => workspace.current)
const workspaces = workspacesQuery.data?.workspaces
const { enableBilling } = useProviderContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const showCloudBilling = deploymentEdition === 'CLOUD' && enableBilling
const [open, setOpen] = useState(false)
const prefetchWorkspaces = () => {
void queryClient.prefetchQuery(workspacesQueryOptions)
}
if (
currentWorkspaceQuery.isPending ||
workspacesQuery.isPending ||
!currentWorkspace?.name ||
!currentWorkspace.role ||
!workspaces ||
!currentWorkspaceInList ||
!isWorkspacePlan(currentWorkspaceInList.plan)
) {
if (currentWorkspaceQuery.isPending || !currentWorkspace?.name) {
return (
<WorkspaceCardSkeleton
showCloudBilling={showCloudBilling}
@ -274,9 +276,9 @@ export function WorkspaceCard() {
)
}
const workspacePlan = currentWorkspaceInList.plan
const workspacePlan = isWorkspacePlan(currentWorkspace.plan) ? currentWorkspace.plan : null
const isFreePlan = workspacePlan === Plan.sandbox
const showPlanAction = showCloudBilling
const showPlanAction = showCloudBilling && workspacePlan !== null
const planActionLabel = t(
($) => $[isFreePlan ? 'upgradeBtn.encourageShort' : 'upgradeBtn.plain'],
{ ns: 'billing' },
@ -284,7 +286,7 @@ export function WorkspaceCard() {
const showInviteMembers = hasPermission(workspacePermissionKeys, 'workspace.member.manage')
const renderWorkspaceStatus = () => {
if (deploymentEdition === 'CLOUD')
return enableBilling ? <WorkspacePlanBadge plan={workspacePlan} /> : null
return enableBilling && workspacePlan ? <WorkspacePlanBadge plan={workspacePlan} /> : null
if (deploymentEdition === 'ENTERPRISE') return <LicenseNav />
return null
}
@ -313,6 +315,7 @@ export function WorkspaceCard() {
showPlanAction={showPlanAction}
planActionLabel={planActionLabel}
creditsHref={buildIntegrationPath('provider')}
onPrefetchWorkspaces={prefetchWorkspaces}
onPlanClick={setShowPricingModal}
/>
<PopoverContent
@ -338,17 +341,14 @@ export function WorkspaceCard() {
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })
}}
/>
{workspaces.length > 0 && (
<div className="p-1 pb-2">
<WorkspaceSwitcher
workspaces={workspaces}
onSwitchWorkspace={(workspaceId) => {
setOpen(false)
void handleSwitchWorkspace(workspaceId)
}}
/>
</div>
)}
<WorkspaceSwitcher
workspaces={workspaces}
isPending={workspacesQuery.isPending}
onSwitchWorkspace={(workspaceId) => {
setOpen(false)
void handleSwitchWorkspace(workspaceId)
}}
/>
</PopoverContent>
</>
</Popover>

View File

@ -17,7 +17,7 @@ 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'
'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'
@ -30,11 +30,13 @@ const getWorkspaceLastOpenedAt = (workspace: TenantListItemResponse) =>
workspace.last_opened_at ?? 0
function WorkspaceSwitchControls({
disabled,
searchText,
sort,
onSearchTextChange,
onSortChange,
}: {
disabled: boolean
searchText: string
sort: WorkspaceSort
onSearchTextChange: (value: string) => void
@ -72,6 +74,7 @@ function WorkspaceSwitchControls({
<DropdownMenu open={sortMenuOpen} onOpenChange={setSortMenuOpen}>
<DropdownMenuTrigger
aria-label={sortMenuLabel}
disabled={disabled}
className={cn(
workspaceSwitchActionButtonClassName,
sortMenuOpen && 'bg-state-base-hover text-text-secondary',
@ -113,6 +116,7 @@ function WorkspaceSwitchControls({
<button
type="button"
aria-label={t(($) => $['operation.search'], { ns: 'common' })}
disabled={disabled}
className={cn(
workspaceSwitchActionButtonClassName,
searchVisible && 'bg-state-base-hover text-text-secondary',
@ -142,20 +146,25 @@ function WorkspaceSwitchControls({
}
type WorkspaceSwitcherProps = {
workspaces: TenantListItemResponse[]
workspaces?: TenantListItemResponse[]
isPending: boolean
onSwitchWorkspace: (workspaceId: string) => void
}
export function WorkspaceSwitcher({ workspaces, onSwitchWorkspace }: WorkspaceSwitcherProps) {
export function WorkspaceSwitcher({
workspaces,
isPending,
onSwitchWorkspace,
}: WorkspaceSwitcherProps) {
const [workspaceSearchText, setWorkspaceSearchText] = useState('')
const [workspaceSort, setWorkspaceSort] = useState<WorkspaceSort>('lastOpened')
const displayedWorkspaces = useMemo(() => {
const normalizedSearchText = workspaceSearchText.trim().toLowerCase()
const filteredWorkspaces = normalizedSearchText
? workspaces.filter((workspace) =>
? (workspaces?.filter((workspace) =>
getWorkspaceName(workspace).toLowerCase().includes(normalizedSearchText),
)
: [...workspaces]
) ?? [])
: [...(workspaces ?? [])]
if (workspaceSort === 'createdAt')
return filteredWorkspaces.sort((a, b) => getWorkspaceCreatedAt(b) - getWorkspaceCreatedAt(a))
@ -168,45 +177,54 @@ export function WorkspaceSwitcher({ workspaces, onSwitchWorkspace }: WorkspaceSw
})
}, [workspaceSearchText, workspaceSort, workspaces])
if (!isPending && !workspaces) return null
return (
<>
<div className="p-1 pb-2">
<WorkspaceSwitchControls
disabled={isPending}
searchText={workspaceSearchText}
sort={workspaceSort}
onSearchTextChange={setWorkspaceSearchText}
onSortChange={setWorkspaceSort}
/>
<div className={workspaceSwitchListClassName}>
{displayedWorkspaces.map((workspace) => {
const workspaceName = getWorkspaceName(workspace)
<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>
) : (
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 (
<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>
)
})
)}
</div>
</>
</div>
)
}

View File

@ -556,6 +556,7 @@ describe('Console bootstrap', () => {
expect.objectContaining({
email: 'user@example.com',
workspace_id: 'workspace-1',
workspace_plan: 'sandbox',
workspace_role: 'editor',
}),
)