Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<template>
<router-view />
<ProgressSpinner
v-if="isLoading"
class="absolute inset-0 flex h-[unset] items-center justify-center"
/>
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
<WorkspaceAuthGate>
<router-view />
<ProgressSpinner
v-if="isLoading"
class="absolute inset-0 flex h-[unset] items-center justify-center"
/>
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
</WorkspaceAuthGate>
</template>

<script setup lang="ts">
Expand All @@ -14,6 +16,7 @@ import BlockUI from 'primevue/blockui'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, onMounted } from 'vue'

import WorkspaceAuthGate from '@/components/auth/WorkspaceAuthGate.vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'
Expand Down
206 changes: 206 additions & 0 deletions src/components/auth/WorkspaceAuthGate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'

import WorkspaceAuthGate from './WorkspaceAuthGate.vue'

const mockIsInitialized = ref(false)
const mockCurrentUser = ref<object | null>(null)

vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: () => ({
isInitialized: mockIsInitialized,
currentUser: mockCurrentUser
})
}))

const mockRefreshRemoteConfig = vi.fn()
vi.mock('@/platform/remoteConfig/refreshRemoteConfig', () => ({
refreshRemoteConfig: (options: unknown) => mockRefreshRemoteConfig(options)
}))

const mockTeamWorkspacesEnabled = vi.hoisted(() => ({ value: false }))
vi.mock('@/composables/useFeatureFlags', () => ({
useFeatureFlags: () => ({
flags: {
get teamWorkspacesEnabled() {
return mockTeamWorkspacesEnabled.value
}
}
})
}))

const mockWorkspaceStoreInitialize = vi.fn()
const mockWorkspaceStoreInitState = vi.hoisted(() => ({
value: 'uninitialized' as string
}))
vi.mock('@/platform/workspace/stores/teamWorkspaceStore', () => ({
useTeamWorkspaceStore: () => ({
get initState() {
return mockWorkspaceStoreInitState.value
},
initialize: mockWorkspaceStoreInitialize
})
}))

const mockIsCloud = vi.hoisted(() => ({ value: true }))
vi.mock('@/platform/distribution/types', () => ({
get isCloud() {
return mockIsCloud.value
}
}))

vi.mock('primevue/progressspinner', () => ({
default: { template: '<div class="progress-spinner" />' }
}))

describe('WorkspaceAuthGate', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsCloud.value = true
mockIsInitialized.value = false
mockCurrentUser.value = null
mockTeamWorkspacesEnabled.value = false
mockWorkspaceStoreInitState.value = 'uninitialized'
mockRefreshRemoteConfig.mockResolvedValue(undefined)
mockWorkspaceStoreInitialize.mockResolvedValue(undefined)
})

const mountComponent = () =>
mount(WorkspaceAuthGate, {
slots: {
default: '<div data-testid="slot-content">App Content</div>'
}
})

describe('non-cloud builds', () => {
it('renders slot immediately when isCloud is false', async () => {
mockIsCloud.value = false

const wrapper = mountComponent()
await flushPromises()

expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
expect(wrapper.find('.progress-spinner').exists()).toBe(false)
expect(mockRefreshRemoteConfig).not.toHaveBeenCalled()
})
})

describe('cloud builds - unauthenticated user', () => {
it('shows spinner while waiting for Firebase auth', () => {
mockIsInitialized.value = false

const wrapper = mountComponent()

expect(wrapper.find('.progress-spinner').exists()).toBe(true)
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(false)
})

it('renders slot when Firebase initializes with no user', async () => {
mockIsInitialized.value = false

const wrapper = mountComponent()
expect(wrapper.find('.progress-spinner').exists()).toBe(true)

mockIsInitialized.value = true
mockCurrentUser.value = null
await flushPromises()

expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
expect(mockRefreshRemoteConfig).not.toHaveBeenCalled()
})
})

