Files
Azornes-Comfyui-Model-Resolver/web/resolver/resolver_dialog.js
T
Dariusz L d4d1715f8a fix(web): prevent stale workflow refreshes and improve signature detection
- Guard workflow-tab-click handler with setTimeout and generation ownership check to avoid stale state updates
- Add generation-based refresh cancellation so outdated retry attempts are discarded
- Expand workflow signature to detect more model reference types, normalize widget values, and include subgraph definitions
2026-06-17 00:29:59 +02:00

202 lines
9.0 KiB
JavaScript

import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { dialogShellMethods } from "./shell/dialog_shell_methods.js";
import { lifecycleGraphMethods } from "./shell/lifecycle_graph_methods.js";
import { workflowStateMethods } from "./shell/workflow_state_methods.js";
import { workflowUpdateMethods } from "./shell/workflow_update_methods.js";
import { modelInfoMethods } from "./views/model_info_methods.js";
import { missingBrowserMethods } from "./views/missing_browser_methods.js";
import { optionsMethods } from "./views/options_methods.js";
import { tabsLoadedMethods } from "./views/tabs_loaded_methods.js";
import { downloadTargetMethods } from "./search/download_target_methods.js";
import { searchPanelMethods } from "./search/search_panel.js";
import { queueMethods } from "./actions/queue_methods.js";
import { resolveDownloadMethods } from "./actions/resolve_download_methods.js";
import { selectionMethods } from "./actions/selection_methods.js";
import { renderFormatMethods } from "./utils/render_format_methods.js";
import { getSvgIcon } from "../utils/icon_utils.js";
export class ResolverManagerDialog extends ComfyDialog {
constructor() {
super();
this.currentWorkflow = null;
this.missingModels = [];
this.allModels = null; // list of all available models for dropdown
this.downloadDirectories = null;
this.capabilities = null;
this.baseModels = null;
this.downloadSubfolders = new Map();
this.downloadTargetSelections = new Map(); // missing model key -> user/suggested download target
this.pendingResolutions = [];
this.pendingIndex = new Map(); // key -> index in pendingResolutions
this.workflowPendingSelections = new Map(); // workflow key -> queued selections
this.workflowAnalysisCaches = new Map(); // workflow key -> analyzed missing models
this.workflowLoadedModelCaches = new Map(); // workflow key -> loaded model inspector data
this.workflowSearchResultCaches = new Map(); // workflow key -> search results by missing model
this.activeDownloads = {}; // Track active downloads
this.downloadProgressByMissingKey = new Map(); // missing model key -> last known download UI state
this.queuePanelActiveTab = 'queued';
this.searchResultCache = new Map();
this.backgroundSearchJobs = new Map();
this.searchProgressTimers = new Map();
this.urnResolvePromises = new Map();
this.urnLocalMatchPromises = new Map();
this.cachedAnalysisData = null;
this.cachedWorkflowSignature = null;
this.cachedLoadedModelsData = null;
this.cachedLoadedModelsSignature = null;
this.selectedMissingModelKey = null;
this.batchSelectedMissingKeys = new Set();
this.lastBatchSelectedMissingKey = null;
this.activeFooterMenu = null;
this.batchSearchRunning = false;
this.batchSearchCancelRequested = false;
this.forceBatchSearch = false;
this.boundHandleOutsideClick = this.handleOutsideClick.bind(this);
this.activeTabStorageKey = 'model_resolver_active_tab';
this.activeTab = this.restoreActiveTab(); // Default tab
this.fullscreen = false;
this.returnToDockedAfterFullscreen = false;
this.docked = false;
this.dockContainer = null;
this.lastDockContainer = null;
this.pendingDockToSidebar = false;
this.sidebarTabId = "comfyui-model-resolver";
this.sidebarOpenModeStorageKey = "model_resolver_sidebar_open_mode";
this.missingBrowserSplitStorageKey = "model_resolver_missing_browser_detail_w";
this.dockButton = null;
this.undockButton = null;
this._floatingRectBeforeDock = null;
this._dragging = false;
this._dragStart = null;
this._analysisProgressToken = null;
this._workflowDataLoadToken = null;
this._loadedModelsLoadToken = null;
this._locateAnimationFrame = null;
this._viewportClampFrame = null;
this.activeWorkflowRouteKey = this.getActiveWorkflowRouteKey();
this.activeWorkflowSignature = null;
this._workflowRefreshTimer = null;
this._workflowRefreshRetryTimer = null;
this._workflowRefreshGeneration = 0;
this._workflowRefreshExpectedRoute = null;
this._workflowRefreshPreviousSignature = null;
this._boundHandleViewportResize = () => this.scheduleModalViewportClamp(true);
// Create backdrop overlay for click-outside-to-close
this.backdrop = $el("div.model-resolver-backdrop", {
parent: document.body
});
// Create context menu for model chips
this.contextMenu = $el("div.mr-context-menu", {
parent: document.body
}, [
$el("div.mr-context-menu-item.mr-context-menu-action-show-info", {
"data-menu-action": "showInfo",
onclick: () => this.handleContextMenuAction('showInfo')
}, [
$el("span.mr-context-menu-item-icon", {
innerHTML: getSvgIcon('eye', 'currentColor', 'mr-context-menu-item-svg')
}),
$el("span", { textContent: "Show Info" })
]),
$el("div.mr-context-menu-item.mr-context-menu-action-show-more", {
"data-menu-action": "showMore",
onclick: () => this.handleContextMenuAction('showMore')
}, [
$el("span.mr-context-menu-item-icon", {
innerHTML: getSvgIcon('database', 'currentColor', 'mr-context-menu-item-svg')
}),
$el("span", { textContent: "Show More" })
]),
$el("div.mr-context-menu-divider.mr-context-menu-divider-source", {
"data-menu-divider": "source"
}),
$el("div.mr-context-menu-item.mr-context-menu-action-civitai", {
"data-menu-action": "civitai",
onclick: () => this.handleContextMenuAction('civitai')
}, [
$el("span.mr-context-menu-item-icon", {
innerHTML: getSvgIcon('civitai', 'currentColor', 'mr-context-menu-item-svg')
}),
$el("span", { textContent: "Open in CivitAI" })
]),
$el("div.mr-context-menu-divider.mr-context-menu-divider-folder", {
"data-menu-divider": "folder"
}),
$el("div.mr-context-menu-item.mr-context-menu-action-open-folder", {
"data-menu-action": "openFolder",
onclick: () => this.handleContextMenuAction('openFolder')
}, [
$el("span.mr-context-menu-item-icon", {
innerHTML: getSvgIcon('folderOpen', 'currentColor', 'mr-context-menu-item-svg')
}),
$el("span", { textContent: "Open Containing Folder" })
])
]);
this.tooltipElement = $el("div.mr-global-tooltip", { parent: document.body });
// Selected model for context menu
this._contextMenuModel = null;
// Create dialog element using $el
this.element = $el("div.comfy-modal.model-resolver-modal", {
id: "model-resolver-modal",
parent: document.body
}, [
this.createHeader(),
this.createContent(),
this.createFooter()
]);
this.tooltipObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
this.bindTooltips(node);
}
}
}
});
this.tooltipObserver.observe(this.element, {
childList: true,
subtree: true
});
this.bindTooltips(this.element);
// Add click listener to hide context menu when clicking outside
this.boundHandleContextMenuClick = (e) => this.handleContextMenuOutsideClick(e);
document.addEventListener('click', this.boundHandleContextMenuClick);
this.boundHandleFooterMenuClick = (e) => this.handleFooterMenuOutsideClick(e);
document.addEventListener('click', this.boundHandleFooterMenuClick);
window.addEventListener('resize', this._boundHandleViewportResize);
}
}
function applyDialogMethods(...sources) {
for (const source of sources) {
for (const key of Reflect.ownKeys(source)) {
const descriptor = Object.getOwnPropertyDescriptor(source, key);
descriptor.enumerable = false;
Object.defineProperty(ResolverManagerDialog.prototype, key, descriptor);
}
}
}
applyDialogMethods(
searchPanelMethods,
modelInfoMethods,
renderFormatMethods,
workflowStateMethods,
selectionMethods,
dialogShellMethods,
downloadTargetMethods,
optionsMethods,
tabsLoadedMethods,
queueMethods,
lifecycleGraphMethods,
missingBrowserMethods,
resolveDownloadMethods,
workflowUpdateMethods
);