mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 01:07:56 +00:00
Compare commits
3 Commits
split/sett
...
glary/disa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a98d5557db | ||
|
|
19f5c9f6ae | ||
|
|
fbff4c07f4 |
@@ -2584,7 +2584,6 @@
|
||||
"viewMoreDetailsPlans": "View more details about plans & pricing",
|
||||
"nextBillingCycle": "next billing cycle",
|
||||
"yourPlanIncludes": "Your plan includes:",
|
||||
"whatsIncluded": "What's included:",
|
||||
"planLoadError": "We couldn't load your plan details.",
|
||||
"planLoadErrorRetry": "Try again",
|
||||
"teamPlanName": "Team",
|
||||
@@ -3308,6 +3307,7 @@
|
||||
"invalidFilenameDetail": "The asset filename could not be determined. Please try again.",
|
||||
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
|
||||
"failedToSetModelValue": "Node added, but its model could not be set automatically. Check the console for details.",
|
||||
"useDisabledNoProvider": "No matching node is registered for this model type.",
|
||||
"fileFormats": "File formats",
|
||||
"fileName": "File Name",
|
||||
"fileSize": "File Size",
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"errorUserTokenAccessDenied": "お使いのAPIトークンにはこのリソースへのアクセス権がありません。トークンの権限を確認してください。",
|
||||
"errorUserTokenInvalid": "保存されているAPIトークンが無効または期限切れです。設定でトークンを更新してください。",
|
||||
"failedToCreateNode": "ノードの作成に失敗しました。再試行するか、詳細はコンソールをご確認ください。",
|
||||
"useDisabledNoProvider": "このモデルタイプに一致するノードが登録されていません。",
|
||||
"fileFormats": "ファイル形式",
|
||||
"fileName": "ファイル名",
|
||||
"fileSize": "ファイルサイズ",
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"errorUserTokenAccessDenied": "您的 API 密钥无权访问此资源。请检查密钥权限。",
|
||||
"errorUserTokenInvalid": "您保存的 API 密钥无效或已过期。请在设置中更新密钥。",
|
||||
"failedToCreateNode": "创建节点失败。请重试或查看控制台获取详细信息。",
|
||||
"useDisabledNoProvider": "没有为此模型类型注册匹配的节点。",
|
||||
"fileFormats": "文件格式",
|
||||
"fileName": "文件名",
|
||||
"fileSize": "文件大小",
|
||||
|
||||
@@ -6,6 +6,24 @@ import { createI18n } from 'vue-i18n'
|
||||
import AssetCard from '@/platform/assets/components/AssetCard.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
|
||||
const mockModelToNodeState = vi.hoisted(() => ({
|
||||
isReady: true,
|
||||
registeredCategories: new Set<string>(['checkpoints', 'loras'])
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
useModelToNodeStore: () => ({
|
||||
get isReady() {
|
||||
return mockModelToNodeState.isReady
|
||||
},
|
||||
getNodeProvider: (category: string) =>
|
||||
mockModelToNodeState.registeredCategories.has(category)
|
||||
? { nodeDef: { name: `${category}Loader` }, key: 'model' }
|
||||
: undefined,
|
||||
registerDefaults: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: () => 0
|
||||
@@ -28,7 +46,9 @@ vi.mock('@/stores/dialogStore', () => ({
|
||||
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
assetService: {
|
||||
deleteAsset: vi.fn()
|
||||
}
|
||||
},
|
||||
MODELS_TAG: 'models',
|
||||
MISSING_TAG: 'missing'
|
||||
}))
|
||||
|
||||
vi.mock('@/components/dialog/confirm/confirmDialog', () => ({
|
||||
@@ -83,7 +103,10 @@ function renderCard(asset: AssetDisplayItem) {
|
||||
IconGroup: true,
|
||||
MoreButton: true,
|
||||
StatusBadge: true,
|
||||
Button: { template: '<button><slot /></button>' }
|
||||
Button: {
|
||||
props: ['disabled'],
|
||||
template: '<button :disabled="disabled"><slot /></button>'
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
tooltip: {}
|
||||
@@ -95,6 +118,64 @@ function renderCard(asset: AssetDisplayItem) {
|
||||
describe('AssetCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockModelToNodeState.isReady = true
|
||||
mockModelToNodeState.registeredCategories = new Set([
|
||||
'checkpoints',
|
||||
'loras'
|
||||
])
|
||||
})
|
||||
|
||||
describe('"Use" button gating for unsupported categories', () => {
|
||||
const findUseButton = () =>
|
||||
screen
|
||||
.getAllByRole('button')
|
||||
.find((b) => b.textContent?.toLowerCase().includes('use'))
|
||||
|
||||
it('enables Use when the asset category has a registered provider', () => {
|
||||
renderCard(
|
||||
createDisplayAsset({
|
||||
id: 'usable',
|
||||
tags: ['models', 'checkpoints']
|
||||
})
|
||||
)
|
||||
const useBtn = findUseButton()
|
||||
expect(useBtn).toBeDefined()
|
||||
expect(useBtn).toHaveAttribute('aria-disabled', 'false')
|
||||
})
|
||||
|
||||
it('disables Use when the registry is ready and the category has no provider', () => {
|
||||
renderCard(
|
||||
createDisplayAsset({
|
||||
id: 'unsupported',
|
||||
tags: ['models', 'BEN']
|
||||
})
|
||||
)
|
||||
const useBtn = findUseButton()
|
||||
expect(useBtn).toHaveAttribute('aria-disabled', 'true')
|
||||
})
|
||||
|
||||
it('keeps Use enabled while the registry is still warming up', () => {
|
||||
mockModelToNodeState.isReady = false
|
||||
renderCard(
|
||||
createDisplayAsset({
|
||||
id: 'warming',
|
||||
tags: ['models', 'BEN']
|
||||
})
|
||||
)
|
||||
const useBtn = findUseButton()
|
||||
expect(useBtn).toHaveAttribute('aria-disabled', 'false')
|
||||
})
|
||||
|
||||
it('keeps the use button hoverable (not pointer-events-none) when disabled so the tooltip can fire', () => {
|
||||
renderCard(
|
||||
createDisplayAsset({
|
||||
id: 'unsupported',
|
||||
tags: ['models', 'BEN']
|
||||
})
|
||||
)
|
||||
const useBtn = findUseButton()
|
||||
expect(useBtn).not.toHaveAttribute('disabled')
|
||||
})
|
||||
})
|
||||
|
||||
describe('FE-228: filename rendering', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"
|
||||
@click.stop="interactive && $emit('focus', asset)"
|
||||
@focus="interactive && $emit('focus', asset)"
|
||||
@keydown.enter.self="interactive && $emit('select', asset)"
|
||||
@keydown.enter.self="interactive && canUseAsset && handleSelect()"
|
||||
>
|
||||
<div class="relative aspect-square w-full overflow-hidden rounded-xl">
|
||||
<div
|
||||
@@ -111,9 +111,23 @@
|
||||
</div>
|
||||
<Button
|
||||
v-if="interactive"
|
||||
v-tooltip.top="
|
||||
canUseAsset
|
||||
? undefined
|
||||
: {
|
||||
value: $t('assetBrowser.useDisabledNoProvider'),
|
||||
showDelay: tooltipDelay
|
||||
}
|
||||
"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
class="relative shrink-0"
|
||||
:class="
|
||||
cn(
|
||||
'relative shrink-0',
|
||||
!canUseAsset && 'cursor-not-allowed opacity-50'
|
||||
)
|
||||
"
|
||||
:aria-disabled="!canUseAsset"
|
||||
@click.stop="handleSelect"
|
||||
>
|
||||
{{ $t('g.use') }}
|
||||
@@ -142,6 +156,7 @@ import AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { getAssetCardTitle } from '@/platform/assets/utils/assetMetadataUtils'
|
||||
import { canCreateNodeForAsset } from '@/platform/assets/utils/resolveModelNodeFromAsset'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useAssetDownloadStore } from '@/stores/assetDownloadStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
@@ -187,6 +202,8 @@ const isNewlyImported = computed(() => isDownloadedThisSession(asset.id))
|
||||
|
||||
const showAssetOptions = computed(() => !(asset.is_immutable ?? true))
|
||||
|
||||
const canUseAsset = computed(() => canCreateNodeForAsset(asset))
|
||||
|
||||
const tooltipDelay = computed<number>(() =>
|
||||
settingStore.get('LiteGraph.Node.TooltipDelay')
|
||||
)
|
||||
@@ -197,6 +214,7 @@ const { isLoading, error } = useImageQuiet({
|
||||
})
|
||||
|
||||
function handleSelect() {
|
||||
if (!canUseAsset.value) return
|
||||
acknowledgeAsset(asset.id)
|
||||
emit('select', asset)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { resolveModelNodeFromAsset } from '@/platform/assets/utils/resolveModelNodeFromAsset'
|
||||
import {
|
||||
canCreateNodeForAsset,
|
||||
getAssetCategory,
|
||||
resolveModelNodeFromAsset
|
||||
} from '@/platform/assets/utils/resolveModelNodeFromAsset'
|
||||
|
||||
const mockGetNodeProvider = vi.hoisted(() => vi.fn())
|
||||
const mockRegisterDefaults = vi.hoisted(() => vi.fn())
|
||||
const mockIsReady = vi.hoisted(() => ({ value: true }))
|
||||
|
||||
vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
useModelToNodeStore: () => ({ getNodeProvider: mockGetNodeProvider })
|
||||
useModelToNodeStore: () => ({
|
||||
getNodeProvider: mockGetNodeProvider,
|
||||
registerDefaults: mockRegisterDefaults,
|
||||
get isReady() {
|
||||
return mockIsReady.value
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
function createMockAsset(overrides: Partial<AssetItem> = {}): AssetItem {
|
||||
@@ -48,6 +60,7 @@ function mockProvider(
|
||||
describe('resolveModelNodeFromAsset', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsReady.value = true
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
@@ -206,3 +219,68 @@ describe('resolveModelNodeFromAsset', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAssetCategory', () => {
|
||||
it('returns the first tag that is not models or missing', () => {
|
||||
expect(
|
||||
getAssetCategory(createMockAsset({ tags: ['models', 'checkpoints'] }))
|
||||
).toBe('checkpoints')
|
||||
})
|
||||
|
||||
it('skips the missing placeholder tag', () => {
|
||||
expect(
|
||||
getAssetCategory(
|
||||
createMockAsset({ tags: ['models', 'missing', 'loras'] })
|
||||
)
|
||||
).toBe('loras')
|
||||
})
|
||||
|
||||
it('returns undefined when only excluded tags are present', () => {
|
||||
expect(
|
||||
getAssetCategory(createMockAsset({ tags: ['models', 'missing'] }))
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined when tags is missing', () => {
|
||||
const asset = { ...createMockAsset(), tags: undefined } as unknown as
|
||||
| AssetItem
|
||||
| { tags: undefined }
|
||||
expect(getAssetCategory(asset as AssetItem)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('canCreateNodeForAsset', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsReady.value = true
|
||||
})
|
||||
|
||||
it('returns true while the registry is still warming up', () => {
|
||||
mockIsReady.value = false
|
||||
mockProvider(null)
|
||||
expect(canCreateNodeForAsset(createMockAsset())).toBe(true)
|
||||
expect(mockRegisterDefaults).toHaveBeenCalled()
|
||||
expect(mockGetNodeProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns true when a provider exists for the category', () => {
|
||||
mockProvider(createMockNodeProvider())
|
||||
expect(canCreateNodeForAsset(createMockAsset())).toBe(true)
|
||||
expect(mockGetNodeProvider).toHaveBeenCalledWith('checkpoints')
|
||||
})
|
||||
|
||||
it('returns false when the registry is ready but no provider matches', () => {
|
||||
mockProvider(null)
|
||||
expect(
|
||||
canCreateNodeForAsset(createMockAsset({ tags: ['models', 'BEN'] }))
|
||||
).toBe(false)
|
||||
expect(mockGetNodeProvider).toHaveBeenCalledWith('BEN')
|
||||
})
|
||||
|
||||
it('returns false when the asset has no usable category tag (would fail INVALID_ASSET)', () => {
|
||||
expect(
|
||||
canCreateNodeForAsset(createMockAsset({ tags: ['models', 'missing'] }))
|
||||
).toBe(false)
|
||||
expect(mockGetNodeProvider).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,32 @@ import { getAssetFilename } from '@/platform/assets/utils/assetMetadataUtils'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
import type { ModelNodeProvider } from '@/stores/modelToNodeStore'
|
||||
|
||||
/**
|
||||
* Extracts the category tag used to look up a node provider for this asset.
|
||||
* Returns `undefined` when the asset carries no usable category tag.
|
||||
*/
|
||||
export function getAssetCategory(asset: AssetItem): string | undefined {
|
||||
return asset.tags?.find((tag) => tag !== MODELS_TAG && tag !== MISSING_TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `false` only when we are confident the asset cannot be turned into a
|
||||
* node — either its category has no provider in the (initialised) registry, or
|
||||
* it carries no usable category tag at all. While the registry is still warming
|
||||
* up we return `true` so the UI stays enabled by default and only transitions
|
||||
* to disabled once we know.
|
||||
*/
|
||||
export function canCreateNodeForAsset(asset: AssetItem): boolean {
|
||||
const store = useModelToNodeStore()
|
||||
store.registerDefaults()
|
||||
if (!store.isReady) return true
|
||||
|
||||
const category = getAssetCategory(asset)
|
||||
if (!category) return false
|
||||
|
||||
return Boolean(store.getNodeProvider(category))
|
||||
}
|
||||
|
||||
type ResolveErrorCode = 'INVALID_ASSET' | 'NO_PROVIDER'
|
||||
|
||||
export interface ResolveModelNodeError {
|
||||
@@ -81,9 +107,7 @@ export function resolveModelNodeFromAsset(
|
||||
}
|
||||
}
|
||||
|
||||
const category = validAsset.tags.find(
|
||||
(tag) => tag !== MODELS_TAG && tag !== MISSING_TAG
|
||||
)
|
||||
const category = getAssetCategory(validAsset)
|
||||
if (!category) {
|
||||
console.error(
|
||||
`Asset ${validAsset.id} has no valid category tag. Available tags: ${validAsset.tags.join(', ')} (expected tag other than '${MODELS_TAG}' or '${MISSING_TAG}')`
|
||||
|
||||
@@ -37,7 +37,9 @@ vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
removeAssetTags: vi.fn()
|
||||
},
|
||||
INPUT_TAG: 'input',
|
||||
OUTPUT_TAG: 'output'
|
||||
OUTPUT_TAG: 'output',
|
||||
MODELS_TAG: 'models',
|
||||
MISSING_TAG: 'missing'
|
||||
}))
|
||||
|
||||
// Mock distribution type - hoisted so it can be changed per test
|
||||
@@ -48,7 +50,16 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock modelToNodeStore with proper node providers and category lookups
|
||||
const mockModelToNodeState = vi.hoisted(() => ({
|
||||
isReady: true,
|
||||
registeredCategories: new Set<string>([
|
||||
'checkpoints',
|
||||
'loras',
|
||||
'vae',
|
||||
'models'
|
||||
])
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
useModelToNodeStore: () => ({
|
||||
getAllNodeProviders: vi.fn((category: string) => {
|
||||
@@ -62,7 +73,7 @@ vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
],
|
||||
loras: [
|
||||
{ nodeDef: { name: 'LoraLoader' }, key: 'lora_name' },
|
||||
{ nodeDef: { name: 'LoraLoaderModelOnly' }, key: 'lora_name' }
|
||||
{ nodeDef: { name: 'VAELoader' }, key: 'vae_name' }
|
||||
],
|
||||
vae: [{ nodeDef: { name: 'VAELoader' }, key: 'vae_name' }]
|
||||
}
|
||||
@@ -78,7 +89,14 @@ vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
}
|
||||
return nodeToCategory[nodeType]
|
||||
}),
|
||||
getNodeProvider: vi.fn(),
|
||||
getNodeProvider: vi.fn((category: string) =>
|
||||
mockModelToNodeState.registeredCategories.has(category)
|
||||
? { nodeDef: { name: `${category}Loader` }, key: 'model' }
|
||||
: undefined
|
||||
),
|
||||
get isReady() {
|
||||
return mockModelToNodeState.isReady
|
||||
},
|
||||
registerDefaults: vi.fn()
|
||||
})
|
||||
}))
|
||||
@@ -1433,6 +1451,125 @@ describe('assetsStore - Model Assets Cache (Cloud)', () => {
|
||||
expect(store.hasCategory('tag:models')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('broken-category warning', () => {
|
||||
const supportedAsset = (id: string, category: string) => ({
|
||||
id,
|
||||
name: `asset-${id}`,
|
||||
size: 100,
|
||||
created_at: new Date().toISOString(),
|
||||
tags: ['models', category],
|
||||
preview_url: `http://test.com/${id}`
|
||||
})
|
||||
|
||||
let warnSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
mockModelToNodeState.isReady = true
|
||||
mockModelToNodeState.registeredCategories = new Set([
|
||||
'checkpoints',
|
||||
'loras',
|
||||
'vae',
|
||||
'models'
|
||||
])
|
||||
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('warns once with the broken set after a category finishes its first load', async () => {
|
||||
const store = useAssetsStore()
|
||||
vi.mocked(assetService.getAssetsByTag).mockResolvedValue([
|
||||
supportedAsset('a', 'checkpoints'),
|
||||
supportedAsset('b', 'BEN'),
|
||||
supportedAsset('c', 'BEN'),
|
||||
supportedAsset('d', 'WAN')
|
||||
])
|
||||
|
||||
await store.updateModelsForTag('models')
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1)
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('No node provider for categories'),
|
||||
['BEN', 'WAN']
|
||||
)
|
||||
})
|
||||
|
||||
it('does not warn when every loaded asset has a registered provider', async () => {
|
||||
const store = useAssetsStore()
|
||||
vi.mocked(assetService.getAssetsByTag).mockResolvedValue([
|
||||
supportedAsset('a', 'checkpoints'),
|
||||
supportedAsset('b', 'loras')
|
||||
])
|
||||
|
||||
await store.updateModelsForTag('models')
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not warn again on subsequent refresh of the same category', async () => {
|
||||
const store = useAssetsStore()
|
||||
vi.mocked(assetService.getAssetsByTag).mockResolvedValue([
|
||||
supportedAsset('a', 'BEN')
|
||||
])
|
||||
|
||||
await store.updateModelsForTag('models')
|
||||
store.invalidateCategory('tag:models')
|
||||
await store.updateModelsForTag('models')
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('warns again when a refresh introduces a new broken category', async () => {
|
||||
const store = useAssetsStore()
|
||||
vi.mocked(assetService.getAssetsByTag)
|
||||
.mockResolvedValueOnce([supportedAsset('a', 'BEN')])
|
||||
.mockResolvedValueOnce([
|
||||
supportedAsset('a', 'BEN'),
|
||||
supportedAsset('b', 'WAN')
|
||||
])
|
||||
|
||||
await store.updateModelsForTag('models')
|
||||
store.invalidateCategory('tag:models')
|
||||
await store.updateModelsForTag('models')
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledTimes(2)
|
||||
expect(warnSpy).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining('No node provider for categories'),
|
||||
['BEN', 'WAN']
|
||||
)
|
||||
})
|
||||
|
||||
it('stops warning once an extension registers a provider for the previously broken category', async () => {
|
||||
const store = useAssetsStore()
|
||||
vi.mocked(assetService.getAssetsByTag).mockResolvedValue([
|
||||
supportedAsset('a', 'BEN')
|
||||
])
|
||||
|
||||
await store.updateModelsForTag('models')
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
mockModelToNodeState.registeredCategories.add('BEN')
|
||||
store.invalidateCategory('tag:models')
|
||||
await store.updateModelsForTag('models')
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('skips the report while the modelToNode registry is not ready', async () => {
|
||||
mockModelToNodeState.isReady = false
|
||||
const store = useAssetsStore()
|
||||
vi.mocked(assetService.getAssetsByTag).mockResolvedValue([
|
||||
supportedAsset('a', 'BEN')
|
||||
])
|
||||
|
||||
await store.updateModelsForTag('models')
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('assetsStore - Deletion State and Input Mapping', () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
OUTPUT_TAG,
|
||||
assetService
|
||||
} from '@/platform/assets/services/assetService'
|
||||
import { getAssetCategory } from '@/platform/assets/utils/resolveModelNodeFromAsset'
|
||||
import type { PaginationOptions } from '@/platform/assets/services/assetService'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
@@ -407,6 +408,11 @@ export const useAssetsStore = defineStore('assets', () => {
|
||||
|
||||
const pendingRequestByCategory = new Map<string, ModelPaginationState>()
|
||||
const pendingPromiseByCategory = new Map<string, Promise<void>>()
|
||||
const reportedBrokenCategoriesByLoad = new Set<string>()
|
||||
|
||||
function brokenCategoriesFingerprint(loadKey: string, broken: string[]) {
|
||||
return `${loadKey}::${[...broken].sort().join(',')}`
|
||||
}
|
||||
|
||||
function createState(
|
||||
existingAssets?: Map<string, AssetItem>
|
||||
@@ -426,6 +432,32 @@ export const useAssetsStore = defineStore('assets', () => {
|
||||
return committed !== state && pending !== state
|
||||
}
|
||||
|
||||
function reportBrokenCategoriesOnce(
|
||||
loadKey: string,
|
||||
assets: Map<string, AssetItem>
|
||||
): void {
|
||||
modelToNodeStore.registerDefaults()
|
||||
if (!modelToNodeStore.isReady) return
|
||||
|
||||
const broken = new Set<string>()
|
||||
for (const asset of assets.values()) {
|
||||
const category = getAssetCategory(asset)
|
||||
if (!category) continue
|
||||
if (!modelToNodeStore.getNodeProvider(category)) broken.add(category)
|
||||
}
|
||||
if (broken.size === 0) return
|
||||
|
||||
const sorted = [...broken].sort()
|
||||
const fingerprint = brokenCategoriesFingerprint(loadKey, sorted)
|
||||
if (reportedBrokenCategoriesByLoad.has(fingerprint)) return
|
||||
reportedBrokenCategoriesByLoad.add(fingerprint)
|
||||
|
||||
console.warn(
|
||||
`[AssetBrowser] No node provider for categories (${loadKey}):`,
|
||||
sorted
|
||||
)
|
||||
}
|
||||
|
||||
const EMPTY_ASSETS: AssetItem[] = []
|
||||
|
||||
/**
|
||||
@@ -587,6 +619,8 @@ export const useAssetsStore = defineStore('assets', () => {
|
||||
}
|
||||
assetsArrayCache.delete(category)
|
||||
pendingRequestByCategory.delete(category)
|
||||
|
||||
reportBrokenCategoriesOnce(category, state.assets)
|
||||
}
|
||||
|
||||
const promise = loadBatches().finally(() => {
|
||||
|
||||
@@ -679,4 +679,23 @@ describe('useModelToNodeStore', () => {
|
||||
expect(modelToNodeStore.getAllNodeProviders(undefined)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('isReady', () => {
|
||||
it('starts false before defaults are registered', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
expect(modelToNodeStore.isReady).toBe(false)
|
||||
})
|
||||
|
||||
it('flips to true after registerDefaults', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
expect(modelToNodeStore.isReady).toBe(true)
|
||||
})
|
||||
|
||||
it('flips to true as a side-effect of getNodeProvider lazy-registering', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.getNodeProvider('checkpoints')
|
||||
expect(modelToNodeStore.isReady).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,6 +25,9 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const haveDefaultsLoaded = ref(false)
|
||||
|
||||
/** True once default provider registrations have been applied. */
|
||||
const isReady = computed(() => haveDefaultsLoaded.value)
|
||||
|
||||
/** Internal computed for reactive caching of registered node types */
|
||||
const registeredNodeTypes = computed<Record<string, string>>(() => {
|
||||
return Object.fromEntries(
|
||||
@@ -164,6 +167,7 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
|
||||
return {
|
||||
modelToNodeMap,
|
||||
isReady,
|
||||
getRegisteredNodeTypes,
|
||||
getCategoryForNodeType,
|
||||
getNodeProvider,
|
||||
|
||||
Reference in New Issue
Block a user