mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 02:02:08 +00:00
[feat] Add reactive feature flags foundation (#4817)
This commit is contained in:
36
src/composables/useFeatureFlags.ts
Normal file
36
src/composables/useFeatureFlags.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { computed, reactive, readonly } from 'vue'
|
||||||
|
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known server feature flags (top-level, not extensions)
|
||||||
|
*/
|
||||||
|
export enum ServerFeatureFlag {
|
||||||
|
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
|
||||||
|
MAX_UPLOAD_SIZE = 'max_upload_size'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a reactive computed for any feature flag
|
||||||
|
const featureFlag = <T = unknown>(featurePath: string, defaultValue?: T) => {
|
||||||
|
return computed(() => api.getServerFeature(featurePath, defaultValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
flags: readonly(flags),
|
||||||
|
featureFlag
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import get from 'lodash/get'
|
||||||
|
|
||||||
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
|
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
|
||||||
import type {
|
import type {
|
||||||
@@ -1082,21 +1083,21 @@ export class ComfyApi extends EventTarget {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the server supports a specific feature.
|
* Checks if the server supports a specific feature.
|
||||||
* @param featureName The name of the feature to check
|
* @param featureName The name of the feature to check (supports dot notation for nested values)
|
||||||
* @returns true if the feature is supported, false otherwise
|
* @returns true if the feature is supported, false otherwise
|
||||||
*/
|
*/
|
||||||
serverSupportsFeature(featureName: string): boolean {
|
serverSupportsFeature(featureName: string): boolean {
|
||||||
return this.serverFeatureFlags[featureName] === true
|
return get(this.serverFeatureFlags, featureName) === true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a server feature flag value.
|
* Gets a server feature flag value.
|
||||||
* @param featureName The name of the feature to get
|
* @param featureName The name of the feature to get (supports dot notation for nested values)
|
||||||
* @param defaultValue The default value if the feature is not found
|
* @param defaultValue The default value if the feature is not found
|
||||||
* @returns The feature value or default
|
* @returns The feature value or default
|
||||||
*/
|
*/
|
||||||
getServerFeature<T = unknown>(featureName: string, defaultValue?: T): T {
|
getServerFeature<T = unknown>(featureName: string, defaultValue?: T): T {
|
||||||
return (this.serverFeatureFlags[featureName] ?? defaultValue) as T
|
return get(this.serverFeatureFlags, featureName, defaultValue) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
121
tests-ui/tests/composables/useFeatureFlags.test.ts
Normal file
121
tests-ui/tests/composables/useFeatureFlags.test.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { isReactive, isReadonly } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ServerFeatureFlag,
|
||||||
|
useFeatureFlags
|
||||||
|
} from '@/composables/useFeatureFlags'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
|
||||||
|
// Mock the API module
|
||||||
|
vi.mock('@/scripts/api', () => ({
|
||||||
|
api: {
|
||||||
|
getServerFeature: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useFeatureFlags', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('flags object', () => {
|
||||||
|
it('should provide reactive readonly flags', () => {
|
||||||
|
const { flags } = useFeatureFlags()
|
||||||
|
|
||||||
|
expect(isReadonly(flags)).toBe(true)
|
||||||
|
expect(isReactive(flags)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should access supportsPreviewMetadata', () => {
|
||||||
|
vi.mocked(api.getServerFeature).mockImplementation(
|
||||||
|
(path, defaultValue) => {
|
||||||
|
if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
|
||||||
|
return true as any
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { flags } = useFeatureFlags()
|
||||||
|
expect(flags.supportsPreviewMetadata).toBe(true)
|
||||||
|
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||||
|
ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should access maxUploadSize', () => {
|
||||||
|
vi.mocked(api.getServerFeature).mockImplementation(
|
||||||
|
(path, defaultValue) => {
|
||||||
|
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||||
|
return 209715200 as any // 200MB
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { flags } = useFeatureFlags()
|
||||||
|
expect(flags.maxUploadSize).toBe(209715200)
|
||||||
|
expect(api.getServerFeature).toHaveBeenCalledWith(
|
||||||
|
ServerFeatureFlag.MAX_UPLOAD_SIZE
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined when features are not available and no default provided', () => {
|
||||||
|
vi.mocked(api.getServerFeature).mockImplementation(
|
||||||
|
(_path, defaultValue) => defaultValue as any
|
||||||
|
)
|
||||||
|
|
||||||
|
const { flags } = useFeatureFlags()
|
||||||
|
expect(flags.supportsPreviewMetadata).toBeUndefined()
|
||||||
|
expect(flags.maxUploadSize).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { featureFlag } = useFeatureFlags()
|
||||||
|
const customFlag = featureFlag('custom.feature', 'default')
|
||||||
|
|
||||||
|
expect(customFlag.value).toBe('custom-value')
|
||||||
|
expect(api.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
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { featureFlag } = useFeatureFlags()
|
||||||
|
const nestedFlag = featureFlag('extension.custom.nested.feature', false)
|
||||||
|
|
||||||
|
expect(nestedFlag.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with ServerFeatureFlag enum', () => {
|
||||||
|
vi.mocked(api.getServerFeature).mockImplementation(
|
||||||
|
(path, defaultValue) => {
|
||||||
|
if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||||
|
return 104857600 as any
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { featureFlag } = useFeatureFlags()
|
||||||
|
const maxUploadSize = featureFlag(ServerFeatureFlag.MAX_UPLOAD_SIZE)
|
||||||
|
|
||||||
|
expect(maxUploadSize.value).toBe(104857600)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user