describe('cloud builds - authenticated user', () => {
beforeEach(() => {
mockIsInitialized.value = true
mockCurrentUser.value = { uid: 'user-123' }
})

it('refreshes remote config with auth after Firebase init', async () => {
mountComponent()
await flushPromises()

expect(mockRefreshRemoteConfig).toHaveBeenCalledWith({ useAuth: true })
})

it('renders slot when teamWorkspacesEnabled is false', async () => {
mockTeamWorkspacesEnabled.value = false

const wrapper = mountComponent()
await flushPromises()

expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
expect(mockWorkspaceStoreInitialize).not.toHaveBeenCalled()
})

it('initializes workspace store when teamWorkspacesEnabled is true', async () => {
mockTeamWorkspacesEnabled.value = true

const wrapper = mountComponent()
await flushPromises()

expect(mockWorkspaceStoreInitialize).toHaveBeenCalled()
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
})

it('skips workspace init when store is already initialized', async () => {
mockTeamWorkspacesEnabled.value = true
mockWorkspaceStoreInitState.value = 'ready'

const wrapper = mountComponent()
await flushPromises()

expect(mockWorkspaceStoreInitialize).not.toHaveBeenCalled()
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
})
})

describe('error handling - graceful degradation', () => {
beforeEach(() => {
mockIsInitialized.value = true
mockCurrentUser.value = { uid: 'user-123' }
})

it('renders slot when remote config refresh fails', async () => {
mockRefreshRemoteConfig.mockRejectedValue(new Error('Network error'))

const wrapper = mountComponent()
await flushPromises()

expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
})

it('renders slot when remote config refresh times out', async () => {
vi.useFakeTimers()
// Never-resolving promise simulates a hanging request
mockRefreshRemoteConfig.mockReturnValue(new Promise(() => {}))

const wrapper = mountComponent()
await flushPromises()

// Still showing spinner before timeout
expect(wrapper.find('.progress-spinner').exists()).toBe(true)

// Advance past the 10 second timeout
await vi.advanceTimersByTimeAsync(10_001)
await flushPromises()

// Should render slot after timeout (graceful degradation)
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
vi.useRealTimers()
})

it('renders slot when workspace store initialization fails', async () => {
mockTeamWorkspacesEnabled.value = true
mockWorkspaceStoreInitialize.mockRejectedValue(
new Error('Workspace init failed')
)

const wrapper = mountComponent()
await flushPromises()

expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
})
})
})
126 changes: 126 additions & 0 deletions src/components/auth/WorkspaceAuthGate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<template>
<slot v-if="isReady" />
<div
v-else
class="fixed inset-0 z-[1100] flex items-center justify-center bg-[var(--p-mask-background)]"
>
<ProgressSpinner />
</div>
</template>
Comment on lines +1 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for existing loading/spinner components and z-index usage patterns
echo "=== Existing spinner/loading components in Vue files ==="
fd -e vue | xargs rg -l -i 'spinner|loading' 2>/dev/null | head -20

echo ""
echo "=== Search for ProgressSpinner usage ==="
rg 'ProgressSpinner' --type vue --type ts | head -20

echo ""
echo "=== Check for alternative loading indicators ==="
fd -e vue -path "*/components/*" | xargs rg -l -i 'loading|spinner' 2>/dev/null | head -20

echo ""
echo "=== z-index values in the codebase ==="
rg 'z-\[' --type vue --type ts | grep -E 'z-\[[0-9]{3,}' | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2556


🏁 Script executed:

#!/bin/bash
# Read the actual file to see what's being used
echo "=== Full WorkspaceAuthGate.vue file ==="
cat -n src/components/auth/WorkspaceAuthGate.vue

echo ""
echo "=== Search for imports of ProgressSpinner in the codebase ==="
rg 'import.*ProgressSpinner|from.*ProgressSpinner' 

echo ""
echo "=== Search for z-index definitions and arbitrary values ==="
rg 'z-\[' src/ --no-heading | head -20

echo ""
echo "=== Look for common loading/spinner alternatives ==="
fd -type f -name '*.vue' -path '*/components/*' | xargs grep -l 'loading\|spinner' 2>/dev/null | head -10

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 8139


🏁 Script executed:

