From 337e0486eaeff4e68ef6d4311836d1bbd0eb88d3 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 20 Feb 2026 17:02:54 -0800 Subject: [PATCH] feat: client-side distribution filtering for blueprint subgraphs (#8686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds client-side filtering of blueprint subgraphs by distribution. **Changes:** - Added `includeOnDistributions` typed field to `GlobalSubgraphData` in `api.ts` - Distribution detection: `isCloud → 'cloud'`, `isDesktop → 'desktop'`, else `'localhost'` - Filters subgraphs before loading — excluded blueprints are never fetched **Depends on:** Comfy-Org/workflow_templates schema update (merge first) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8686-feat-client-side-distribution-filtering-for-blueprint-subgraphs-2ff6d73d365081d29f79c4e3cab174ac) by [Unito](https://www.unito.io) --- src/scripts/api.ts | 9 ++-- src/stores/subgraphStore.test.ts | 89 ++++++++++++++++++++++++++++++++ src/stores/subgraphStore.ts | 21 ++++++-- 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 324dc67ce..67357ea6d 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -11,9 +11,10 @@ import type { import { isCloud } from '@/platform/distribution/types' import { useToastStore } from '@/platform/updates/common/toastStore' import type { IFuseOptions } from 'fuse.js' -import { - type TemplateInfo, - type WorkflowTemplates +import type { + TemplateIncludeOnDistributionEnum, + TemplateInfo, + WorkflowTemplates } from '@/platform/workflow/templates/types/template' import type { ComfyApiWorkflow, @@ -241,6 +242,8 @@ export type GlobalSubgraphData = { node_pack: string category?: string search_aliases?: string[] + requiresCustomNodes?: string[] + includeOnDistributions?: TemplateIncludeOnDistributionEnum[] } data: string | Promise essentials_category?: string diff --git a/src/stores/subgraphStore.test.ts b/src/stores/subgraphStore.test.ts index 523823576..92457598d 100644 --- a/src/stores/subgraphStore.test.ts +++ b/src/stores/subgraphStore.test.ts @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema' import type { GlobalSubgraphData } from '@/scripts/api' import type { ExportedSubgraph } from '@/lib/litegraph/src/types/serialisation' +import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template' import { api } from '@/scripts/api' import { app as comfyApp } from '@/scripts/app' import { useLitegraphService } from '@/services/litegraphService' @@ -16,6 +17,12 @@ import { } from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers' import { createTestingPinia } from '@pinia/testing' +const mockDistributionTypes = vi.hoisted(() => ({ + isCloud: false, + isDesktop: false +})) +vi.mock('@/platform/distribution/types', () => mockDistributionTypes) + // Mock telemetry to break circular dependency (telemetry → workflowStore → app → telemetry) vi.mock('@/platform/telemetry', () => ({ useTelemetry: () => null @@ -85,6 +92,8 @@ describe('useSubgraphStore', () => { } beforeEach(() => { + mockDistributionTypes.isCloud = false + mockDistributionTypes.isDesktop = false setActivePinia(createTestingPinia({ stubActions: false })) store = useSubgraphStore() vi.clearAllMocks() @@ -305,4 +314,84 @@ describe('useSubgraphStore', () => { expect(subgraphExtra?.BlueprintSearchAliases).toBeUndefined() }) }) + + describe('global blueprint filtering', () => { + function globalBlueprint( + overrides: Partial = {} + ): GlobalSubgraphData { + return { + name: 'Filtered Blueprint', + info: { node_pack: 'test_pack', ...overrides }, + data: JSON.stringify(mockGraph) + } + } + + it('should exclude blueprints with requiresCustomNodes on non-cloud', async () => { + await mockFetch( + {}, + { + bp: globalBlueprint({ requiresCustomNodes: ['custom-node-pack'] }) + } + ) + expect(store.isGlobalBlueprint('bp')).toBe(false) + }) + + it('should include blueprints with requiresCustomNodes on cloud', async () => { + mockDistributionTypes.isCloud = true + await mockFetch( + {}, + { + bp: globalBlueprint({ requiresCustomNodes: ['custom-node-pack'] }) + } + ) + expect(store.isGlobalBlueprint('bp')).toBe(true) + }) + + it('should include blueprints with empty requiresCustomNodes everywhere', async () => { + await mockFetch({}, { bp: globalBlueprint({ requiresCustomNodes: [] }) }) + expect(store.isGlobalBlueprint('bp')).toBe(true) + }) + + it('should exclude blueprints whose includeOnDistributions does not match', async () => { + await mockFetch( + {}, + { + bp: globalBlueprint({ + includeOnDistributions: [TemplateIncludeOnDistributionEnum.Cloud] + }) + } + ) + expect(store.isGlobalBlueprint('bp')).toBe(false) + }) + + it('should include blueprints whose includeOnDistributions matches current distribution', async () => { + await mockFetch( + {}, + { + bp: globalBlueprint({ + includeOnDistributions: [TemplateIncludeOnDistributionEnum.Local] + }) + } + ) + expect(store.isGlobalBlueprint('bp')).toBe(true) + }) + + it('should include blueprints on desktop when includeOnDistributions has desktop', async () => { + mockDistributionTypes.isDesktop = true + await mockFetch( + {}, + { + bp: globalBlueprint({ + includeOnDistributions: [TemplateIncludeOnDistributionEnum.Desktop] + }) + } + ) + expect(store.isGlobalBlueprint('bp')).toBe(true) + }) + + it('should include blueprints with no filtering fields', async () => { + await mockFetch({}, { bp: globalBlueprint() }) + expect(store.isGlobalBlueprint('bp')).toBe(true) + }) + }) }) diff --git a/src/stores/subgraphStore.ts b/src/stores/subgraphStore.ts index 33aa49558..c911e19df 100644 --- a/src/stores/subgraphStore.ts +++ b/src/stores/subgraphStore.ts @@ -20,6 +20,8 @@ import type { ComfyNodeDef as ComfyNodeDefV1, InputSpec } from '@/schemas/nodeDefSchema' +import { isCloud, isDesktop } from '@/platform/distribution/types' +import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template' import { api } from '@/scripts/api' import type { GlobalSubgraphData } from '@/scripts/api' import { useDialogService } from '@/services/dialogService' @@ -221,9 +223,22 @@ export const useSubgraphStore = defineStore('subgraph', () => { ) } const subgraphs = await api.getGlobalSubgraphs() - await Promise.allSettled( - Object.entries(subgraphs).map(loadGlobalBlueprint) - ) + const currentDistribution: TemplateIncludeOnDistributionEnum = isCloud + ? TemplateIncludeOnDistributionEnum.Cloud + : isDesktop + ? TemplateIncludeOnDistributionEnum.Desktop + : TemplateIncludeOnDistributionEnum.Local + const filteredEntries = Object.entries(subgraphs).filter(([, v]) => { + if (!isCloud && (v.info.requiresCustomNodes?.length ?? 0) > 0) + return false + if ( + (v.info.includeOnDistributions?.length ?? 0) > 0 && + !v.info.includeOnDistributions!.includes(currentDistribution) + ) + return false + return true + }) + await Promise.allSettled(filteredEntries.map(loadGlobalBlueprint)) } const userSubs = (