Compare commits

...

4 Commits

6 changed files with 129 additions and 4 deletions

View File

@@ -596,8 +596,7 @@ const coordinateNavAndSort = (source: 'nav' | 'sort') => {
}
}
// Watch for changes from the two sources ('nav' and 'sort') and trigger the coordinator.
watch(selectedNavItem, () => coordinateNavAndSort('nav'))
watch(selectedNavItem, () => coordinateNavAndSort('nav'), { immediate: true })
watch(sortBy, () => coordinateNavAndSort('sort'))
// Convert between string array and object array for MultiSelect component

View File

@@ -219,5 +219,66 @@ describe('useFeatureFlags', () => {
const { flags } = useFeatureFlags()
expect(flags.teamWorkspacesEnabled).toBe(true)
})
it('newUserDefaultTemplateTab returns dev override value', () => {
localStorage.setItem(
'ff:new_user_default_template_tab',
JSON.stringify('popular')
)
const { flags } = useFeatureFlags()
expect(flags.newUserDefaultTemplateTab).toBe('popular')
})
it('newUserDefaultTemplateTab trims whitespace', () => {
localStorage.setItem(
'ff:new_user_default_template_tab',
JSON.stringify(' popular ')
)
const { flags } = useFeatureFlags()
expect(flags.newUserDefaultTemplateTab).toBe('popular')
})
it.each(['', ' ', '\t\n'])(
'newUserDefaultTemplateTab normalizes %j to undefined',
(raw) => {
localStorage.setItem(
'ff:new_user_default_template_tab',
JSON.stringify(raw)
)
const { flags } = useFeatureFlags()
expect(flags.newUserDefaultTemplateTab).toBeUndefined()
}
)
it('newUserDefaultTemplateTab returns undefined when unset', () => {
vi.mocked(api.getServerFeature).mockImplementation(
(_path, defaultValue) => defaultValue
)
const { flags } = useFeatureFlags()
expect(flags.newUserDefaultTemplateTab).toBeUndefined()
})
it('newUserDefaultTemplateTab falls through to server feature when remoteConfig is blank', async () => {
const { remoteConfig } =
await import('@/platform/remoteConfig/remoteConfig')
const previous = remoteConfig.value
remoteConfig.value = { new_user_default_template_tab: ' ' }
vi.mocked(api.getServerFeature).mockImplementation((path) => {
if (path === ServerFeatureFlag.NEW_USER_DEFAULT_TEMPLATE_TAB)
return 'popular'
return undefined
})
try {
const { flags } = useFeatureFlags()
expect(flags.newUserDefaultTemplateTab).toBe('popular')
} finally {
remoteConfig.value = previous
}
})
})
})

View File

@@ -27,7 +27,8 @@ export enum ServerFeatureFlag {
WORKFLOW_SHARING_ENABLED = 'workflow_sharing_enabled',
COMFYHUB_UPLOAD_ENABLED = 'comfyhub_upload_enabled',
COMFYHUB_PROFILE_GATE_ENABLED = 'comfyhub_profile_gate_enabled',
SHOW_SIGNIN_BUTTON = 'show_signin_button'
SHOW_SIGNIN_BUTTON = 'show_signin_button',
NEW_USER_DEFAULT_TEMPLATE_TAB = 'new_user_default_template_tab'
}
/**
@@ -163,6 +164,22 @@ export function useFeatureFlags() {
ServerFeatureFlag.SHOW_SIGNIN_BUTTON,
undefined
)
},
/**
* Template category id shown by default when the template selector
* opens for a new user during onboarding. Used for A/B testing the
* onboarding tab via PostHog feature flags. Returns `undefined` when
* unset so callers fall back to the built-in default.
*/
get newUserDefaultTemplateTab(): string | undefined {
const remote = remoteConfig.value.new_user_default_template_tab?.trim()
const value = resolveFlag<string | undefined>(
ServerFeatureFlag.NEW_USER_DEFAULT_TEMPLATE_TAB,
remote ? remote : undefined,
undefined
)
const normalized = value?.trim()
return normalized ? normalized : undefined
}
})

View File

@@ -16,6 +16,10 @@ const mockTelemetry = vi.hoisted(() => ({
trackTemplateLibraryOpened: vi.fn()
}))
const mockFlags = vi.hoisted(() => ({
newUserDefaultTemplateTab: undefined as string | undefined
}))
vi.mock('@/services/dialogService', () => ({
useDialogService: () => mockDialogService
}))
@@ -32,6 +36,10 @@ vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => mockTelemetry
}))
vi.mock('@/composables/useFeatureFlags', () => ({
useFeatureFlags: () => ({ flags: mockFlags })
}))
vi.mock(
'@/components/custom/widget/WorkflowTemplateSelectorDialog.vue',
() => ({
@@ -44,6 +52,7 @@ import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelector
describe('useWorkflowTemplateSelectorDialog', () => {
beforeEach(() => {
vi.clearAllMocks()
mockFlags.newUserDefaultTemplateTab = undefined
})
describe('show', () => {
@@ -92,6 +101,38 @@ describe('useWorkflowTemplateSelectorDialog', () => {
)
})
it('uses feature flag override for new users when set', () => {
mockNewUserService.isNewUser.mockReturnValue(true)
mockFlags.newUserDefaultTemplateTab = 'popular'
const dialog = useWorkflowTemplateSelectorDialog()
dialog.show()
expect(mockDialogService.showLayoutDialog).toHaveBeenCalledWith(
expect.objectContaining({
props: expect.objectContaining({
initialCategory: 'popular'
})
})
)
})
it('ignores feature flag override for non-new users', () => {
mockNewUserService.isNewUser.mockReturnValue(false)
mockFlags.newUserDefaultTemplateTab = 'popular'
const dialog = useWorkflowTemplateSelectorDialog()
dialog.show()
expect(mockDialogService.showLayoutDialog).toHaveBeenCalledWith(
expect.objectContaining({
props: expect.objectContaining({
initialCategory: 'all'
})
})
)
})
it('uses explicit initialCategory when provided', () => {
mockNewUserService.isNewUser.mockReturnValue(true)

View File

@@ -1,4 +1,5 @@
import WorkflowTemplateSelectorDialog from '@/components/custom/widget/WorkflowTemplateSelectorDialog.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useTelemetry } from '@/platform/telemetry'
import type { TemplateLibraryMetadata } from '@/platform/telemetry/types'
import { useDialogService } from '@/services/dialogService'
@@ -12,11 +13,16 @@ export const useWorkflowTemplateSelectorDialog = () => {
const dialogService = useDialogService()
const dialogStore = useDialogStore()
const newUserService = useNewUserService()
const { flags } = useFeatureFlags()
function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY })
}
function newUserDefaultCategory() {
return flags.newUserDefaultTemplateTab ?? GETTING_STARTED_CATEGORY_ID
}
function show(
source: TemplateLibraryMetadata['source'] = 'command',
options?: { initialCategory?: string; afterClose?: () => void }
@@ -25,7 +31,7 @@ export const useWorkflowTemplateSelectorDialog = () => {
const initialCategory =
options?.initialCategory ??
(newUserService.isNewUser() ? GETTING_STARTED_CATEGORY_ID : 'all')
(newUserService.isNewUser() ? newUserDefaultCategory() : 'all')
dialogService.showLayoutDialog({
key: DIALOG_KEY,

View File

@@ -104,4 +104,5 @@ export type RemoteConfig = {
comfyhub_upload_enabled?: boolean
comfyhub_profile_gate_enabled?: boolean
sentry_dsn?: string
new_user_default_template_tab?: string
}