mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 14:16:00 +00:00
Compare commits
3 Commits
free-tier-
...
blueprint-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db86114d7d | ||
|
|
15c98676e9 | ||
|
|
38edba7024 |
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
@@ -94,6 +94,8 @@
|
||||
"openNewIssue": "Open New Issue",
|
||||
"showReport": "Show Report",
|
||||
"imageFailedToLoad": "Image failed to load",
|
||||
"imageDoesNotExist": "Image does not exist",
|
||||
"unknownFile": "Unknown file",
|
||||
"reconnecting": "Reconnecting",
|
||||
"reconnected": "Reconnected",
|
||||
"delete": "Delete",
|
||||
|
||||
@@ -29,6 +29,8 @@ const i18n = createI18n({
|
||||
failedToDownloadImage: 'Failed to download image',
|
||||
calculatingDimensions: 'Calculating dimensions',
|
||||
imageFailedToLoad: 'Image failed to load',
|
||||
imageDoesNotExist: 'Image does not exist',
|
||||
unknownFile: 'Unknown file',
|
||||
loading: 'Loading'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,10 +321,11 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
||||
}
|
||||
|
||||
const getImageFilename = (url: string): string => {
|
||||
if (!url) return t('g.imageDoesNotExist')
|
||||
try {
|
||||
return new URL(url).searchParams.get('filename') || 'Unknown file'
|
||||
return new URL(url).searchParams.get('filename') || t('g.unknownFile')
|
||||
} catch {
|
||||
return 'Invalid URL'
|
||||
return t('g.imageDoesNotExist')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,16 +2,31 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { computed } from 'vue'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { FormDropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
||||
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
|
||||
|
||||
const mockAssetsData = vi.hoisted(() => ({ items: [] as AssetItem[] }))
|
||||
vi.mock(
|
||||
'@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData',
|
||||
() => ({
|
||||
useAssetWidgetData: () => ({
|
||||
category: computed(() => 'checkpoints'),
|
||||
assets: computed(() => mockAssetsData.items),
|
||||
isLoading: computed(() => false),
|
||||
error: computed(() => null)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
@@ -306,3 +321,133 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('WidgetSelectDropdown cloud asset mode (COM-14333)', () => {
|
||||
interface CloudModeInstance extends ComponentPublicInstance {
|
||||
dropdownItems: FormDropdownItem[]
|
||||
displayItems: FormDropdownItem[]
|
||||
selectedSet: Set<string>
|
||||
}
|
||||
|
||||
const createTestAsset = (
|
||||
id: string,
|
||||
name: string,
|
||||
preview_url: string
|
||||
): AssetItem => ({
|
||||
id,
|
||||
name,
|
||||
preview_url,
|
||||
tags: []
|
||||
})
|
||||
|
||||
const createCloudModeWidget = (
|
||||
value: string = 'model.safetensors'
|
||||
): SimplifiedWidget<string | undefined> => ({
|
||||
name: 'test_model_select',
|
||||
type: 'combo',
|
||||
value,
|
||||
options: {
|
||||
values: [],
|
||||
nodeType: 'CheckpointLoaderSimple'
|
||||
}
|
||||
})
|
||||
|
||||
const mountCloudComponent = (
|
||||
widget: SimplifiedWidget<string | undefined>,
|
||||
modelValue: string | undefined
|
||||
): VueWrapper<CloudModeInstance> => {
|
||||
return mount(WidgetSelectDropdown, {
|
||||
props: {
|
||||
widget,
|
||||
modelValue,
|
||||
assetKind: 'model',
|
||||
isAssetMode: true,
|
||||
nodeType: 'CheckpointLoaderSimple'
|
||||
},
|
||||
global: {
|
||||
plugins: [PrimeVue, createTestingPinia(), i18n]
|
||||
}
|
||||
}) as unknown as VueWrapper<CloudModeInstance>
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockAssetsData.items = []
|
||||
})
|
||||
|
||||
it('does not include missing items in cloud asset mode dropdown', () => {
|
||||
mockAssetsData.items = [
|
||||
createTestAsset(
|
||||
'asset-1',
|
||||
'existing_model.safetensors',
|
||||
'https://example.com/preview.jpg'
|
||||
)
|
||||
]
|
||||
|
||||
const widget = createCloudModeWidget('missing_model.safetensors')
|
||||
const wrapper = mountCloudComponent(widget, 'missing_model.safetensors')
|
||||
|
||||
const dropdownItems = wrapper.vm.dropdownItems
|
||||
expect(dropdownItems).toHaveLength(1)
|
||||
expect(dropdownItems[0].name).toBe('existing_model.safetensors')
|
||||
expect(
|
||||
dropdownItems.some((item) => item.name === 'missing_model.safetensors')
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('shows only available cloud assets in dropdown', () => {
|
||||
mockAssetsData.items = [
|
||||
createTestAsset(
|
||||
'asset-1',
|
||||
'model_a.safetensors',
|
||||
'https://example.com/a.jpg'
|
||||
),
|
||||
createTestAsset(
|
||||
'asset-2',
|
||||
'model_b.safetensors',
|
||||
'https://example.com/b.jpg'
|
||||
)
|
||||
]
|
||||
|
||||
const widget = createCloudModeWidget('model_a.safetensors')
|
||||
const wrapper = mountCloudComponent(widget, 'model_a.safetensors')
|
||||
|
||||
const dropdownItems = wrapper.vm.dropdownItems
|
||||
expect(dropdownItems).toHaveLength(2)
|
||||
expect(dropdownItems.map((item) => item.name)).toEqual([
|
||||
'model_a.safetensors',
|
||||
'model_b.safetensors'
|
||||
])
|
||||
})
|
||||
|
||||
it('returns empty dropdown when no cloud assets available', () => {
|
||||
mockAssetsData.items = []
|
||||
|
||||
const widget = createCloudModeWidget('missing_model.safetensors')
|
||||
const wrapper = mountCloudComponent(widget, 'missing_model.safetensors')
|
||||
|
||||
const dropdownItems = wrapper.vm.dropdownItems
|
||||
expect(dropdownItems).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('includes missing cloud asset in displayItems for input field visibility', () => {
|
||||
mockAssetsData.items = [
|
||||
createTestAsset(
|
||||
'asset-1',
|
||||
'existing_model.safetensors',
|
||||
'https://example.com/preview.jpg'
|
||||
)
|
||||
]
|
||||
|
||||
const widget = createCloudModeWidget('missing_model.safetensors')
|
||||
const wrapper = mountCloudComponent(widget, 'missing_model.safetensors')
|
||||
|
||||
const displayItems = wrapper.vm.displayItems
|
||||
expect(displayItems).toHaveLength(2)
|
||||
expect(displayItems[0].name).toBe('missing_model.safetensors')
|
||||
expect(displayItems[0].id).toBe('missing-missing_model.safetensors')
|
||||
expect(displayItems[1].name).toBe('existing_model.safetensors')
|
||||
|
||||
const selectedSet = wrapper.vm.selectedSet
|
||||
expect(selectedSet.has('missing-missing_model.safetensors')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -254,9 +254,8 @@ const baseModelFilteredAssetItems = computed<FormDropdownItem[]>(() =>
|
||||
|
||||
const allItems = computed<FormDropdownItem[]>(() => {
|
||||
if (props.isAssetMode && assetData) {
|
||||
if (missingValueItem.value) {
|
||||
return [missingValueItem.value, ...baseModelFilteredAssetItems.value]
|
||||
}
|
||||
// Cloud assets not in user's library shouldn't appear as search results (COM-14333).
|
||||
// Unlike local mode, cloud users can't access files they don't own.
|
||||
return baseModelFilteredAssetItems.value
|
||||
}
|
||||
return [
|
||||
@@ -282,6 +281,17 @@ const dropdownItems = computed<FormDropdownItem[]>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Items used for display in the input field. In cloud mode, includes
|
||||
* missing items so users can see their selected value even if not in library.
|
||||
*/
|
||||
const displayItems = computed<FormDropdownItem[]>(() => {
|
||||
if (props.isAssetMode && assetData && missingValueItem.value) {
|
||||
return [missingValueItem.value, ...baseModelFilteredAssetItems.value]
|
||||
}
|
||||
return dropdownItems.value
|
||||
})
|
||||
|
||||
const mediaPlaceholder = computed(() => {
|
||||
const options = props.widget.options
|
||||
|
||||
@@ -332,18 +342,20 @@ const acceptTypes = computed(() => {
|
||||
const layoutMode = ref<LayoutMode>(props.defaultLayoutMode ?? 'grid')
|
||||
|
||||
watch(
|
||||
[modelValue, dropdownItems],
|
||||
([currentValue, _dropdownItems]) => {
|
||||
[modelValue, displayItems],
|
||||
([currentValue]) => {
|
||||
if (currentValue === undefined) {
|
||||
selectedSet.value.clear()
|
||||
return
|
||||
}
|
||||
|
||||
const item = dropdownItems.value.find((item) => item.name === currentValue)
|
||||
if (item) {
|
||||
const item = displayItems.value.find((item) => item.name === currentValue)
|
||||
if (!item) {
|
||||
selectedSet.value.clear()
|
||||
selectedSet.value.add(item.id)
|
||||
return
|
||||
}
|
||||
selectedSet.value.clear()
|
||||
selectedSet.value.add(item.id)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -461,6 +473,7 @@ function getMediaUrl(
|
||||
v-model:ownership-selected="ownershipSelected"
|
||||
v-model:base-model-selected="baseModelSelected"
|
||||
:items="dropdownItems"
|
||||
:display-items="displayItems"
|
||||
:placeholder="mediaPlaceholder"
|
||||
:multiple="false"
|
||||
:uploadable
|
||||
|
||||
@@ -18,6 +18,8 @@ import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
||||
|
||||
interface Props {
|
||||
items: FormDropdownItem[]
|
||||
/** Items used for display in the input field. Falls back to items if not provided. */
|
||||
displayItems?: FormDropdownItem[]
|
||||
placeholder?: string
|
||||
/**
|
||||
* If true, allows multiple selections. If a number is provided,
|
||||
@@ -193,6 +195,7 @@ async function customSearcher(
|
||||
:is-open
|
||||
:placeholder="placeholderText"
|
||||
:items
|
||||
:display-items
|
||||
:max-selectable
|
||||
:selected
|
||||
:uploadable
|
||||
|
||||
@@ -10,6 +10,8 @@ interface Props {
|
||||
isOpen?: boolean
|
||||
placeholder?: string
|
||||
items: FormDropdownItem[]
|
||||
/** Items used for display in the input field. Falls back to items if not provided. */
|
||||
displayItems?: FormDropdownItem[]
|
||||
selected: Set<string>
|
||||
maxSelectable: number
|
||||
uploadable: boolean
|
||||
@@ -28,7 +30,8 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const selectedItems = computed(() => {
|
||||
return props.items.filter((item) => props.selected.has(item.id))
|
||||
const itemsToSearch = props.displayItems ?? props.items
|
||||
return itemsToSearch.filter((item) => props.selected.has(item.id))
|
||||
})
|
||||
|
||||
const theButtonStyle = computed(() =>
|
||||
|
||||
@@ -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,
|
||||
@@ -235,6 +236,8 @@ export type GlobalSubgraphData = {
|
||||
node_pack: string
|
||||
category?: string
|
||||
search_aliases?: string[]
|
||||
requiresCustomNodes?: string[]
|
||||
includeOnDistributions?: TemplateIncludeOnDistributionEnum[]
|
||||
}
|
||||
data: string | Promise<string>
|
||||
}
|
||||
|
||||
@@ -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,174 @@ describe('useSubgraphStore', () => {
|
||||
expect(subgraphExtra?.BlueprintSearchAliases).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('subgraph definition category', () => {
|
||||
it('should use category from subgraph definition as default', async () => {
|
||||
const mockGraphWithCategory = {
|
||||
nodes: [{ type: '123' }],
|
||||
definitions: {
|
||||
subgraphs: [{ id: '123', category: 'Image Processing' }]
|
||||
}
|
||||
}
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
categorized: {
|
||||
name: 'Categorized Blueprint',
|
||||
info: { node_pack: 'test_pack' },
|
||||
data: JSON.stringify(mockGraphWithCategory)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.categorized'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.category).toBe('Subgraph Blueprints/Image Processing')
|
||||
})
|
||||
|
||||
it('should use User override for user blueprints even with definition category', async () => {
|
||||
const mockGraphWithCategory = {
|
||||
nodes: [{ type: '123' }],
|
||||
definitions: {
|
||||
subgraphs: [{ id: '123', category: 'Image Processing' }]
|
||||
}
|
||||
}
|
||||
await mockFetch({ 'user-bp.json': mockGraphWithCategory })
|
||||
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.user-bp'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.category).toBe('Subgraph Blueprints/User')
|
||||
})
|
||||
|
||||
it('should fallback to bare Subgraph Blueprints when no category anywhere', async () => {
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
no_cat_global: {
|
||||
name: 'No Category Global',
|
||||
info: { node_pack: 'test_pack' },
|
||||
data: JSON.stringify(mockGraph)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.no_cat_global'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.category).toBe('Subgraph Blueprints')
|
||||
})
|
||||
|
||||
it('should let overrides take precedence over definition category', async () => {
|
||||
const mockGraphWithCategory = {
|
||||
nodes: [{ type: '123' }],
|
||||
definitions: {
|
||||
subgraphs: [{ id: '123', category: 'Image Processing' }]
|
||||
}
|
||||
}
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
bp_override: {
|
||||
name: 'Override Blueprint',
|
||||
info: {
|
||||
node_pack: 'test_pack',
|
||||
category: 'Custom Category'
|
||||
},
|
||||
data: JSON.stringify(mockGraphWithCategory)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.bp_override'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.category).toBe('Subgraph Blueprints/Custom Category')
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
@@ -208,22 +210,35 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
const loaded = await blueprint.load()
|
||||
const category = v.info.category
|
||||
? `Subgraph Blueprints/${v.info.category}`
|
||||
: 'Subgraph Blueprints'
|
||||
: undefined
|
||||
registerNodeDef(
|
||||
loaded,
|
||||
{
|
||||
python_module: v.info.node_pack,
|
||||
display_name: v.name,
|
||||
category,
|
||||
...(category && { category }),
|
||||
search_aliases: v.info.search_aliases
|
||||
},
|
||||
k
|
||||
)
|
||||
}
|
||||
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 = (
|
||||
@@ -265,6 +280,11 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
const description =
|
||||
workflowExtra?.BlueprintDescription ?? 'User generated subgraph blueprint'
|
||||
const search_aliases = workflowExtra?.BlueprintSearchAliases
|
||||
const subgraphDefCategory =
|
||||
workflow.initialState.definitions?.subgraphs?.[0]?.category
|
||||
const category = subgraphDefCategory
|
||||
? `Subgraph Blueprints/${subgraphDefCategory}`
|
||||
: 'Subgraph Blueprints'
|
||||
const nodedefv1: ComfyNodeDefV1 = {
|
||||
input: { required: inputs },
|
||||
output: subgraphNode.outputs.map((o) => `${o.type}`),
|
||||
@@ -272,7 +292,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
name: typePrefix + name,
|
||||
display_name: name,
|
||||
description,
|
||||
category: 'Subgraph Blueprints',
|
||||
category,
|
||||
output_node: false,
|
||||
python_module: 'blueprint',
|
||||
search_aliases,
|
||||
|
||||
Reference in New Issue
Block a user