mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +00:00
feat: add feature flag to disable Essentials tab in node library (#9067)
## Summary Add a `node_library_essentials_enabled` feature flag to gate the Essentials tab, allowing the rest of the new node library to ship while the Essentials tab is finalized. ## Changes - **What**: New feature flag (`node_library_essentials_enabled`) that hides the Essentials tab in the node library sidebar and search category sidebar. Defaults to `true` in dev/nightly builds, `false` in production. Overridable via remote config or server feature flags. Falls back to the "All" tab if a user previously had Essentials selected. **Disabled UI** <img width="547" height="782" alt="image" src="https://github.com/user-attachments/assets/bcfcecd4-cbae-4d7b-9bcc-64bdf57929e2" /> **Enabled UI** <img width="547" height="782" alt="image" src="https://github.com/user-attachments/assets/0fb030ea-3bde-475e-982b-45e8f190cb8f" /> ## Review Focus - Feature flag pattern follows existing conventions (e.g. `linearToggleEnabled`) - Fallback behavior when essentials tab was previously selected by user ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9067-feat-add-feature-flag-to-disable-Essentials-tab-in-node-library-30e6d73d36508103b3cad9fc5d260611) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -53,6 +53,7 @@ import NodeSearchCategoryTreeNode, {
|
|||||||
CATEGORY_UNSELECTED_CLASS
|
CATEGORY_UNSELECTED_CLASS
|
||||||
} from '@/components/searchbox/v2/NodeSearchCategoryTreeNode.vue'
|
} from '@/components/searchbox/v2/NodeSearchCategoryTreeNode.vue'
|
||||||
import type { CategoryNode } from '@/components/searchbox/v2/NodeSearchCategoryTreeNode.vue'
|
import type { CategoryNode } from '@/components/searchbox/v2/NodeSearchCategoryTreeNode.vue'
|
||||||
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||||
import { nodeOrganizationService } from '@/services/nodeOrganizationService'
|
import { nodeOrganizationService } from '@/services/nodeOrganizationService'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { NodeSourceType } from '@/types/nodeSource'
|
import { NodeSourceType } from '@/types/nodeSource'
|
||||||
@@ -64,6 +65,7 @@ const selectedCategory = defineModel<string>('selectedCategory', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { flags } = useFeatureFlags()
|
||||||
const nodeDefStore = useNodeDefStore()
|
const nodeDefStore = useNodeDefStore()
|
||||||
|
|
||||||
const topCategories = computed(() => [
|
const topCategories = computed(() => [
|
||||||
@@ -79,7 +81,7 @@ const hasEssentialNodes = computed(() =>
|
|||||||
|
|
||||||
const sourceCategories = computed(() => {
|
const sourceCategories = computed(() => {
|
||||||
const categories = []
|
const categories = []
|
||||||
if (hasEssentialNodes.value) {
|
if (flags.nodeLibraryEssentialsEnabled && hasEssentialNodes.value) {
|
||||||
categories.push({ id: 'essentials', label: t('g.essentials') })
|
categories.push({ id: 'essentials', label: t('g.essentials') })
|
||||||
}
|
}
|
||||||
categories.push({ id: 'custom', label: t('g.custom') })
|
categories.push({ id: 'custom', label: t('g.custom') })
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
:value="tab.value"
|
:value="tab.value"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'select-none border-none outline-none px-3 py-2 rounded-lg cursor-pointer',
|
'flex-1 text-center select-none border-none outline-none px-3 py-2 rounded-lg cursor-pointer',
|
||||||
'text-sm text-foreground transition-colors',
|
'text-sm text-foreground transition-colors',
|
||||||
selectedTab === tab.value
|
selectedTab === tab.value
|
||||||
? 'bg-comfy-input font-bold'
|
? 'bg-comfy-input font-bold'
|
||||||
@@ -70,7 +70,9 @@
|
|||||||
<!-- Tab content (scrollable) -->
|
<!-- Tab content (scrollable) -->
|
||||||
<TabsRoot v-model="selectedTab" class="h-full">
|
<TabsRoot v-model="selectedTab" class="h-full">
|
||||||
<EssentialNodesPanel
|
<EssentialNodesPanel
|
||||||
v-if="selectedTab === 'essentials'"
|
v-if="
|
||||||
|
flags.nodeLibraryEssentialsEnabled && selectedTab === 'essentials'
|
||||||
|
"
|
||||||
v-model:expanded-keys="expandedKeys"
|
v-model:expanded-keys="expandedKeys"
|
||||||
:root="renderedEssentialRoot"
|
:root="renderedEssentialRoot"
|
||||||
@node-click="handleNodeClick"
|
@node-click="handleNodeClick"
|
||||||
@@ -109,10 +111,11 @@ import {
|
|||||||
TabsRoot,
|
TabsRoot,
|
||||||
TabsTrigger
|
TabsTrigger
|
||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import SearchBox from '@/components/common/SearchBoxV2.vue'
|
import SearchBox from '@/components/common/SearchBoxV2.vue'
|
||||||
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||||
import { useNodeDragToCanvas } from '@/composables/node/useNodeDragToCanvas'
|
import { useNodeDragToCanvas } from '@/composables/node/useNodeDragToCanvas'
|
||||||
import { usePerTabState } from '@/composables/usePerTabState'
|
import { usePerTabState } from '@/composables/usePerTabState'
|
||||||
import {
|
import {
|
||||||
@@ -136,11 +139,22 @@ import EssentialNodesPanel from './nodeLibrary/EssentialNodesPanel.vue'
|
|||||||
import NodeDragPreview from './nodeLibrary/NodeDragPreview.vue'
|
import NodeDragPreview from './nodeLibrary/NodeDragPreview.vue'
|
||||||
import SidebarTabTemplate from './SidebarTabTemplate.vue'
|
import SidebarTabTemplate from './SidebarTabTemplate.vue'
|
||||||
|
|
||||||
|
const { flags } = useFeatureFlags()
|
||||||
|
|
||||||
const selectedTab = useLocalStorage<TabId>(
|
const selectedTab = useLocalStorage<TabId>(
|
||||||
'Comfy.NodeLibrary.Tab',
|
'Comfy.NodeLibrary.Tab',
|
||||||
DEFAULT_TAB_ID
|
DEFAULT_TAB_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (
|
||||||
|
!flags.nodeLibraryEssentialsEnabled &&
|
||||||
|
selectedTab.value === 'essentials'
|
||||||
|
) {
|
||||||
|
selectedTab.value = DEFAULT_TAB_ID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const sortOrderByTab = useLocalStorage<Record<TabId, SortingStrategyId>>(
|
const sortOrderByTab = useLocalStorage<Record<TabId, SortingStrategyId>>(
|
||||||
'Comfy.NodeLibrary.SortByTab',
|
'Comfy.NodeLibrary.SortByTab',
|
||||||
{
|
{
|
||||||
@@ -324,11 +338,21 @@ async function handleSearch() {
|
|||||||
expandedKeys.value = allKeys
|
expandedKeys.value = allKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = computed(() => [
|
const tabs = computed(() => {
|
||||||
{ value: 'essentials', label: t('sideToolbar.nodeLibraryTab.essentials') },
|
const baseTabs: Array<{ value: TabId; label: string }> = [
|
||||||
{ value: 'all', label: t('sideToolbar.nodeLibraryTab.allNodes') },
|
{ value: 'all', label: t('sideToolbar.nodeLibraryTab.allNodes') },
|
||||||
{ value: 'custom', label: t('sideToolbar.nodeLibraryTab.custom') }
|
{ value: 'custom', label: t('sideToolbar.nodeLibraryTab.custom') }
|
||||||
])
|
]
|
||||||
|
return flags.nodeLibraryEssentialsEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: 'essentials' as TabId,
|
||||||
|
label: t('sideToolbar.nodeLibraryTab.essentials')
|
||||||
|
},
|
||||||
|
...baseTabs
|
||||||
|
]
|
||||||
|
: baseTabs
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
searchBoxRef.value?.focus()
|
searchBoxRef.value?.focus()
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export enum ServerFeatureFlag {
|
|||||||
LINEAR_TOGGLE_ENABLED = 'linear_toggle_enabled',
|
LINEAR_TOGGLE_ENABLED = 'linear_toggle_enabled',
|
||||||
TEAM_WORKSPACES_ENABLED = 'team_workspaces_enabled',
|
TEAM_WORKSPACES_ENABLED = 'team_workspaces_enabled',
|
||||||
USER_SECRETS_ENABLED = 'user_secrets_enabled',
|
USER_SECRETS_ENABLED = 'user_secrets_enabled',
|
||||||
NODE_REPLACEMENTS = 'node_replacements'
|
NODE_REPLACEMENTS = 'node_replacements',
|
||||||
|
NODE_LIBRARY_ESSENTIALS_ENABLED = 'node_library_essentials_enabled'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +119,17 @@ export function useFeatureFlags() {
|
|||||||
},
|
},
|
||||||
get nodeReplacementsEnabled() {
|
get nodeReplacementsEnabled() {
|
||||||
return api.getServerFeature(ServerFeatureFlag.NODE_REPLACEMENTS, false)
|
return api.getServerFeature(ServerFeatureFlag.NODE_REPLACEMENTS, false)
|
||||||
|
},
|
||||||
|
get nodeLibraryEssentialsEnabled() {
|
||||||
|
if (isNightly || import.meta.env.DEV) return true
|
||||||
|
|
||||||
|
return (
|
||||||
|
remoteConfig.value.node_library_essentials_enabled ??
|
||||||
|
api.getServerFeature(
|
||||||
|
ServerFeatureFlag.NODE_LIBRARY_ESSENTIALS_ENABLED,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -43,4 +43,5 @@ export type RemoteConfig = {
|
|||||||
linear_toggle_enabled?: boolean
|
linear_toggle_enabled?: boolean
|
||||||
team_workspaces_enabled?: boolean
|
team_workspaces_enabled?: boolean
|
||||||
user_secrets_enabled?: boolean
|
user_secrets_enabled?: boolean
|
||||||
|
node_library_essentials_enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user