feat: client-side distribution filtering for blueprint subgraphs (#8686)

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)
This commit is contained in:
Christian Byrne
2026-02-20 17:02:54 -08:00
committed by GitHub
parent b27eb5861a
commit 337e0486ea
3 changed files with 113 additions and 6 deletions

View File

@@ -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<string>
essentials_category?: string

View File

@@ -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['info']> = {}
): 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)
})
})
})

View File

@@ -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 = (