Skip to content
2 changes: 1 addition & 1 deletion src/components/topbar/CurrentUserButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const showWorkspaceSkeleton = computed(
() => isCloud && initState.value === 'loading'
)
const showWorkspaceIcon = computed(
() => isCloud && initState.value === 'ready' && !isInPersonalWorkspace.value
() => initState.value === 'ready' && !isInPersonalWorkspace.value
)

const workspaceName = computed(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/topbar/CurrentUserPopoverLegacy.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createTestingPinia } from '@pinia/testing'
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
Expand Down Expand Up @@ -175,7 +176,7 @@ describe('CurrentUserPopoverLegacy', () => {

render(CurrentUserPopoverLegacy, {
global: {
plugins: [i18n],
plugins: [i18n, createTestingPinia({ createSpy: vi.fn })],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
stubs: {
Divider: true
}
Expand Down
43 changes: 42 additions & 1 deletion src/components/topbar/CurrentUserPopoverLegacy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@
</span>
</div>

<!-- Workspace Selector -->
<div v-if="showWorkspaceSwitcher" class="relative">
<div
v-tooltip="{ value: workspaceName, showDelay: 300 }"
class="flex cursor-pointer items-center justify-between rounded-lg px-4 py-2 hover:bg-secondary-background-hover"
data-testid="workspace-switcher-trigger"
@click="isWorkspaceSwitcherOpen = !isWorkspaceSwitcherOpen"
>
<div class="flex w-0 flex-1 items-center gap-2">
<WorkspaceProfilePic
class="size-6 shrink-0 text-xs"
:workspace-name="workspaceName"
/>
<span class="truncate text-sm text-base-foreground">
{{ workspaceName }}
</span>
</div>
<i class="pi pi-chevron-down shrink-0 text-sm text-muted-foreground" />
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

<div
v-if="isWorkspaceSwitcherOpen"
class="absolute top-0 right-full z-10 mr-4 rounded-lg border border-border-default bg-base-background shadow-[1px_1px_8px_0_rgba(0,0,0,0.4)]"
data-testid="workspace-switcher-panel"
>
<WorkspaceSwitcherPopover @select="isWorkspaceSwitcherOpen = false" />
</div>
</div>

<!-- Credits Section -->
<div
v-if="canAccessSubscriptionFeatures"
Expand Down Expand Up @@ -160,9 +189,10 @@

<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import { storeToRefs } from 'pinia'
import Divider from 'primevue/divider'
import Skeleton from 'primevue/skeleton'
import { computed, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'

import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
Expand All @@ -176,7 +206,10 @@ import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
import WorkspaceProfilePic from '@/platform/workspace/components/WorkspaceProfilePic.vue'
import WorkspaceSwitcherPopover from '@/platform/workspace/components/WorkspaceSwitcherPopover.vue'
import { useWorkspaceTierLabel } from '@/platform/workspace/composables/useWorkspaceTierLabel'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
import { useDialogService } from '@/services/dialogService'

const emit = defineEmits<{
Expand All @@ -203,6 +236,14 @@ const { formatTierName } = useWorkspaceTierLabel()
const subscriptionDialog = useSubscriptionDialog()
const { locale, t } = useI18n()

const { initState, workspaces, workspaceName } = storeToRefs(
useTeamWorkspaceStore()
)
const isWorkspaceSwitcherOpen = ref(false)
const showWorkspaceSwitcher = computed(
() => initState.value === 'ready' && workspaces.value.length > 0
)

const subscriptionTierName = computed(() =>
formatTierName(tier.value, subscription.value?.duration === 'ANNUAL')
)
Expand Down
27 changes: 26 additions & 1 deletion src/platform/workspace/auth/WorkspaceAuthGate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ function cancelInitialization(): void {
}

async function initialize(): Promise<void> {
if (!isCloud) return
if (!isCloud) {
void initializeWorkspacesInBackground()
return
}

cancelInitialization()
const generation = initializationGeneration
Expand Down Expand Up @@ -230,6 +233,28 @@ async function initializeWorkspaceMode(): Promise<void> {
}
}

// On non-cloud distributions the gate never blocks rendering; workspace
// context (credit-wallet selection) is hydrated in the background so the
// switcher can appear once ready, and the app works untouched if it fails.
async function initializeWorkspacesInBackground(): Promise<void> {
const { isInitialized, currentUser } = storeToRefs(useAuthStore())
try {
if (!isInitialized.value) {
await until(isInitialized).toBe(true, {
timeout: FIREBASE_INIT_TIMEOUT_MS,
throwOnTimeout: true
})
}
if (!currentUser.value) return
await initializeWorkspaceMode()
} catch (error) {
console.warn(
'[WorkspaceAuthGate] Background workspace initialization failed:',
error
)
}
}

// Initialize on mount. This gate should be placed on the authenticated layout
// (LayoutDefault) so it mounts fresh after login and unmounts on logout.
// The router guard ensures only authenticated users reach this layout.
Expand Down
14 changes: 14 additions & 0 deletions src/platform/workspace/components/WorkspaceSwitcherPopover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
useBillingContext: () => ({ subscription: billingMocks.subscription })
}))

const distributionMocks = vi.hoisted(() => ({ isCloud: true }))

vi.mock('@/platform/distribution/types', () => distributionMocks)

const LONG_WORKSPACE_NAME =
'Quantum Renaissance Collective for Hyperdimensional Latent Diffusion Research and Experimental Workflow Engineering'

Expand Down Expand Up @@ -101,6 +105,7 @@ function renderComponent(
describe('WorkspaceSwitcherPopover', () => {
beforeEach(() => {
billingMocks.subscription.value = null
distributionMocks.isCloud = true
})

it.for([
Expand Down Expand Up @@ -239,4 +244,13 @@ describe('WorkspaceSwitcherPopover', () => {
const createWorkspaceButton = screen.getByText('Create a team workspace')
expect(list).not.toContainElement(createWorkspaceButton)
})

it('hides the create-workspace footer on non-cloud distributions', () => {
distributionMocks.isCloud = false

renderComponent()

expect(screen.queryByText('Create a team workspace')).toBeNull()
expect(screen.queryByText(/You can only own 10 workspaces/)).toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
</div>

<!-- Create workspace button -->
<div class="shrink-0 border-t border-border-default p-2">
<div v-if="isCloud" class="shrink-0 border-t border-border-default p-2">
<div
:class="
cn(
Expand Down Expand Up @@ -113,6 +113,7 @@ import { useI18n } from 'vue-i18n'

import WorkspaceProfilePic from '@/platform/workspace/components/WorkspaceProfilePic.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { isCloud } from '@/platform/distribution/types'
import { useWorkspaceSwitch } from '@/platform/workspace/composables/useWorkspaceSwitch'
import { useWorkspaceTierLabel } from '@/platform/workspace/composables/useWorkspaceTierLabel'
import type {
Expand Down
Loading