mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 18:10:08 +00:00
fix: Make feature flags reactive and fix manager UI state determination
- Create reactive featureFlagsStore for centralized feature flag management - Fix race conditions in feature flag updates with version tracking - Make managerStateStore properly reactive to feature flag changes - Add useManagerHelper composable as single entry point for opening manager - Fix Legacy UI keybinding fallback to new manager when appropriate - Update WebSocket handler to sync feature flags with reactive store - Add comprehensive tests for all manager UI states This ensures the Manager UI state (NEW_UI, LEGACY_UI, DISABLED) updates reactively when feature flags change, fixing the issue where manager state wasn't properly responding to server capability changes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -54,19 +54,16 @@
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useManagerHelper } from '@/composables/useManagerHelper'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -112,6 +109,7 @@ const uniqueNodes = computed(() => {
|
||||
})
|
||||
|
||||
const managerStateStore = useManagerStateStore()
|
||||
const { openManager: openManagerHelper } = useManagerHelper()
|
||||
|
||||
// Show manager buttons unless manager is disabled
|
||||
const showManagerButtons = computed(() => {
|
||||
@@ -123,35 +121,12 @@ const showInstallAllButton = computed(() => {
|
||||
return managerStateStore.managerUIState === ManagerUIState.NEW_UI
|
||||
})
|
||||
|
||||
// Open manager with Missing tab for NEW_UI
|
||||
const openManager = async () => {
|
||||
const state = managerStateStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
useDialogService().showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch {
|
||||
// If legacy command doesn't exist, show toast
|
||||
const { t } = useI18n()
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
useDialogService().showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
break
|
||||
}
|
||||
// Use the helper with Missing tab option
|
||||
await openManagerHelper({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useManagerHelper } from '@/composables/useManagerHelper'
|
||||
import { type ReleaseNote } from '@/services/releaseService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
@@ -191,7 +191,7 @@ const { t, locale } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
const { openManager } = useManagerHelper()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -313,8 +313,8 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
icon: PuzzleIcon,
|
||||
label: t('helpCenter.managerExtension'),
|
||||
showRedDot: shouldShowManagerRedDot.value,
|
||||
action: () => {
|
||||
dialogService.showManagerDialog()
|
||||
action: async () => {
|
||||
await openManager()
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -106,13 +106,9 @@ import { useI18n } from 'vue-i18n'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useManagerHelper } from '@/composables/useManagerHelper'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
@@ -159,30 +155,7 @@ const showSettings = (defaultPanel?: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const managerStateStore = useManagerStateStore()
|
||||
|
||||
const showManageExtensions = async () => {
|
||||
const state = managerStateStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
showSettings('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch {
|
||||
// If legacy command doesn't exist, fall back to extensions panel
|
||||
showSettings('extension')
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
useDialogService().showManagerDialog()
|
||||
break
|
||||
}
|
||||
}
|
||||
const { openManager } = useManagerHelper()
|
||||
|
||||
const extraMenuItems = computed<MenuItem[]>(() => [
|
||||
{ separator: true },
|
||||
@@ -207,7 +180,7 @@ const extraMenuItems = computed<MenuItem[]>(() => [
|
||||
key: 'manage-extensions',
|
||||
label: t('menu.manageExtensions'),
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
command: showManageExtensions
|
||||
command: () => openManager()
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -763,7 +763,8 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog()
|
||||
// Show settings dialog instead of new manager for legacy UI state
|
||||
dialogService.showSettingsDialog('extension')
|
||||
}
|
||||
break
|
||||
|
||||
@@ -1015,12 +1016,31 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
|
||||
)
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
// Check manager state to avoid infinite loop
|
||||
const { ManagerUIState, useManagerStateStore } = await import(
|
||||
'@/stores/managerStateStore'
|
||||
)
|
||||
const managerState = useManagerStateStore().managerUIState
|
||||
|
||||
if (managerState === ManagerUIState.NEW_UI) {
|
||||
// Only fallback to new manager if we're in NEW_UI state
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
const managerHelper = await import('@/composables/useManagerHelper')
|
||||
await managerHelper.openManager()
|
||||
} else {
|
||||
// In LEGACY_UI state, just show error without fallback
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1033,12 +1053,31 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
// Check manager state to avoid infinite loop
|
||||
const { ManagerUIState, useManagerStateStore } = await import(
|
||||
'@/stores/managerStateStore'
|
||||
)
|
||||
const managerState = useManagerStateStore().managerUIState
|
||||
|
||||
if (managerState === ManagerUIState.NEW_UI) {
|
||||
// Only fallback to new manager if we're in NEW_UI state
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
const managerHelper = await import('@/composables/useManagerHelper')
|
||||
await managerHelper.openManager()
|
||||
} else {
|
||||
// In LEGACY_UI state, just show error without fallback
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,40 +1,77 @@
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
import { computed, readonly } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFeatureFlagsStore } from '@/stores/featureFlagsStore'
|
||||
|
||||
/**
|
||||
* Known server feature flags (top-level, not extensions)
|
||||
* Known server feature flags
|
||||
*/
|
||||
export enum ServerFeatureFlag {
|
||||
// Core features
|
||||
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
|
||||
MAX_UPLOAD_SIZE = 'max_upload_size',
|
||||
|
||||
// Extension features
|
||||
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4'
|
||||
}
|
||||
|
||||
/**
|
||||
* Known client feature flags
|
||||
*/
|
||||
export enum ClientFeatureFlag {
|
||||
SUPPORTS_MANAGER_V4_UI = 'supports_manager_v4_ui'
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for reactive access to feature flags
|
||||
*/
|
||||
export function useFeatureFlags() {
|
||||
// Create reactive state that tracks server feature flags
|
||||
const flags = reactive({
|
||||
get supportsPreviewMetadata() {
|
||||
return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
|
||||
},
|
||||
get maxUploadSize() {
|
||||
return api.getServerFeature(ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
},
|
||||
get supportsManagerV4() {
|
||||
return api.getServerFeature(ServerFeatureFlag.MANAGER_SUPPORTS_V4)
|
||||
}
|
||||
})
|
||||
const featureFlagsStore = useFeatureFlagsStore()
|
||||
|
||||
// Expose commonly used flags directly as computed properties
|
||||
const flags = computed(() => ({
|
||||
// Server flags
|
||||
supportsPreviewMetadata: featureFlagsStore.getServerFeature<boolean>(
|
||||
ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA,
|
||||
false
|
||||
),
|
||||
maxUploadSize: featureFlagsStore.getServerFeature<number>(
|
||||
ServerFeatureFlag.MAX_UPLOAD_SIZE
|
||||
),
|
||||
supportsManagerV4: featureFlagsStore.supportsManagerV4,
|
||||
|
||||
// Client flags
|
||||
clientSupportsManagerV4UI: featureFlagsStore.clientSupportsManagerV4UI,
|
||||
|
||||
// Store ready state
|
||||
isReady: featureFlagsStore.isReady
|
||||
}))
|
||||
|
||||
// Create a reactive computed for any feature flag
|
||||
const featureFlag = <T = unknown>(featurePath: string, defaultValue?: T) => {
|
||||
return computed(() => api.getServerFeature(featurePath, defaultValue))
|
||||
return computed(() =>
|
||||
featureFlagsStore.getServerFeature(featurePath, defaultValue)
|
||||
)
|
||||
}
|
||||
|
||||
// Create a reactive computed for checking if server supports a feature
|
||||
const serverSupportsFeature = (featurePath: string) => {
|
||||
return computed(() => featureFlagsStore.serverSupportsFeature(featurePath))
|
||||
}
|
||||
|
||||
return {
|
||||
// Enums for type-safe access
|
||||
ServerFeatureFlag,
|
||||
ClientFeatureFlag,
|
||||
|
||||
// Computed flags object
|
||||
flags: readonly(flags),
|
||||
featureFlag
|
||||
|
||||
// Helper functions
|
||||
featureFlag,
|
||||
serverSupportsFeature,
|
||||
|
||||
// Direct access to store methods (for advanced usage)
|
||||
getServerFeature: featureFlagsStore.getServerFeature,
|
||||
getClientFeature: featureFlagsStore.getClientFeature
|
||||
}
|
||||
}
|
||||
|
||||
79
src/composables/useManagerHelper.ts
Normal file
79
src/composables/useManagerHelper.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import type { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
/**
|
||||
* Options for opening the Manager
|
||||
*/
|
||||
export interface OpenManagerOptions {
|
||||
/**
|
||||
* Initial tab to show when opening the Manager (NEW_UI only)
|
||||
*/
|
||||
initialTab?: ManagerTab
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Manager UI based on the current state (NEW_UI, LEGACY_UI, or DISABLED)
|
||||
* This is the single entry point for opening the Manager from anywhere in the app
|
||||
*/
|
||||
export async function openManager(options?: OpenManagerOptions): Promise<void> {
|
||||
const managerStateStore = useManagerStateStore()
|
||||
const commandStore = useCommandStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const state = managerStateStore.managerUIState
|
||||
console.log('[Manager Helper] Opening manager with state:', state)
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
// Show settings dialog with extension tab
|
||||
dialogStore.showDialog({
|
||||
key: 'global-settings',
|
||||
headerComponent: SettingDialogHeader,
|
||||
component: SettingDialogContent,
|
||||
props: {
|
||||
defaultPanel: 'extension'
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
// Try legacy manager command directly without causing recursion
|
||||
await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch (e) {
|
||||
console.warn('[Manager Helper] Legacy manager not available:', e)
|
||||
// Show settings as fallback
|
||||
dialogStore.showDialog({
|
||||
key: 'global-settings',
|
||||
headerComponent: SettingDialogHeader,
|
||||
component: SettingDialogContent,
|
||||
props: {
|
||||
defaultPanel: 'extension'
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
dialogService.showManagerDialog(options)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for manager helper functions
|
||||
*/
|
||||
export function useManagerHelper() {
|
||||
return {
|
||||
openManager
|
||||
}
|
||||
}
|
||||
@@ -261,6 +261,24 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
reportedUnknownMessageTypes = new Set<string>()
|
||||
|
||||
/**
|
||||
* Cached feature flags store module to avoid repeated imports
|
||||
*/
|
||||
#featureFlagsStorePromise?: Promise<
|
||||
typeof import('@/stores/featureFlagsStore')
|
||||
>
|
||||
|
||||
/**
|
||||
* Get cached feature flags store module (lazy loading)
|
||||
*/
|
||||
async #getFeatureFlagsStore() {
|
||||
if (!this.#featureFlagsStorePromise) {
|
||||
this.#featureFlagsStorePromise = import('@/stores/featureFlagsStore')
|
||||
}
|
||||
const module = await this.#featureFlagsStorePromise
|
||||
return module.useFeatureFlagsStore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feature flags supported by this frontend client.
|
||||
* Returns a copy to prevent external modification.
|
||||
@@ -417,13 +435,23 @@ export class ComfyApi extends EventTarget {
|
||||
opened = true
|
||||
|
||||
// Send feature flags as the first message
|
||||
const clientFlags = this.getClientFeatureFlags()
|
||||
this.socket!.send(
|
||||
JSON.stringify({
|
||||
type: 'feature_flags',
|
||||
data: this.getClientFeatureFlags()
|
||||
data: clientFlags
|
||||
})
|
||||
)
|
||||
|
||||
// Update client flags in the store
|
||||
this.#getFeatureFlagsStore()
|
||||
.then((store) => {
|
||||
store.updateClientFlags(clientFlags)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[API] Failed to update client flags:', error)
|
||||
})
|
||||
|
||||
if (isReconnect) {
|
||||
this.dispatchCustomEvent('reconnected')
|
||||
}
|
||||
@@ -444,10 +472,18 @@ export class ComfyApi extends EventTarget {
|
||||
if (opened) {
|
||||
this.dispatchCustomEvent('status', null)
|
||||
this.dispatchCustomEvent('reconnecting')
|
||||
// Reset feature flags store when connection is lost
|
||||
this.#getFeatureFlagsStore()
|
||||
.then((store) => {
|
||||
store.resetStore()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[API] Failed to reset feature flags store:', error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.addEventListener('message', (event) => {
|
||||
this.socket.addEventListener('message', async (event) => {
|
||||
try {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
const view = new DataView(event.data)
|
||||
@@ -552,6 +588,18 @@ export class ComfyApi extends EventTarget {
|
||||
'Server feature flags received:',
|
||||
this.serverFeatureFlags
|
||||
)
|
||||
// Update the reactive store asynchronously without blocking
|
||||
this.#getFeatureFlagsStore()
|
||||
.then((store) => {
|
||||
store.updateServerFlags(msg.data)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'[API] Failed to update feature flags store:',
|
||||
error
|
||||
)
|
||||
// Store update failed but api.serverFeatureFlags is still updated
|
||||
})
|
||||
break
|
||||
default:
|
||||
if (this.#registered.has(msg.type)) {
|
||||
|
||||
139
src/stores/featureFlagsStore.ts
Normal file
139
src/stores/featureFlagsStore.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { get } from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Store for managing server and client feature flags reactively
|
||||
*/
|
||||
export const useFeatureFlagsStore = defineStore('featureFlags', () => {
|
||||
// Server feature flags received from WebSocket
|
||||
const serverFlags = ref<Record<string, unknown>>({})
|
||||
|
||||
// Client feature flags (local)
|
||||
const clientFlags = ref<Record<string, unknown>>({})
|
||||
|
||||
// Flag to indicate if the store has received initial server flags
|
||||
const isReady = ref(false)
|
||||
|
||||
// Track update version to prevent race conditions
|
||||
let updateVersion = 0
|
||||
|
||||
/**
|
||||
* Update server feature flags
|
||||
* Called when WebSocket receives feature_flags message
|
||||
*/
|
||||
function updateServerFlags(flags: Record<string, unknown>) {
|
||||
// Validate input
|
||||
if (!flags || typeof flags !== 'object') {
|
||||
console.error('[FeatureFlags] Invalid flags received:', flags)
|
||||
return
|
||||
}
|
||||
|
||||
// Increment version for this update
|
||||
const currentVersion = ++updateVersion
|
||||
|
||||
// Use nextTick to batch updates and check version
|
||||
void nextTick(() => {
|
||||
// Only apply if this is still the latest update
|
||||
if (currentVersion === updateVersion) {
|
||||
serverFlags.value = { ...flags }
|
||||
isReady.value = true
|
||||
console.log('[FeatureFlags] Server flags updated:', serverFlags.value)
|
||||
} else {
|
||||
console.log(
|
||||
'[FeatureFlags] Skipping outdated update, version:',
|
||||
currentVersion
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update client feature flags
|
||||
*/
|
||||
function updateClientFlags(flags: Record<string, unknown>) {
|
||||
clientFlags.value = { ...flags }
|
||||
console.log('[FeatureFlags] Client flags updated:', clientFlags.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a server feature flag value (reactive)
|
||||
*/
|
||||
function getServerFeature<T = unknown>(
|
||||
featurePath: string,
|
||||
defaultValue?: T
|
||||
): T {
|
||||
return get(serverFlags.value, featurePath, defaultValue) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if server supports a feature (reactive)
|
||||
*/
|
||||
function serverSupportsFeature(featurePath: string): boolean {
|
||||
return get(serverFlags.value, featurePath) === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a client feature flag value (reactive)
|
||||
*/
|
||||
function getClientFeature<T = unknown>(
|
||||
featurePath: string,
|
||||
defaultValue?: T
|
||||
): T {
|
||||
return get(clientFlags.value, featurePath, defaultValue) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed property for manager v4 support
|
||||
*/
|
||||
const supportsManagerV4 = computed<boolean>(() => {
|
||||
return (
|
||||
getServerFeature<boolean>('extension.manager.supports_v4', false) === true
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Computed property for client manager v4 UI support
|
||||
*/
|
||||
const clientSupportsManagerV4UI = computed<boolean>(() => {
|
||||
return getClientFeature<boolean>('supports_manager_v4_ui', false) === true
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset store state (useful for reconnection)
|
||||
*/
|
||||
function resetStore() {
|
||||
console.log('[FeatureFlags] Resetting store state')
|
||||
serverFlags.value = {}
|
||||
isReady.value = false
|
||||
// Note: We don't reset clientFlags as they're local
|
||||
// Increment version to invalidate any pending updates
|
||||
updateVersion++
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup store (for unmounting or preventing memory leaks)
|
||||
*/
|
||||
function cleanup() {
|
||||
console.log('[FeatureFlags] Cleaning up store')
|
||||
resetStore()
|
||||
clientFlags.value = {}
|
||||
// Cancel any pending nextTick callbacks by incrementing version
|
||||
updateVersion = Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
|
||||
return {
|
||||
serverFlags,
|
||||
clientFlags,
|
||||
isReady,
|
||||
updateServerFlags,
|
||||
updateClientFlags,
|
||||
getServerFeature,
|
||||
serverSupportsFeature,
|
||||
getClientFeature,
|
||||
supportsManagerV4,
|
||||
clientSupportsManagerV4UI,
|
||||
resetStore,
|
||||
cleanup
|
||||
}
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, readonly } from 'vue'
|
||||
import { computed, readonly, watchEffect } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useFeatureFlagsStore } from '@/stores/featureFlagsStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
export enum ManagerUIState {
|
||||
@@ -14,27 +14,17 @@ export enum ManagerUIState {
|
||||
export const useManagerStateStore = defineStore('managerState', () => {
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const extensionStore = useExtensionStore()
|
||||
const featureFlagsStore = useFeatureFlagsStore()
|
||||
|
||||
// Reactive computed manager state that updates when dependencies change
|
||||
const managerUIState = computed(() => {
|
||||
const systemStats = systemStatsStore.systemStats
|
||||
const clientSupportsV4 =
|
||||
api.getClientFeatureFlags().supports_manager_v4_ui ?? false
|
||||
const clientSupportsV4 = featureFlagsStore.clientSupportsManagerV4UI
|
||||
const hasLegacyManager = extensionStore.extensions.some(
|
||||
(ext) => ext.name === 'Comfy.CustomNodesManager'
|
||||
)
|
||||
|
||||
const serverSupportsV4 = api.getServerFeature(
|
||||
'extension.manager.supports_v4'
|
||||
)
|
||||
|
||||
console.log('[Manager State Debug]', {
|
||||
systemStats: systemStats?.system?.argv,
|
||||
clientSupportsV4,
|
||||
serverSupportsV4,
|
||||
hasLegacyManager,
|
||||
extensions: extensionStore.extensions.map((e) => e.name)
|
||||
})
|
||||
const serverSupportsV4 = featureFlagsStore.supportsManagerV4
|
||||
|
||||
// Check command line args first
|
||||
if (systemStats?.system?.argv?.includes('--disable-manager')) {
|
||||
@@ -55,21 +45,44 @@ export const useManagerStateStore = defineStore('managerState', () => {
|
||||
return ManagerUIState.LEGACY_UI
|
||||
}
|
||||
|
||||
// No server v4 support but legacy manager extension exists = LEGACY_UI
|
||||
if (hasLegacyManager) {
|
||||
return ManagerUIState.LEGACY_UI
|
||||
}
|
||||
|
||||
// If server feature flags haven't loaded yet, return DISABLED for now
|
||||
// This will update reactively once feature flags load
|
||||
if (serverSupportsV4 === undefined) {
|
||||
if (!featureFlagsStore.isReady || serverSupportsV4 === undefined) {
|
||||
return ManagerUIState.DISABLED
|
||||
}
|
||||
|
||||
// Server explicitly doesn't support v4 (false) = assume legacy manager exists
|
||||
// OR legacy manager extension is detected
|
||||
if (serverSupportsV4 === false || hasLegacyManager) {
|
||||
return ManagerUIState.LEGACY_UI
|
||||
}
|
||||
|
||||
// No manager at all = DISABLED
|
||||
return ManagerUIState.DISABLED
|
||||
})
|
||||
|
||||
// Debug logging in development mode only
|
||||
if (import.meta.env.DEV) {
|
||||
watchEffect(() => {
|
||||
const systemStats = systemStatsStore.systemStats
|
||||
const clientSupportsV4 = featureFlagsStore.clientSupportsManagerV4UI
|
||||
const serverSupportsV4 = featureFlagsStore.supportsManagerV4
|
||||
const hasLegacyManager = extensionStore.extensions.some(
|
||||
(ext) => ext.name === 'Comfy.CustomNodesManager'
|
||||
)
|
||||
|
||||
console.log('[Manager State Debug]', {
|
||||
currentState: managerUIState.value,
|
||||
systemStats: systemStats?.system?.argv,
|
||||
clientSupportsV4,
|
||||
serverSupportsV4,
|
||||
hasLegacyManager,
|
||||
isReady: featureFlagsStore.isReady,
|
||||
extensions: extensionStore.extensions.map((e) => e.name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
managerUIState: readonly(managerUIState)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { isReactive, isReadonly } from 'vue'
|
||||
|
||||
@@ -5,18 +6,31 @@ import {
|
||||
ServerFeatureFlag,
|
||||
useFeatureFlags
|
||||
} from '@/composables/useFeatureFlags'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useFeatureFlagsStore } from '@/stores/featureFlagsStore'
|
||||
|
||||
// Mock the API module
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getServerFeature: vi.fn()
|
||||
}
|
||||
// Mock the store module
|
||||
vi.mock('@/stores/featureFlagsStore', () => ({
|
||||
useFeatureFlagsStore: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useFeatureFlags', () => {
|
||||
let mockStore: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Create mock store
|
||||
mockStore = {
|
||||
getServerFeature: vi.fn(),
|
||||
getClientFeature: vi.fn(),
|
||||
serverSupportsFeature: vi.fn(),
|
||||
supportsManagerV4: false,
|
||||
clientSupportsManagerV4UI: false,
|
||||
isReady: false
|
||||
}
|
||||
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue(mockStore)
|
||||
})
|
||||
|
||||
describe('flags object', () => {
|
||||
@@ -28,69 +42,74 @@ describe('useFeatureFlags', () => {
|
||||
})
|
||||
|
||||
it('should access supportsPreviewMetadata', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
|
||||
return true as any
|
||||
mockStore.getServerFeature.mockImplementation(
|
||||
(path: string, defaultValue?: any) => {
|
||||
if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA) return true
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.supportsPreviewMetadata).toBe(true)
|
||||
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||
ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA
|
||||
expect(flags.value.supportsPreviewMetadata).toBe(true)
|
||||
expect(mockStore.getServerFeature).toHaveBeenCalledWith(
|
||||
ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA,
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('should access maxUploadSize', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
return 209715200 as any // 200MB
|
||||
mockStore.getServerFeature.mockImplementation(
|
||||
(path: string, defaultValue?: any) => {
|
||||
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) return 209715200 // 200MB
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.maxUploadSize).toBe(209715200)
|
||||
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||
expect(flags.value.maxUploadSize).toBe(209715200)
|
||||
expect(mockStore.getServerFeature).toHaveBeenCalledWith(
|
||||
ServerFeatureFlag.MAX_UPLOAD_SIZE
|
||||
)
|
||||
})
|
||||
|
||||
it('should access supportsManagerV4', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === ServerFeatureFlag.MANAGER_SUPPORTS_V4) return true as any
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
mockStore.supportsManagerV4 = true
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.supportsManagerV4).toBe(true)
|
||||
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||
ServerFeatureFlag.MANAGER_SUPPORTS_V4
|
||||
)
|
||||
expect(flags.value.supportsManagerV4).toBe(true)
|
||||
})
|
||||
|
||||
it('should return undefined when features are not available and no default provided', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(_path, defaultValue) => defaultValue as any
|
||||
it('should access clientSupportsManagerV4UI', () => {
|
||||
mockStore.clientSupportsManagerV4UI = true
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.value.clientSupportsManagerV4UI).toBe(true)
|
||||
})
|
||||
|
||||
it('should access isReady state', () => {
|
||||
mockStore.isReady = true
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.value.isReady).toBe(true)
|
||||
})
|
||||
|
||||
it('should return default values when features are not available', () => {
|
||||
mockStore.getServerFeature.mockImplementation(
|
||||
(_path: string, defaultValue?: any) => defaultValue
|
||||
)
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.supportsPreviewMetadata).toBeUndefined()
|
||||
expect(flags.maxUploadSize).toBeUndefined()
|
||||
expect(flags.supportsManagerV4).toBeUndefined()
|
||||
expect(flags.value.supportsPreviewMetadata).toBe(false) // default value is false
|
||||
expect(flags.value.maxUploadSize).toBeUndefined()
|
||||
expect(flags.value.supportsManagerV4).toBe(false) // store mock returns false
|
||||
})
|
||||
})
|
||||
|
||||
describe('featureFlag', () => {
|
||||
it('should create reactive computed for custom feature flags', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === 'custom.feature') return 'custom-value' as any
|
||||
mockStore.getServerFeature.mockImplementation(
|
||||
(path: string, defaultValue?: any) => {
|
||||
if (path === 'custom.feature') return 'custom-value'
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
@@ -99,16 +118,16 @@ describe('useFeatureFlags', () => {
|
||||
const customFlag = featureFlag('custom.feature', 'default')
|
||||
|
||||
expect(customFlag.value).toBe('custom-value')
|
||||
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||
expect(mockStore.getServerFeature).toHaveBeenCalledWith(
|
||||
'custom.feature',
|
||||
'default'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle nested paths', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === 'extension.custom.nested.feature') return true as any
|
||||
mockStore.getServerFeature.mockImplementation(
|
||||
(path: string, defaultValue?: any) => {
|
||||
if (path === 'extension.custom.nested.feature') return true
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
@@ -120,10 +139,9 @@ describe('useFeatureFlags', () => {
|
||||
})
|
||||
|
||||
it('should work with ServerFeatureFlag enum', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||
return 104857600 as any
|
||||
mockStore.getServerFeature.mockImplementation(
|
||||
(path: string, defaultValue?: any) => {
|
||||
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) return 104857600
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
@@ -134,4 +152,32 @@ describe('useFeatureFlags', () => {
|
||||
expect(maxUploadSize.value).toBe(104857600)
|
||||
})
|
||||
})
|
||||
|
||||
describe('serverSupportsFeature', () => {
|
||||
it('should create reactive computed for feature support check', () => {
|
||||
mockStore.serverSupportsFeature.mockImplementation(
|
||||
(path: string) => path === 'supported.feature'
|
||||
)
|
||||
|
||||
const { serverSupportsFeature } = useFeatureFlags()
|
||||
const isSupported = serverSupportsFeature('supported.feature')
|
||||
|
||||
expect(isSupported.value).toBe(true)
|
||||
expect(mockStore.serverSupportsFeature).toHaveBeenCalledWith(
|
||||
'supported.feature'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('direct store methods', () => {
|
||||
it('should expose getServerFeature method', () => {
|
||||
const { getServerFeature } = useFeatureFlags()
|
||||
expect(getServerFeature).toBe(mockStore.getServerFeature)
|
||||
})
|
||||
|
||||
it('should expose getClientFeature method', () => {
|
||||
const { getClientFeature } = useFeatureFlags()
|
||||
expect(getClientFeature).toBe(mockStore.getClientFeature)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useFeatureFlagsStore } from '@/stores/featureFlagsStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
@@ -11,18 +10,8 @@ import {
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getClientFeatureFlags: vi.fn(),
|
||||
getServerFeature: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
||||
useFeatureFlags: vi.fn(() => ({
|
||||
flags: { supportsManagerV4: false },
|
||||
featureFlag: vi.fn()
|
||||
}))
|
||||
vi.mock('@/stores/featureFlagsStore', () => ({
|
||||
useFeatureFlagsStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/extensionStore', () => ({
|
||||
@@ -46,7 +35,11 @@ describe('useManagerStateStore', () => {
|
||||
system: { argv: ['python', 'main.py', '--disable-manager'] }
|
||||
}
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue({
|
||||
clientSupportsManagerV4UI: false,
|
||||
supportsManagerV4: false,
|
||||
isReady: false
|
||||
} as any)
|
||||
vi.mocked(useExtensionStore).mockReturnValue({
|
||||
extensions: []
|
||||
} as any)
|
||||
@@ -62,7 +55,11 @@ describe('useManagerStateStore', () => {
|
||||
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
|
||||
}
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue({
|
||||
clientSupportsManagerV4UI: false,
|
||||
supportsManagerV4: false,
|
||||
isReady: false
|
||||
} as any)
|
||||
vi.mocked(useExtensionStore).mockReturnValue({
|
||||
extensions: []
|
||||
} as any)
|
||||
@@ -76,13 +73,10 @@ describe('useManagerStateStore', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: { system: { argv: ['python', 'main.py'] } }
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
supports_manager_v4_ui: true
|
||||
})
|
||||
vi.mocked(api.getServerFeature).mockReturnValue(true)
|
||||
vi.mocked(useFeatureFlags).mockReturnValue({
|
||||
flags: { supportsManagerV4: true },
|
||||
featureFlag: vi.fn()
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue({
|
||||
clientSupportsManagerV4UI: true,
|
||||
supportsManagerV4: true,
|
||||
isReady: true
|
||||
} as any)
|
||||
vi.mocked(useExtensionStore).mockReturnValue({
|
||||
extensions: []
|
||||
@@ -97,13 +91,10 @@ describe('useManagerStateStore', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: { system: { argv: ['python', 'main.py'] } }
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
supports_manager_v4_ui: false
|
||||
})
|
||||
vi.mocked(api.getServerFeature).mockReturnValue(true)
|
||||
vi.mocked(useFeatureFlags).mockReturnValue({
|
||||
flags: { supportsManagerV4: true },
|
||||
featureFlag: vi.fn()
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue({
|
||||
clientSupportsManagerV4UI: false,
|
||||
supportsManagerV4: true,
|
||||
isReady: true
|
||||
} as any)
|
||||
vi.mocked(useExtensionStore).mockReturnValue({
|
||||
extensions: []
|
||||
@@ -118,10 +109,10 @@ describe('useManagerStateStore', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: { system: { argv: ['python', 'main.py'] } }
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
|
||||
vi.mocked(useFeatureFlags).mockReturnValue({
|
||||
flags: { supportsManagerV4: false },
|
||||
featureFlag: vi.fn()
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue({
|
||||
clientSupportsManagerV4UI: false,
|
||||
supportsManagerV4: false,
|
||||
isReady: true
|
||||
} as any)
|
||||
vi.mocked(useExtensionStore).mockReturnValue({
|
||||
extensions: [{ name: 'Comfy.CustomNodesManager' }]
|
||||
@@ -136,11 +127,10 @@ describe('useManagerStateStore', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: { system: { argv: ['python', 'main.py'] } }
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
|
||||
vi.mocked(api.getServerFeature).mockReturnValue(undefined)
|
||||
vi.mocked(useFeatureFlags).mockReturnValue({
|
||||
flags: { supportsManagerV4: undefined },
|
||||
featureFlag: vi.fn()
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue({
|
||||
clientSupportsManagerV4UI: false,
|
||||
supportsManagerV4: undefined,
|
||||
isReady: true
|
||||
} as any)
|
||||
vi.mocked(useExtensionStore).mockReturnValue({
|
||||
extensions: []
|
||||
@@ -155,11 +145,10 @@ describe('useManagerStateStore', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: { system: { argv: ['python', 'main.py'] } }
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
|
||||
vi.mocked(api.getServerFeature).mockReturnValue(false)
|
||||
vi.mocked(useFeatureFlags).mockReturnValue({
|
||||
flags: { supportsManagerV4: false },
|
||||
featureFlag: vi.fn()
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue({
|
||||
clientSupportsManagerV4UI: false,
|
||||
supportsManagerV4: false,
|
||||
isReady: true
|
||||
} as any)
|
||||
vi.mocked(useExtensionStore).mockReturnValue({
|
||||
extensions: []
|
||||
@@ -174,13 +163,10 @@ describe('useManagerStateStore', () => {
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue({
|
||||
systemStats: null
|
||||
} as any)
|
||||
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
|
||||
supports_manager_v4_ui: true
|
||||
})
|
||||
vi.mocked(api.getServerFeature).mockReturnValue(true)
|
||||
vi.mocked(useFeatureFlags).mockReturnValue({
|
||||
flags: { supportsManagerV4: true },
|
||||
featureFlag: vi.fn()
|
||||
vi.mocked(useFeatureFlagsStore).mockReturnValue({
|
||||
clientSupportsManagerV4UI: true,
|
||||
supportsManagerV4: true,
|
||||
isReady: true
|
||||
} as any)
|
||||
vi.mocked(useExtensionStore).mockReturnValue({
|
||||
extensions: []
|
||||
|
||||
Reference in New Issue
Block a user