mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-21 13:03:18 +00:00
## 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)
251 lines
8.6 KiB
TypeScript
251 lines
8.6 KiB
TypeScript
/**
|
|
* Centralized test selectors for browser tests.
|
|
* Use data-testid attributes for stable selectors.
|
|
*/
|
|
|
|
export const TestIds = {
|
|
sidebar: {
|
|
toolbar: 'side-toolbar',
|
|
nodeLibrary: 'node-library-tree',
|
|
nodeLibrarySearch: 'node-library-search',
|
|
workflows: 'workflows-sidebar',
|
|
modeToggle: 'mode-toggle'
|
|
},
|
|
tree: {
|
|
folder: 'tree-folder',
|
|
leaf: 'tree-leaf',
|
|
node: 'tree-node'
|
|
},
|
|
canvas: {
|
|
main: 'graph-canvas',
|
|
contextMenu: 'canvas-context-menu',
|
|
toggleMinimapButton: 'toggle-minimap-button',
|
|
closeMinimapButton: 'close-minimap-button',
|
|
minimapContainer: 'minimap-container',
|
|
minimapCanvas: 'minimap-canvas',
|
|
minimapViewport: 'minimap-viewport',
|
|
minimapInteractionOverlay: 'minimap-interaction-overlay',
|
|
toggleLinkVisibilityButton: 'toggle-link-visibility-button',
|
|
zoomControlsButton: 'zoom-controls-button',
|
|
zoomInAction: 'zoom-in-action',
|
|
zoomOutAction: 'zoom-out-action',
|
|
zoomToFitAction: 'zoom-to-fit-action',
|
|
zoomPercentageInput: 'zoom-percentage-input'
|
|
},
|
|
dialogs: {
|
|
settings: 'settings-dialog',
|
|
settingsContainer: 'settings-container',
|
|
settingsTabAbout: 'settings-tab-about',
|
|
confirm: 'confirm-dialog',
|
|
errorOverlay: 'error-overlay',
|
|
errorOverlaySeeErrors: 'error-overlay-see-errors',
|
|
errorOverlayDismiss: 'error-overlay-dismiss',
|
|
errorOverlayMessages: 'error-overlay-messages',
|
|
runtimeErrorPanel: 'runtime-error-panel',
|
|
missingNodeCard: 'missing-node-card',
|
|
errorCardFindOnGithub: 'error-card-find-on-github',
|
|
errorCardCopy: 'error-card-copy',
|
|
errorDialog: 'error-dialog',
|
|
errorDialogShowReport: 'error-dialog-show-report',
|
|
errorDialogContactSupport: 'error-dialog-contact-support',
|
|
errorDialogCopyReport: 'error-dialog-copy-report',
|
|
errorDialogFindIssues: 'error-dialog-find-issues',
|
|
about: 'about-panel',
|
|
whatsNewSection: 'whats-new-section',
|
|
missingNodePacksGroup: 'error-group-missing-node',
|
|
missingModelsGroup: 'error-group-missing-model',
|
|
missingModelExpand: 'missing-model-expand',
|
|
missingModelLocate: 'missing-model-locate',
|
|
missingModelCopyName: 'missing-model-copy-name',
|
|
missingModelCopyUrl: 'missing-model-copy-url',
|
|
missingModelDownload: 'missing-model-download',
|
|
missingModelActions: 'missing-model-actions',
|
|
missingModelDownloadAll: 'missing-model-download-all',
|
|
missingModelRefresh: 'missing-model-refresh',
|
|
missingModelImportUnsupported: 'missing-model-import-unsupported',
|
|
missingMediaGroup: 'error-group-missing-media',
|
|
missingMediaRow: 'missing-media-row',
|
|
missingMediaUploadDropzone: 'missing-media-upload-dropzone',
|
|
missingMediaLibrarySelect: 'missing-media-library-select',
|
|
missingMediaStatusCard: 'missing-media-status-card',
|
|
missingMediaConfirmButton: 'missing-media-confirm-button',
|
|
missingMediaCancelButton: 'missing-media-cancel-button',
|
|
missingMediaLocateButton: 'missing-media-locate-button',
|
|
publishTabPanel: 'publish-tab-panel',
|
|
apiSignin: 'api-signin-dialog',
|
|
updatePassword: 'update-password-dialog',
|
|
cloudNotification: 'cloud-notification-dialog'
|
|
},
|
|
keybindings: {
|
|
presetMenu: 'keybinding-preset-menu'
|
|
},
|
|
nodeTemplates: {
|
|
manageDialog: 'manage-node-templates-dialog'
|
|
},
|
|
topbar: {
|
|
queueButton: 'queue-button',
|
|
queueModeMenuTrigger: 'queue-mode-menu-trigger',
|
|
saveButton: 'save-workflow-button',
|
|
subscribeButton: 'topbar-subscribe-button'
|
|
},
|
|
nodeLibrary: {
|
|
bookmarksSection: 'node-library-bookmarks-section'
|
|
},
|
|
propertiesPanel: {
|
|
root: 'properties-panel',
|
|
errorsTab: 'panel-tab-errors'
|
|
},
|
|
subgraphEditor: {
|
|
toggle: 'subgraph-editor-toggle',
|
|
shownSection: 'subgraph-editor-shown-section',
|
|
hiddenSection: 'subgraph-editor-hidden-section',
|
|
widgetToggle: 'subgraph-widget-toggle',
|
|
widgetLabel: 'subgraph-widget-label',
|
|
iconLink: 'icon-link',
|
|
iconEye: 'icon-eye',
|
|
widgetActionsMenuButton: 'widget-actions-menu-button'
|
|
},
|
|
node: {
|
|
titleInput: 'node-title-input',
|
|
pinIndicator: 'node-pin-indicator',
|
|
innerWrapper: 'node-inner-wrapper',
|
|
mainImage: 'main-image'
|
|
},
|
|
selectionToolbox: {
|
|
root: 'selection-toolbox',
|
|
colorPickerButton: 'color-picker-button',
|
|
colorPickerCurrentColor: 'color-picker-current-color',
|
|
colorBlue: 'blue',
|
|
colorRed: 'red'
|
|
},
|
|
menu: {
|
|
moreMenuContent: 'more-menu-content'
|
|
},
|
|
helpCenter: {
|
|
button: 'help-center-button',
|
|
popup: 'help-center-popup',
|
|
backdrop: 'help-center-backdrop',
|
|
menuItem: (key: string) => `help-menu-item-${key}`,
|
|
releaseItem: (version: string) => `help-release-item-${version}`
|
|
},
|
|
widgets: {
|
|
container: 'node-widgets',
|
|
widget: 'node-widget',
|
|
decrement: 'decrement',
|
|
increment: 'increment',
|
|
domWidgetTextarea: 'dom-widget-textarea',
|
|
subgraphEnterButton: 'subgraph-enter-button'
|
|
},
|
|
builder: {
|
|
footerNav: 'builder-footer-nav',
|
|
saveButton: 'builder-save-button',
|
|
saveAsButton: 'builder-save-as-button',
|
|
saveGroup: 'builder-save-group',
|
|
saveAsChevron: 'builder-save-as-chevron',
|
|
ioItem: 'builder-io-item',
|
|
ioItemTitle: 'builder-io-item-title',
|
|
ioItemSubtitle: 'builder-io-item-subtitle',
|
|
widgetActionsMenu: 'widget-actions-menu',
|
|
opensAs: 'builder-opens-as',
|
|
widgetItem: 'builder-widget-item',
|
|
widgetLabel: 'builder-widget-label',
|
|
outputPlaceholder: 'builder-output-placeholder',
|
|
connectOutputPopover: 'builder-connect-output-popover',
|
|
connectOutputSwitch: 'builder-connect-output-switch',
|
|
emptyWorkflowDialog: 'builder-empty-workflow-dialog',
|
|
emptyWorkflowBack: 'builder-empty-workflow-back',
|
|
emptyWorkflowLoadTemplate: 'builder-empty-workflow-load-template'
|
|
},
|
|
outputHistory: {
|
|
outputs: 'linear-outputs',
|
|
welcome: 'linear-welcome',
|
|
outputInfo: 'linear-output-info',
|
|
activeQueue: 'linear-job',
|
|
queueBadge: 'linear-job-badge',
|
|
inProgressItem: 'linear-in-progress-item',
|
|
historyItem: 'linear-history-item',
|
|
skeleton: 'linear-skeleton',
|
|
latentPreview: 'linear-latent-preview',
|
|
imageOutput: 'linear-image-output',
|
|
videoOutput: 'linear-video-output',
|
|
cancelRun: 'linear-cancel-run',
|
|
headerProgressBar: 'linear-header-progress-bar',
|
|
itemProgressBar: 'linear-item-progress-bar',
|
|
progressOverall: 'linear-progress-overall',
|
|
progressNode: 'linear-progress-node'
|
|
},
|
|
appMode: {
|
|
widgetItem: 'app-mode-widget-item',
|
|
welcome: 'linear-welcome',
|
|
emptyWorkflow: 'linear-welcome-empty-workflow',
|
|
buildApp: 'linear-welcome-build-app',
|
|
backToWorkflow: 'linear-welcome-back-to-workflow',
|
|
loadTemplate: 'linear-welcome-load-template',
|
|
arrangePreview: 'linear-arrange-preview',
|
|
arrangeNoOutputs: 'linear-arrange-no-outputs',
|
|
arrangeSwitchToOutputs: 'linear-arrange-switch-to-outputs',
|
|
vueNodeSwitchPopup: 'linear-vue-node-switch-popup',
|
|
vueNodeSwitchDismiss: 'linear-vue-node-switch-dismiss',
|
|
vueNodeSwitchDontShowAgain: 'linear-vue-node-switch-dont-show-again'
|
|
},
|
|
breadcrumb: {
|
|
subgraph: 'subgraph-breadcrumb',
|
|
back: 'subgraph-breadcrumb-back',
|
|
item: (key: string) => `subgraph-breadcrumb-item-${key}`,
|
|
blueprintTag: 'subgraph-breadcrumb-blueprint-tag',
|
|
missingNodesIcon: 'subgraph-breadcrumb-missing-nodes-icon',
|
|
renameInput: 'subgraph-breadcrumb-rename-input',
|
|
menu: (key: string) => `subgraph-breadcrumb-menu-${key}`
|
|
},
|
|
templates: {
|
|
content: 'template-workflows-content',
|
|
workflowCard: (id: string) => `template-workflow-${id}`
|
|
},
|
|
user: {
|
|
currentUserIndicator: 'current-user-indicator'
|
|
},
|
|
queue: {
|
|
overlayToggle: 'queue-overlay-toggle',
|
|
clearHistoryAction: 'clear-history-action'
|
|
},
|
|
errors: {
|
|
imageLoadError: 'error-loading-image',
|
|
videoLoadError: 'error-loading-video'
|
|
},
|
|
loading: {
|
|
overlay: 'loading-overlay'
|
|
},
|
|
load3d: {
|
|
recordingDuration: 'load3d-recording-duration'
|
|
},
|
|
load3dViewer: {
|
|
sidebar: 'load3d-viewer-sidebar'
|
|
},
|
|
terminal: {
|
|
root: 'terminal-root',
|
|
host: 'terminal-host',
|
|
copyButton: 'terminal-copy-button',
|
|
errorMessage: 'terminal-error-message',
|
|
loadingSpinner: 'terminal-loading-spinner'
|
|
},
|
|
imageCompare: {
|
|
viewport: 'image-compare-viewport',
|
|
empty: 'image-compare-empty',
|
|
batchNav: 'batch-nav',
|
|
beforeBatch: 'before-batch',
|
|
afterBatch: 'after-batch',
|
|
batchCounter: 'batch-counter',
|
|
batchNext: 'batch-next',
|
|
batchPrev: 'batch-prev'
|
|
}
|
|
} as const
|
|
|
|
export type TestId<K extends keyof typeof TestIds> = Exclude<
|
|
(typeof TestIds)[K][keyof (typeof TestIds)[K]],
|
|
(...args: never[]) => string
|
|
>
|
|
|
|
export type TestIdValue = {
|
|
[K in keyof typeof TestIds]: TestId<K>
|
|
}[keyof typeof TestIds]
|