#!/bin/bash
# Check for z-index design tokens or theme values
echo "=== Search for z-index in CSS/theme files ==="
find src -name '*.css' -o -name 'style.*' | xargs grep -n 'z-index' 2>/dev/null | head -20

echo ""
echo "=== Check for tailwind z-index configuration ==="
cat tailwind.config.ts 2>/dev/null | grep -A 20 'zIndex' || echo "Config check completed"

echo ""
echo "=== Look for custom loading/spinner components in src/components ==="
fd -type f -name '*.vue' src/components/ | xargs grep -l 'spinner\|loading' -i 2>/dev/null | head -10

echo ""
echo "=== Check style.css for semantic z-index values ==="
grep -n 'z-index\|--.*z' src/style.css 2>/dev/null | head -15

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 781


🏁 Script executed:

#!/bin/bash
# Check if Suspense is used in similar auth/init gate patterns
echo "=== Search for Suspense usage in Vue components ==="
rg 'Suspense|<Suspense' src/ --no-heading | head -15

echo ""
echo "=== Check if there are similar auth gate components ==="
fd -type f -name '*[Aa]uth*[Gg]ate*.vue' src/

echo ""
echo "=== Check the main App.vue to see how it handles initial loading ==="
head -50 src/App.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2601


Replace PrimeVue ProgressSpinner with a custom loading component.

Per coding guidelines, "Avoid new usage of PrimeVue components." Consider using a project-standard loading indicator from src/components/ or implementing a simple spinner alternative.

Also, the z-[1100] hardcoded value should use a design token instead of an arbitrary number to maintain consistency with the theme system and avoid conflicts with other overlays.

🧰 Tools
🪛 ESLint

