From 3678e65becd302411f1e05f4629ad3d6446a6f52 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 21 Feb 2026 21:40:45 -0800 Subject: [PATCH] feat: add feature flag to disable Essentials tab in node library (#9067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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** image **Enabled UI** image ## 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 --- .../v2/NodeSearchCategorySidebar.vue | 4 +- .../sidebar/tabs/NodeLibrarySidebarTabV2.vue | 40 +++++++++++++++---- src/composables/useFeatureFlags.ts | 14 ++++++- src/platform/remoteConfig/types.ts | 1 + 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/components/searchbox/v2/NodeSearchCategorySidebar.vue b/src/components/searchbox/v2/NodeSearchCategorySidebar.vue index 0b0fb7aa4..fad96faf6 100644 --- a/src/components/searchbox/v2/NodeSearchCategorySidebar.vue +++ b/src/components/searchbox/v2/NodeSearchCategorySidebar.vue @@ -53,6 +53,7 @@ import NodeSearchCategoryTreeNode, { CATEGORY_UNSELECTED_CLASS } 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 { useNodeDefStore } from '@/stores/nodeDefStore' import { NodeSourceType } from '@/types/nodeSource' @@ -64,6 +65,7 @@ const selectedCategory = defineModel('selectedCategory', { }) const { t } = useI18n() +const { flags } = useFeatureFlags() const nodeDefStore = useNodeDefStore() const topCategories = computed(() => [ @@ -79,7 +81,7 @@ const hasEssentialNodes = computed(() => const sourceCategories = computed(() => { const categories = [] - if (hasEssentialNodes.value) { + if (flags.nodeLibraryEssentialsEnabled && hasEssentialNodes.value) { categories.push({ id: 'essentials', label: t('g.essentials') }) } categories.push({ id: 'custom', label: t('g.custom') }) diff --git a/src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue b/src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue index ec521103e..a86947df6 100644 --- a/src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue +++ b/src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue @@ -52,7 +52,7 @@ :value="tab.value" :class=" 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', selectedTab === tab.value ? 'bg-comfy-input font-bold' @@ -70,7 +70,9 @@ ( 'Comfy.NodeLibrary.Tab', DEFAULT_TAB_ID ) +watchEffect(() => { + if ( + !flags.nodeLibraryEssentialsEnabled && + selectedTab.value === 'essentials' + ) { + selectedTab.value = DEFAULT_TAB_ID + } +}) + const sortOrderByTab = useLocalStorage>( 'Comfy.NodeLibrary.SortByTab', { @@ -324,11 +338,21 @@ async function handleSearch() { expandedKeys.value = allKeys } -const tabs = computed(() => [ - { value: 'essentials', label: t('sideToolbar.nodeLibraryTab.essentials') }, - { value: 'all', label: t('sideToolbar.nodeLibraryTab.allNodes') }, - { value: 'custom', label: t('sideToolbar.nodeLibraryTab.custom') } -]) +const tabs = computed(() => { + const baseTabs: Array<{ value: TabId; label: string }> = [ + { value: 'all', label: t('sideToolbar.nodeLibraryTab.allNodes') }, + { value: 'custom', label: t('sideToolbar.nodeLibraryTab.custom') } + ] + return flags.nodeLibraryEssentialsEnabled + ? [ + { + value: 'essentials' as TabId, + label: t('sideToolbar.nodeLibraryTab.essentials') + }, + ...baseTabs + ] + : baseTabs +}) onMounted(() => { searchBoxRef.value?.focus() diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 2f2d7059f..88e48a9b2 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -22,7 +22,8 @@ export enum ServerFeatureFlag { LINEAR_TOGGLE_ENABLED = 'linear_toggle_enabled', TEAM_WORKSPACES_ENABLED = 'team_workspaces_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() { 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 + ) + ) } }) diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts index 28316cf0c..6dd64feb3 100644 --- a/src/platform/remoteConfig/types.ts +++ b/src/platform/remoteConfig/types.ts @@ -43,4 +43,5 @@ export type RemoteConfig = { linear_toggle_enabled?: boolean team_workspaces_enabled?: boolean user_secrets_enabled?: boolean + node_library_essentials_enabled?: boolean }