mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-20 04:20:10 +00:00
feat: refresh missing models through pipeline (#11661)
## Summary Follow-up to the closed earlier attempt in #11646. This PR keeps the same user-facing goal, but changes the implementation to reuse the existing missing model pipeline for refresh instead of maintaining a separate candidate-only recheck path. Adds a missing model refresh action in the Errors tab by reusing the existing missing model pipeline, so users can re-check models after downloading or manually placing files without reloading the workflow. ## Changes - **What**: - Adds `app.refreshMissingModels()` as a reusable refresh entry point for the current root graph. - Splits node definition reloading into `app.reloadNodeDefs()` so missing-model refresh can pull fresh `object_info` without showing the generic combo refresh success flow. - Reuses the existing missing model pipeline instead of adding a separate candidate-only checker. The refresh path serializes the current graph, reuses active workflow model metadata when available, falls back to current missing-model metadata, and then reruns the same candidate discovery/enrichment/surfacing flow used during workflow load. - Adds missing model refresh state and error handling to `missingModelStore`. - Adds a Refresh button next to Download all in the missing model card action bar. - Moves Download all from the Errors tab header into the missing model card, so the Download all and Refresh actions render or hide together. - Changes Download all visibility from “more than one downloadable model” to “at least one downloadable model.” - Keeps the action bar hidden when there are no downloadable missing models; Cloud still does not render this action area. - Normalizes active workflow `pendingWarnings` updates so resolved missing model warnings do not get revived by stale empty warning objects. - Adds test IDs and coverage for the new action bar, refresh state, refresh delegation, pending warning sync, and E2E refresh behavior. - **Breaking**: None. - **Dependencies**: None. ## Review Focus The main design choice is intentionally reusing the missing model pipeline for refresh instead of implementing a smaller candidate-only recheck. The earlier candidate-only approach was cheaper, but it created a separate source of truth for missing-model resolution and made edge cases harder to reason about. In particular, it could diverge from the behavior used when a workflow is loaded, and it did not naturally handle the case where a model becomes missing after the workflow is already open. This version pays the cost of refreshing node definitions and rerunning the missing-model scan for the current graph, but keeps the refresh behavior aligned with workflow load semantics. Expected behavior by environment: - OSS browser: - The action bar appears when at least one missing model has a downloadable URL and directory. - Download all uses the existing browser download path. - Refresh reloads `object_info`, refreshes node definitions/combo values, reruns missing-model detection for the current graph, and clears the error if the selected model is now available. - OSS desktop: - The same action bar appears under the same downloadable-model condition. - Download all uses the existing Electron DownloadManager path. - Refresh uses the same missing-model pipeline as browser, so manually placed files or desktop-downloaded files can be rechecked without reloading the workflow. - Cloud: - The action bar remains hidden because model download/import is not supported in this section for Cloud. A few boundaries are intentional: - This PR does not add automatic filesystem watching. Browser OSS cannot reliably observe local model folder changes, so the user-triggered Refresh button remains the cross-environment mechanism. - This PR does not redesign the public `refreshComboInNodes` API beyond extracting `reloadNodeDefs()` for reuse. Further cleanup of toast behavior or a more explicit object-info reload API can be follow-up work. - This PR keeps refresh scoped to missing-model validation; missing media and missing nodes continue to use their existing flows. Linear: FE-417 ## Screenshots (if applicable) https://github.com/user-attachments/assets/2e02799f-1374-4377-b7b3-172241517772 ## Validation - `pnpm format` - `pnpm lint` (passes; existing unrelated warning remains in `src/platform/workspace/composables/useWorkspaceBilling.test.ts`) - `pnpm typecheck` - `pnpm test:unit` - `pnpm test:browser:local -- --project=chromium browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts` - `pnpm build` - `NX_SKIP_NX_CACHE=true DISTRIBUTION=desktop USE_PROD_CONFIG=true NODE_OPTIONS='--max-old-space-size=8192' pnpm exec nx build` - Manual desktop verification through `~/Projects/desktop` after copying the desktop build into `assets/ComfyUI/web_custom_versions/desktop_app`: - confirmed the FE bundle is built with `DISTRIBUTION = "desktop"` - confirmed missing model Download uses the desktop download path instead of browser download - confirmed Refresh can clear the missing model error after the model is available - Push hook: `pnpm knip --cache` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11661-feat-refresh-missing-models-through-pipeline-34f6d73d3650811488defee54a7a6667) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type {
|
||||
@@ -5,6 +6,10 @@ import type {
|
||||
LGraphCanvas,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
ComfyWorkflowJSON,
|
||||
ModelFile
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { ComfyApp } from './app'
|
||||
import { createNode } from '@/utils/litegraphUtil'
|
||||
import {
|
||||
@@ -16,6 +21,32 @@ import {
|
||||
pasteVideoNodes
|
||||
} from '@/composables/usePaste'
|
||||
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import type { LoadedComfyWorkflow } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import type { MissingModelCandidate } from '@/platform/missingModel/types'
|
||||
|
||||
const {
|
||||
mockToastStore,
|
||||
mockExtensionService,
|
||||
mockNodeOutputStore,
|
||||
mockWorkspaceWorkflow
|
||||
} = vi.hoisted(() => ({
|
||||
mockToastStore: {
|
||||
addAlert: vi.fn(),
|
||||
add: vi.fn(),
|
||||
remove: vi.fn()
|
||||
},
|
||||
mockExtensionService: {
|
||||
invokeExtensions: vi.fn(),
|
||||
invokeExtensionsAsync: vi.fn()
|
||||
},
|
||||
mockNodeOutputStore: {
|
||||
refreshNodeOutputs: vi.fn()
|
||||
},
|
||||
mockWorkspaceWorkflow: {
|
||||
activeWorkflow: null as unknown
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
createNode: vi.fn(),
|
||||
@@ -40,10 +71,20 @@ vi.mock('@/scripts/metadata/parser', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||
useToastStore: vi.fn(() => ({
|
||||
addAlert: vi.fn(),
|
||||
add: vi.fn(),
|
||||
remove: vi.fn()
|
||||
useToastStore: vi.fn(() => mockToastStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/services/extensionService', () => ({
|
||||
useExtensionService: vi.fn(() => mockExtensionService)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/nodeOutputStore', () => ({
|
||||
useNodeOutputStore: vi.fn(() => mockNodeOutputStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspaceStore', () => ({
|
||||
useWorkspaceStore: vi.fn(() => ({
|
||||
workflow: mockWorkspaceWorkflow
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -74,15 +115,177 @@ function createTestFile(name: string, type: string): File {
|
||||
return new File([''], name, { type })
|
||||
}
|
||||
|
||||
type ComfyAppMissingModelPipelineTarget = {
|
||||
runMissingModelPipeline: (
|
||||
graphData: ComfyWorkflowJSON,
|
||||
options?: { silent?: boolean; missingNodeTypes?: string[] }
|
||||
) => Promise<{
|
||||
missingModels: ModelFile[]
|
||||
confirmedCandidates: MissingModelCandidate[]
|
||||
}>
|
||||
}
|
||||
|
||||
function createWorkflowGraphData(): ComfyWorkflowJSON {
|
||||
return {
|
||||
last_node_id: 0,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4
|
||||
}
|
||||
}
|
||||
|
||||
describe('ComfyApp', () => {
|
||||
let app: ComfyApp
|
||||
let mockCanvas: LGraphCanvas
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
app = new ComfyApp()
|
||||
mockCanvas = createMockCanvas() as LGraphCanvas
|
||||
app.canvas = mockCanvas as LGraphCanvas
|
||||
mockExtensionService.invokeExtensions.mockReturnValue([])
|
||||
mockExtensionService.invokeExtensionsAsync.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
describe('refreshComboInNodes', () => {
|
||||
it('shows success toast and removes the pending toast after node defs reload', async () => {
|
||||
app.vueAppReady = true
|
||||
vi.spyOn(app, 'reloadNodeDefs').mockResolvedValue()
|
||||
|
||||
await app.refreshComboInNodes()
|
||||
|
||||
expect(mockToastStore.add).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ severity: 'info' })
|
||||
)
|
||||
expect(mockToastStore.add).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ severity: 'success' })
|
||||
)
|
||||
expect(mockToastStore.remove).toHaveBeenCalledWith(
|
||||
mockToastStore.add.mock.calls[0][0]
|
||||
)
|
||||
})
|
||||
|
||||
it('shows failure toast, removes the pending toast, and rethrows reload failures', async () => {
|
||||
app.vueAppReady = true
|
||||
const error = new Error('object_info failed')
|
||||
vi.spyOn(app, 'reloadNodeDefs').mockRejectedValue(error)
|
||||
|
||||
await expect(app.refreshComboInNodes()).rejects.toThrow(error)
|
||||
|
||||
expect(mockToastStore.add).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ severity: 'error' })
|
||||
)
|
||||
expect(mockToastStore.remove).toHaveBeenCalledWith(
|
||||
mockToastStore.add.mock.calls[0][0]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('refreshMissingModels', () => {
|
||||
function mockRefreshMissingModelsApp(
|
||||
graphData: ComfyWorkflowJSON,
|
||||
candidates: MissingModelCandidate[] = []
|
||||
) {
|
||||
mockWorkspaceWorkflow.activeWorkflow = null
|
||||
Reflect.set(app, 'rootGraphInternal', {
|
||||
nodes: [],
|
||||
serialize: vi.fn(() => graphData)
|
||||
})
|
||||
vi.spyOn(app, 'reloadNodeDefs').mockResolvedValue()
|
||||
const appWithPrivate =
|
||||
app as unknown as ComfyAppMissingModelPipelineTarget
|
||||
const pipelineSpy = vi
|
||||
.spyOn(appWithPrivate, 'runMissingModelPipeline')
|
||||
.mockResolvedValue({
|
||||
missingModels: [],
|
||||
confirmedCandidates: []
|
||||
})
|
||||
useMissingModelStore().missingModelCandidates = candidates
|
||||
return pipelineSpy
|
||||
}
|
||||
|
||||
it('reuses active workflow model metadata when refreshing the current graph', async () => {
|
||||
const graphData = createWorkflowGraphData()
|
||||
const activeModels = [
|
||||
{
|
||||
name: 'embedded.safetensors',
|
||||
url: 'https://example.com/embedded.safetensors',
|
||||
directory: 'checkpoints'
|
||||
}
|
||||
]
|
||||
const pipelineSpy = mockRefreshMissingModelsApp(graphData, [
|
||||
{
|
||||
nodeId: '1',
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'candidate.safetensors',
|
||||
url: 'https://example.com/candidate.safetensors',
|
||||
directory: 'checkpoints',
|
||||
isMissing: true,
|
||||
isAssetSupported: true
|
||||
}
|
||||
])
|
||||
mockWorkspaceWorkflow.activeWorkflow = {
|
||||
activeState: { models: activeModels }
|
||||
} as LoadedComfyWorkflow
|
||||
|
||||
await app.refreshMissingModels({ silent: false })
|
||||
|
||||
expect(app.reloadNodeDefs).toHaveBeenCalled()
|
||||
expect(pipelineSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ models: activeModels }),
|
||||
{ silent: false }
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to current missing model metadata when workflow state has no models', async () => {
|
||||
const graphData = createWorkflowGraphData()
|
||||
const pipelineSpy = mockRefreshMissingModelsApp(graphData, [
|
||||
{
|
||||
nodeId: '1',
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'candidate.safetensors',
|
||||
url: 'https://example.com/candidate.safetensors',
|
||||
directory: 'checkpoints',
|
||||
hash: 'abc123',
|
||||
hashType: 'sha256',
|
||||
isMissing: true,
|
||||
isAssetSupported: true
|
||||
},
|
||||
{
|
||||
nodeId: '2',
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'missing-url.safetensors',
|
||||
directory: 'checkpoints',
|
||||
isMissing: true,
|
||||
isAssetSupported: true
|
||||
}
|
||||
])
|
||||
|
||||
await app.refreshMissingModels()
|
||||
|
||||
expect(pipelineSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
models: [
|
||||
{
|
||||
name: 'candidate.safetensors',
|
||||
url: 'https://example.com/candidate.safetensors',
|
||||
directory: 'checkpoints',
|
||||
hash: 'abc123',
|
||||
hash_type: 'sha256'
|
||||
}
|
||||
]
|
||||
}),
|
||||
{ silent: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleFileList', () => {
|
||||
|
||||
@@ -27,8 +27,8 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { WorkflowOpenSource } from '@/platform/telemetry/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { updatePendingWarnings } from '@/platform/workflow/core/utils/pendingWarnings'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import type { PendingWarnings } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowValidation } from '@/platform/workflow/validation/composables/useWorkflowValidation'
|
||||
import type {
|
||||
@@ -127,7 +127,6 @@ import {
|
||||
findLegacyRerouteNodes,
|
||||
noNativeReroutes
|
||||
} from '@/utils/migration/migrateReroute'
|
||||
|
||||
import { deserialiseAndCreate } from '@/utils/vintageClipboard'
|
||||
|
||||
import { type ComfyApi, PromptExecutionError, api } from './api'
|
||||
@@ -155,6 +154,11 @@ import {
|
||||
pasteVideoNodes
|
||||
} from '@/composables/usePaste'
|
||||
|
||||
interface MissingModelPipelineOptions {
|
||||
missingNodeTypes?: MissingNodeType[]
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
|
||||
export function sanitizeNodeName(string: string) {
|
||||
@@ -1450,11 +1454,10 @@ export class ComfyApp {
|
||||
)
|
||||
|
||||
if (!skipAssetScans) {
|
||||
await this.runMissingModelPipeline(
|
||||
graphData,
|
||||
activeMissingNodeTypes,
|
||||
silentAssetErrors
|
||||
)
|
||||
await this.runMissingModelPipeline(graphData, {
|
||||
missingNodeTypes: activeMissingNodeTypes,
|
||||
silent: silentAssetErrors
|
||||
})
|
||||
|
||||
await this.runMissingMediaPipeline(silentAssetErrors)
|
||||
}
|
||||
@@ -1476,10 +1479,13 @@ export class ComfyApp {
|
||||
|
||||
private async runMissingModelPipeline(
|
||||
graphData: ComfyWorkflowJSON,
|
||||
missingNodeTypes: MissingNodeType[],
|
||||
silent: boolean = false
|
||||
): Promise<{ missingModels: ModelFile[] }> {
|
||||
{ missingNodeTypes, silent = false }: MissingModelPipelineOptions = {}
|
||||
): Promise<{
|
||||
missingModels: ModelFile[]
|
||||
confirmedCandidates: MissingModelCandidate[]
|
||||
}> {
|
||||
const missingModelStore = useMissingModelStore()
|
||||
const controller = missingModelStore.createVerificationAbortController()
|
||||
|
||||
const getDirectory = (nodeType: string) =>
|
||||
useModelToNodeStore().getCategoryForNodeType(nodeType)
|
||||
@@ -1539,22 +1545,13 @@ export class ComfyApp {
|
||||
)
|
||||
|
||||
const activeWf = useWorkspaceStore().workflow.activeWorkflow
|
||||
if (activeWf) {
|
||||
activeWf.pendingWarnings = {
|
||||
...activeWf.pendingWarnings,
|
||||
missingNodeTypes: missingNodeTypes.length
|
||||
? missingNodeTypes
|
||||
: undefined,
|
||||
missingModelCandidates: confirmedCandidates.length
|
||||
? confirmedCandidates
|
||||
: undefined
|
||||
}
|
||||
this.cleanupPendingWarnings(activeWf)
|
||||
}
|
||||
updatePendingWarnings(activeWf, {
|
||||
...(missingNodeTypes ? { missingNodeTypes } : {}),
|
||||
missingModelCandidates: confirmedCandidates
|
||||
})
|
||||
|
||||
if (enrichedCandidates.length) {
|
||||
if (isCloud) {
|
||||
const controller = missingModelStore.createVerificationAbortController()
|
||||
void verifyAssetSupportedCandidates(
|
||||
enrichedCandidates,
|
||||
controller.signal
|
||||
@@ -1566,11 +1563,7 @@ export class ComfyApp {
|
||||
const confirmed = enrichedCandidates.filter((c) =>
|
||||
isMissingCandidateActive(this.rootGraph, c)
|
||||
)
|
||||
if (confirmed.length) {
|
||||
useExecutionErrorStore().surfaceMissingModels(confirmed, {
|
||||
silent
|
||||
})
|
||||
}
|
||||
useExecutionErrorStore().surfaceMissingModels(confirmed, { silent })
|
||||
this.cacheModelCandidates(activeWf, confirmed)
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -1588,9 +1581,11 @@ export class ComfyApp {
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const controller = missingModelStore.createVerificationAbortController()
|
||||
const confirmed = enrichedCandidates.filter((c) => c.isMissing === true)
|
||||
if (confirmed.length) {
|
||||
if (!confirmed.length) {
|
||||
useExecutionErrorStore().surfaceMissingModels([], { silent })
|
||||
this.cacheModelCandidates(activeWf, [])
|
||||
} else {
|
||||
void api
|
||||
.getFolderPaths()
|
||||
.then((paths) => {
|
||||
@@ -1625,21 +1620,49 @@ export class ComfyApp {
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
useExecutionErrorStore().surfaceMissingModels([], { silent })
|
||||
this.cacheModelCandidates(activeWf, [])
|
||||
}
|
||||
|
||||
return { missingModels }
|
||||
return { missingModels, confirmedCandidates }
|
||||
}
|
||||
|
||||
private cleanupPendingWarnings(wf: {
|
||||
pendingWarnings: PendingWarnings | null
|
||||
}) {
|
||||
if (
|
||||
!wf.pendingWarnings?.missingNodeTypes &&
|
||||
!wf.pendingWarnings?.missingModelCandidates &&
|
||||
!wf.pendingWarnings?.missingMediaCandidates
|
||||
) {
|
||||
wf.pendingWarnings = null
|
||||
}
|
||||
async refreshMissingModels(options: { silent?: boolean } = {}): Promise<{
|
||||
missingModels: ModelFile[]
|
||||
confirmedCandidates: MissingModelCandidate[]
|
||||
}> {
|
||||
await this.reloadNodeDefs()
|
||||
const graphData = this.rootGraph.serialize() as unknown as ComfyWorkflowJSON
|
||||
const activeWorkflowState =
|
||||
useWorkspaceStore().workflow.activeWorkflow?.activeState
|
||||
const currentModelMetadata =
|
||||
useMissingModelStore()
|
||||
.missingModelCandidates?.filter(
|
||||
(
|
||||
candidate
|
||||
): candidate is MissingModelCandidate & {
|
||||
url: string
|
||||
directory: string
|
||||
} => !!candidate.url && !!candidate.directory
|
||||
)
|
||||
.map((candidate) => ({
|
||||
name: candidate.name,
|
||||
url: candidate.url,
|
||||
directory: candidate.directory,
|
||||
hash: candidate.hash,
|
||||
hash_type: candidate.hashType
|
||||
})) ?? []
|
||||
const models = activeWorkflowState?.models?.length
|
||||
? activeWorkflowState.models
|
||||
: currentModelMetadata
|
||||
|
||||
return this.runMissingModelPipeline(
|
||||
models.length ? { ...graphData, models } : graphData,
|
||||
{
|
||||
silent: options.silent ?? true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private cacheModelCandidates(
|
||||
@@ -1647,11 +1670,9 @@ export class ComfyApp {
|
||||
confirmed: MissingModelCandidate[]
|
||||
) {
|
||||
if (!wf) return
|
||||
wf.pendingWarnings = {
|
||||
...wf.pendingWarnings,
|
||||
missingModelCandidates: confirmed.length ? confirmed : undefined
|
||||
}
|
||||
this.cleanupPendingWarnings(wf)
|
||||
updatePendingWarnings(wf, {
|
||||
missingModelCandidates: confirmed
|
||||
})
|
||||
}
|
||||
|
||||
private cacheMediaCandidates(
|
||||
@@ -1659,11 +1680,9 @@ export class ComfyApp {
|
||||
confirmed: MissingMediaCandidate[]
|
||||
) {
|
||||
if (!wf) return
|
||||
wf.pendingWarnings = {
|
||||
...wf.pendingWarnings,
|
||||
missingMediaCandidates: confirmed.length ? confirmed : undefined
|
||||
}
|
||||
this.cleanupPendingWarnings(wf)
|
||||
updatePendingWarnings(wf, {
|
||||
missingMediaCandidates: confirmed
|
||||
})
|
||||
}
|
||||
|
||||
private async runMissingMediaPipeline(
|
||||
@@ -2224,18 +2243,9 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh combo list on whole nodes
|
||||
* Reload node definitions and refresh combo lists on all nodes.
|
||||
*/
|
||||
async refreshComboInNodes() {
|
||||
const requestToastMessage: ToastMessageOptions = {
|
||||
severity: 'info',
|
||||
summary: t('g.update'),
|
||||
detail: t('toastMessages.updateRequested')
|
||||
}
|
||||
if (this.vueAppReady) {
|
||||
useToastStore().add(requestToastMessage)
|
||||
}
|
||||
|
||||
async reloadNodeDefs() {
|
||||
const defs = await this.getNodeDefs()
|
||||
for (const nodeId in defs) {
|
||||
this.registerNodeDef(nodeId, defs[nodeId])
|
||||
@@ -2289,13 +2299,46 @@ export class ComfyApp {
|
||||
|
||||
if (this.vueAppReady) {
|
||||
this.updateVueAppNodeDefs(defs)
|
||||
useToastStore().remove(requestToastMessage)
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: t('g.updated'),
|
||||
detail: t('toastMessages.nodeDefinitionsUpdated'),
|
||||
life: 1000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh combo list on whole nodes
|
||||
*/
|
||||
async refreshComboInNodes() {
|
||||
const requestToastMessage: ToastMessageOptions = {
|
||||
severity: 'info',
|
||||
summary: t('g.update'),
|
||||
detail: t('toastMessages.updateRequested')
|
||||
}
|
||||
if (this.vueAppReady) {
|
||||
useToastStore().add(requestToastMessage)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.reloadNodeDefs()
|
||||
|
||||
if (this.vueAppReady) {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: t('g.updated'),
|
||||
detail: t('toastMessages.nodeDefinitionsUpdated'),
|
||||
life: 1000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.vueAppReady) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.nodeDefinitionsUpdateFailed')
|
||||
})
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
if (this.vueAppReady) {
|
||||
useToastStore().remove(requestToastMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -646,7 +646,9 @@ export class ComfyUI {
|
||||
$el('button', {
|
||||
id: 'comfy-refresh-button',
|
||||
textContent: 'Refresh',
|
||||
onclick: () => app.refreshComboInNodes()
|
||||
onclick: () => {
|
||||
void app.refreshComboInNodes().catch(() => {})
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clipspace-button',
|
||||
|
||||
Reference in New Issue
Block a user