[error] 1-1: Resolve error: EACCES: permission denied, open '/iVKfOXJLLN'
at Object.writeFileSync (node:fs:2409:20)
at l (file:///home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.mjs:7:13670)
at createFilesMatcher (file:///home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.mjs:7:14422)
at resolve (file:///home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import-x@4.16.1_@typescript-eslin_44eddb5b99ae4bce470e6fb9a90221ee/node_modules/eslint-import-resolver-typescript/lib/index.js:70:65)
at Object.resolve (file:///home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import-x@4.16.1_@typescript-eslin_44eddb5b99ae4bce470e6fb9a90221ee/node_modules/eslint-import-resolver-typescript/lib/index.js:147:20)
at file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/resolve.js:170:69
at setRuleContext (/home/jailuser/git/node_modules/.pnpm/eslint-import-context@0.1.9_unrs-resolver@1.11.1/node_modules/eslint-import-context/lib/index.js:23:20)
at fullResolve (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/resolve.js:170:30)
at relative (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/resolve.js:215:12)
at resolve (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/resolve.js:220:16)
at checkSourceValue (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/rules/no-unresolved.js:31:34)
at checkSourceValue (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/module-visitor.js:14:9)
at checkSource (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/module-visitor.js:17:9)
at ruleErrorHandler (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1173:33)
at /home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/source-code-visitor.js:76:46
at Array.forEach ()
at SourceCodeVisitor.callSync (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/source-code-visitor.js:76:30)
at /home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/source-code-traverser.js:291:18
at Array.forEach ()
at SourceCodeTraverser.traverseSync (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/source-code-traverser.js:290:10)
at runRules (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1214:12)
at #flatVerifyWithoutProcessors (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:2101:4)
at Linter._verifyWithFlatConfigArrayAndWithoutProcessors (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:2189:43)
at /home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1773:17
at Array.map ()
at Linter._verifyWithFlatConfigArrayAndProcessor (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1768:30)
at Linter._verifyWithFlatConfigArray (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:2275:16)
at Linter.verify (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1677:10)
at Linter.verifyAndFix (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:2557:20)
at verifyText (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint-helpers.js:1180:45)
at readAndVerifyFile (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint-helpers.js:1321:10)

(import-x/no-unresolved)


[error] 1-1: Resolve error: EACCES: permission denied, open '/HOcZIXwqKG'
at Object.writeFileSync (node:fs:2409:20)
at l (file:///home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.mjs:7:13670)
at createFilesMatcher (file:///home/jailuser/git/node_modules/.pnpm/get-tsconfig@4.10.1/node_modules/get-tsconfig/dist/index.mjs:7:14422)
at resolve (file:///home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import-x@4.16.1_@typescript-eslin_44eddb5b99ae4bce470e6fb9a90221ee/node_modules/eslint-import-resolver-typescript/lib/index.js:70:65)
at Object.resolve (file:///home/jailuser/git/node_modules/.pnpm/eslint-import-resolver-typescript@4.4.4_eslint-plugin-import-x@4.16.1_@typescript-eslin_44eddb5b99ae4bce470e6fb9a90221ee/node_modules/eslint-import-resolver-typescript/lib/index.js:147:20)
at file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/resolve.js:170:69
at setRuleContext (/home/jailuser/git/node_modules/.pnpm/eslint-import-context@0.1.9_unrs-resolver@1.11.1/node_modules/eslint-import-context/lib/index.js:23:20)
at fullResolve (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/resolve.js:170:30)
at relative (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/resolve.js:215:12)
at resolve (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/resolve.js:220:16)
at importType (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/import-type.js:126:63)
at checkImportForRelativePackage (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/rules/no-relative-packages.js:15:38)
at file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/rules/no-relative-packages.js:59:40
at checkSourceValue (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/module-visitor.js:14:9)
at checkSource (file:///home/jailuser/git/node_modules/.pnpm/eslint-plugin-import-x@4.16.1_@typescript-eslint+utils@8.50.0_eslint@9.39.1_jiti@2.6.1__eacac87f98d760f1781d40e8519857dc/node_modules/eslint-plugin-import-x/lib/utils/module-visitor.js:17:9)
at ruleErrorHandler (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1173:33)
at /home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/source-code-visitor.js:76:46
at Array.forEach ()
at SourceCodeVisitor.callSync (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/source-code-visitor.js:76:30)
at /home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/source-code-traverser.js:291:18
at Array.forEach ()
at SourceCodeTraverser.traverseSync (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/source-code-traverser.js:290:10)
at runRules (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1214:12)
at #flatVerifyWithoutProcessors (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:2101:4)
at Linter._verifyWithFlatConfigArrayAndWithoutProcessors (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:2189:43)
at /home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1773:17
at Array.map ()
at Linter._verifyWithFlatConfigArrayAndProcessor (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1768:30)
at Linter._verifyWithFlatConfigArray (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:2275:16)
at Linter.verify (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:1677:10)
at Linter.verifyAndFix (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/linter/linter.js:2557:20)
at verifyText (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint-helpers.js:1180:45)
at readAndVerifyFile (/home/jailuser/git/node_modules/.pnpm/eslint@9.39.1_jiti@2.6.1/node_modules/eslint/lib/eslint/eslint-helpers.js:1321:10)

(import-x/no-relative-packages)

🤖 Prompt for AI Agents
In `@src/components/auth/WorkspaceAuthGate.vue` around lines 1 - 9, Replace the
PrimeVue ProgressSpinner usage in WorkspaceAuthGate.vue with the
project-standard loading component (e.g., import and use the shared
FullscreenLoader or LoadingSpinner from src/components/) and remove any
PrimeVue-specific imports; render that component in place of <ProgressSpinner />
inside the v-else block. Also replace the hardcoded z-[1100] with the theme
design token (use the CSS variable or class token used across the project, e.g.,
use style/class that references --z-overlay or the project's overlay z-index
token) so the wrapper uses the centralized z-index token instead of an arbitrary
number.


<script setup lang="ts">
/**
* WorkspaceAuthGate - Conditional auth checkpoint for workspace mode.
*
* This gate ensures proper initialization order for workspace-scoped auth:
* 1. Wait for Firebase auth to resolve
* 2. Check if teamWorkspacesEnabled feature flag is on
* 3. If YES: Initialize workspace token and store before rendering
* 4. If NO: Render immediately using existing Firebase auth
*
* This prevents race conditions where API calls use Firebase tokens
* instead of workspace tokens when the workspace feature is enabled.
*/
import { promiseTimeout, until } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import ProgressSpinner from 'primevue/progressspinner'
import { ref } from 'vue'

import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { refreshRemoteConfig } from '@/platform/remoteConfig/refreshRemoteConfig'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'

const FIREBASE_INIT_TIMEOUT_MS = 16_000
const CONFIG_REFRESH_TIMEOUT_MS = 10_000

const isReady = ref(!isCloud)

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

const authStore = useFirebaseAuthStore()
const { isInitialized, currentUser } = storeToRefs(authStore)

try {
// Step 1: Wait for Firebase auth to resolve
// This is shared with router guard - both wait for the same thing,
// but this gate blocks rendering while router guard blocks navigation
if (!isInitialized.value) {
await until(isInitialized).toBe(true, {
timeout: FIREBASE_INIT_TIMEOUT_MS
})
}

// Step 2: If not authenticated, nothing more to do
// Unauthenticated users don't have workspace context
if (!currentUser.value) {
isReady.value = true
return
}

// Step 3: Refresh feature flags with auth context
// This ensures teamWorkspacesEnabled reflects the authenticated user's state
// Timeout prevents hanging if server is slow/unresponsive
try {
await Promise.race([
refreshRemoteConfig({ useAuth: true }),
promiseTimeout(CONFIG_REFRESH_TIMEOUT_MS).then(() => {
throw new Error('Config refresh timeout')
})
])
} catch (error) {
console.warn(
'[WorkspaceAuthGate] Failed to refresh remote config:',
error
)
// Continue - feature flags will use defaults (teamWorkspacesEnabled=false)
// App will render with Firebase auth fallback
}

// Step 4: THE CHECKPOINT - Are we in workspace mode?
const { flags } = useFeatureFlags()
if (!flags.teamWorkspacesEnabled) {
// Not in workspace mode - use existing Firebase auth flow
// No additional initialization needed
isReady.value = true
return
}

// Step 5: WORKSPACE MODE - Full initialization
await initializeWorkspaceMode()
} catch (error) {
console.error('[WorkspaceAuthGate] Initialization failed:', error)
} finally {
// Always render (graceful degradation)
// If workspace init failed, API calls fall back to Firebase token
isReady.value = true
}
}

async function initializeWorkspaceMode(): Promise<void> {
// Initialize the full workspace store which handles:
// - Restoring workspace token from session (fast path for refresh)
// - Fetching workspace list
// - Switching to last used workspace if needed
// - Setting active workspace
try {
const workspaceStore = useTeamWorkspaceStore()
if (workspaceStore.initState === 'uninitialized') {
await workspaceStore.initialize()
}
} catch (error) {
// Log but don't block - workspace UI features may not work but app will render
// API calls will fall back to Firebase token
console.warn(
'[WorkspaceAuthGate] Failed to initialize workspace store:',
error
)
}
}

// Start initialization immediately during component setup
// (not in onMounted, so initialization starts before DOM is ready)
void initialize()
</script>
16 changes: 3 additions & 13 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -502,19 +502,9 @@ onMounted(async () => {
await workflowPersistence.loadTemplateFromUrlIfPresent()

// Accept workspace invite from URL if present (e.g., ?invite=TOKEN)
// Uses watch because feature flags load asynchronously - flag may be false initially
// then become true once remoteConfig or websocket features are loaded
if (inviteUrlLoader) {
const stopWatching = watch(
() => flags.teamWorkspacesEnabled,
async (enabled) => {
if (enabled) {
stopWatching()
await inviteUrlLoader.loadInviteFromUrl()
}
},
{ immediate: true }
)
// WorkspaceAuthGate ensures flag state is resolved before GraphCanvas mounts
if (inviteUrlLoader && flags.teamWorkspacesEnabled) {
await inviteUrlLoader.loadInviteFromUrl()
}

// Initialize release store to fetch releases from comfy-api (fire-and-forget)
Expand Down
Loading
Loading