mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-03 00:35:32 -03:00
feat(downloads): allow manual checkpoint/diffusion root override in download modal
This commit is contained in:
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "Wählen Sie ein Stammverzeichnis",
|
||||
"selectModelRoot": "Modell-Stammverzeichnis auswählen:",
|
||||
"selectTypeRoot": "{type}-Stammverzeichnis auswählen:",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "Zielordnerpfad:",
|
||||
"browseFolders": "Ordner durchsuchen:",
|
||||
"createNewFolder": "Neuen Ordner erstellen",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "Select a root directory",
|
||||
"selectModelRoot": "Select Model Root:",
|
||||
"selectTypeRoot": "Select {type} Root:",
|
||||
"routingOverride": {
|
||||
"label": "Destination type:",
|
||||
"tooltip": "Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "Target Folder Path:",
|
||||
"browseFolders": "Browse Folders:",
|
||||
"createNewFolder": "Create new folder",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "Selecciona un directorio raíz",
|
||||
"selectModelRoot": "Seleccionar raíz del modelo:",
|
||||
"selectTypeRoot": "Seleccionar raíz de {type}:",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "Ruta de carpeta de destino:",
|
||||
"browseFolders": "Explorar carpetas:",
|
||||
"createNewFolder": "Crear nueva carpeta",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "Sélectionner un répertoire racine",
|
||||
"selectModelRoot": "Sélectionner la racine du modèle :",
|
||||
"selectTypeRoot": "Sélectionner la racine {type} :",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "Chemin du dossier cible :",
|
||||
"browseFolders": "Parcourir les dossiers :",
|
||||
"createNewFolder": "Créer un nouveau dossier",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "בחר ספריית שורש",
|
||||
"selectModelRoot": "בחר שורש מודל:",
|
||||
"selectTypeRoot": "בחר שורש {type}:",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "נתיב תיקיית יעד:",
|
||||
"browseFolders": "דפדף בתיקיות:",
|
||||
"createNewFolder": "צור תיקייה חדשה",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "ルートディレクトリを選択",
|
||||
"selectModelRoot": "モデルルートを選択:",
|
||||
"selectTypeRoot": "{type}ルートを選択:",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "ターゲットフォルダパス:",
|
||||
"browseFolders": "フォルダを参照:",
|
||||
"createNewFolder": "新しいフォルダを作成",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "루트 디렉토리를 선택하세요",
|
||||
"selectModelRoot": "모델 루트 선택:",
|
||||
"selectTypeRoot": "{type} 루트 선택:",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "대상 폴더 경로:",
|
||||
"browseFolders": "폴더 탐색:",
|
||||
"createNewFolder": "새 폴더 만들기",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "Выберите корневую папку",
|
||||
"selectModelRoot": "Выберите корень моделей:",
|
||||
"selectTypeRoot": "Выберите корень {type}:",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "Путь к целевой папке:",
|
||||
"browseFolders": "Обзор папок:",
|
||||
"createNewFolder": "Создать новую папку",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "选择根目录",
|
||||
"selectModelRoot": "选择模型根目录:",
|
||||
"selectTypeRoot": "选择 {type} 根目录:",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "目标文件夹路径:",
|
||||
"browseFolders": "浏览文件夹:",
|
||||
"createNewFolder": "新建文件夹",
|
||||
|
||||
@@ -1563,6 +1563,10 @@
|
||||
"selectRootDirectory": "選擇根目錄",
|
||||
"selectModelRoot": "選擇模型根目錄:",
|
||||
"selectTypeRoot": "選擇 {type} 根目錄:",
|
||||
"routingOverride": {
|
||||
"label": "[TODO: Translate] Destination type:",
|
||||
"tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download."
|
||||
},
|
||||
"targetFolderPath": "目標資料夾路徑:",
|
||||
"browseFolders": "瀏覽資料夾:",
|
||||
"createNewFolder": "建立新資料夾",
|
||||
|
||||
@@ -1088,3 +1088,56 @@
|
||||
[data-theme="dark"] .batch-preview-group-body {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
|
||||
/* Routing override segmented toggle (Checkpoint / Diffusion Model) shown on
|
||||
the checkpoints page of the download modal when auto-routing can misdetect. */
|
||||
.routing-override-group {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.routing-override-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.routing-override-toggle {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.routing-override-option {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
opacity: 0.75;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-base);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.routing-override-option:first-child {
|
||||
border-radius: var(--border-radius-xs) 0 0 var(--border-radius-xs);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.routing-override-option:last-child {
|
||||
border-radius: 0 var(--border-radius-xs) var(--border-radius-xs) 0;
|
||||
}
|
||||
|
||||
.routing-override-option:not(.active):hover,
|
||||
.routing-override-option:not(.active):focus-visible {
|
||||
border-color: var(--lora-accent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.routing-override-option.active {
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
border-color: var(--lora-accent);
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ export class DownloadManager {
|
||||
this.selectedFolder = '';
|
||||
this.apiClient = null;
|
||||
this.useDefaultPath = false;
|
||||
// Manual checkpoint/diffusion root override on the location step:
|
||||
// null follows the auto classification, true/false is user-picked.
|
||||
this._routingOverride = null;
|
||||
|
||||
// Multi-file selection state: selectedFile stays the first selected
|
||||
// file for backward compatibility with single-file flows (#1058).
|
||||
@@ -127,6 +130,11 @@ export class DownloadManager {
|
||||
// Default path toggle handler
|
||||
document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath);
|
||||
|
||||
// Checkpoint/diffusion routing override toggle (checkpoints page only)
|
||||
document.querySelectorAll('#routingOverrideGroup .routing-override-option').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.handleRoutingOverrideChange(btn.dataset.routing === 'diffusion'));
|
||||
});
|
||||
|
||||
// Auto-append newline after pasting a URL so users can paste multiple URLs in succession
|
||||
setupAutoNewlineOnPaste('modelUrl');
|
||||
}
|
||||
@@ -180,6 +188,7 @@ export class DownloadManager {
|
||||
this.selectedFiles = [];
|
||||
this._lastDownloadError = null;
|
||||
this._isDiffusionModel = false;
|
||||
this._routingOverride = null;
|
||||
|
||||
this.selectedFolder = '';
|
||||
this.batchModels = [];
|
||||
@@ -974,48 +983,12 @@ export class DownloadManager {
|
||||
try {
|
||||
this._isDiffusionModel = await this._resolveIsDiffusionModel();
|
||||
this._otherSubType = await this._resolveOtherSubType();
|
||||
// Each location-step entry starts from the auto classification;
|
||||
// the user can override it via the checkpoint/diffusion toggle.
|
||||
this._routingOverride = null;
|
||||
this._syncRoutingOverrideToggle();
|
||||
|
||||
let rootsData;
|
||||
if (this._isDiffusionModel && this.apiClient.modelType === 'checkpoints') {
|
||||
rootsData = await this.apiClient.fetchModelRoots('diffusion_model');
|
||||
} else if (this.apiClient.modelType === 'other' && this._otherSubType) {
|
||||
rootsData = await this.apiClient.fetchModelRoots(this._otherSubType);
|
||||
} else {
|
||||
// An undecidable other sub_type (null) intentionally lands
|
||||
// here: fetchModelRoots() lists all other roots so the user
|
||||
// can pick manually.
|
||||
rootsData = await this.apiClient.fetchModelRoots();
|
||||
}
|
||||
const modelRoot = document.getElementById('modelRoot');
|
||||
modelRoot.innerHTML = rootsData.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
let defaultRoot;
|
||||
let subtypeDisplay;
|
||||
if (this.apiClient.modelType === 'other') {
|
||||
const otherDefaultRoots = state.global.settings.default_other_roots || {};
|
||||
defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : '';
|
||||
subtypeDisplay = this._otherSubType
|
||||
? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType)
|
||||
: this.apiClient.apiConfig.config.displayName;
|
||||
} else {
|
||||
const singularType = this._isDiffusionModel
|
||||
? 'unet'
|
||||
: this.apiClient.modelType.replace(/s$/, '');
|
||||
const defaultRootKey = `default_${singularType}_root`;
|
||||
defaultRoot = state.global.settings[defaultRootKey];
|
||||
subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName;
|
||||
}
|
||||
console.log('Default root:', defaultRoot);
|
||||
console.log('Available roots:', rootsData.roots);
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
console.log(`Setting default root: ${defaultRoot}`);
|
||||
modelRoot.value = defaultRoot;
|
||||
}
|
||||
|
||||
document.getElementById('modelRootLabel').textContent =
|
||||
translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
||||
await this._populateModelRoots();
|
||||
|
||||
// Set autocomplete="off" on folderPath input
|
||||
const folderPathInput = document.getElementById('folderPath');
|
||||
@@ -1035,6 +1008,7 @@ export class DownloadManager {
|
||||
});
|
||||
|
||||
// Setup model root change handler
|
||||
const modelRoot = document.getElementById('modelRoot');
|
||||
modelRoot.addEventListener('change', async () => {
|
||||
await this.initializeFolderTree();
|
||||
this.updateTargetPath();
|
||||
@@ -1049,6 +1023,116 @@ export class DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective checkpoint/diffusion routing for the location step: the
|
||||
* user's manual override when set, otherwise the auto classification.
|
||||
*/
|
||||
_getEffectiveIsDiffusionModel() {
|
||||
return this._routingOverride ?? this._isDiffusionModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the root list for the effective routing group and repopulate
|
||||
* the root dropdown, preselecting the matching default root and
|
||||
* updating the root label.
|
||||
*/
|
||||
async _populateModelRoots() {
|
||||
const isDiffusion = this._getEffectiveIsDiffusionModel();
|
||||
|
||||
let rootsData;
|
||||
if (isDiffusion && this.apiClient.modelType === 'checkpoints') {
|
||||
rootsData = await this.apiClient.fetchModelRoots('diffusion_model');
|
||||
} else if (this.apiClient.modelType === 'other' && this._otherSubType) {
|
||||
rootsData = await this.apiClient.fetchModelRoots(this._otherSubType);
|
||||
} else {
|
||||
// An undecidable other sub_type (null) intentionally lands
|
||||
// here: fetchModelRoots() lists all other roots so the user
|
||||
// can pick manually.
|
||||
rootsData = await this.apiClient.fetchModelRoots();
|
||||
}
|
||||
const modelRoot = document.getElementById('modelRoot');
|
||||
modelRoot.innerHTML = rootsData.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
let defaultRoot;
|
||||
let subtypeDisplay;
|
||||
if (this.apiClient.modelType === 'other') {
|
||||
const otherDefaultRoots = state.global.settings.default_other_roots || {};
|
||||
defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : '';
|
||||
subtypeDisplay = this._otherSubType
|
||||
? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType)
|
||||
: this.apiClient.apiConfig.config.displayName;
|
||||
} else {
|
||||
const singularType = isDiffusion
|
||||
? 'unet'
|
||||
: this.apiClient.modelType.replace(/s$/, '');
|
||||
const defaultRootKey = `default_${singularType}_root`;
|
||||
defaultRoot = state.global.settings[defaultRootKey];
|
||||
subtypeDisplay = isDiffusion ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName;
|
||||
}
|
||||
console.log('Default root:', defaultRoot);
|
||||
console.log('Available roots:', rootsData.roots);
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
console.log(`Setting default root: ${defaultRoot}`);
|
||||
modelRoot.value = defaultRoot;
|
||||
}
|
||||
|
||||
document.getElementById('modelRootLabel').textContent =
|
||||
translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the checkpoint/diffusion override toggle on the checkpoints page
|
||||
* (hidden elsewhere) and reflect the effective routing on its options.
|
||||
*/
|
||||
_syncRoutingOverrideToggle() {
|
||||
const group = document.getElementById('routingOverrideGroup');
|
||||
if (!group) return;
|
||||
const isCheckpoints = this.apiClient.modelType === 'checkpoints';
|
||||
group.style.display = isCheckpoints ? '' : 'none';
|
||||
if (!isCheckpoints) return;
|
||||
|
||||
const effective = this._getEffectiveIsDiffusionModel();
|
||||
group.querySelectorAll('.routing-override-option').forEach(btn => {
|
||||
const active = (btn.dataset.routing === 'diffusion') === effective;
|
||||
btn.classList.toggle('active', active);
|
||||
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the user switching the checkpoint/diffusion routing toggle.
|
||||
* The override must never be silently ignored: the backend re-derives
|
||||
* the routing group only under "use default path", so overriding turns
|
||||
* that off for this session (the localStorage preference is left
|
||||
* untouched) to guarantee the picked root is the actual destination.
|
||||
*/
|
||||
async handleRoutingOverrideChange(isDiffusion) {
|
||||
if (this.apiClient.modelType !== 'checkpoints') return;
|
||||
|
||||
const overridden = isDiffusion !== this._isDiffusionModel;
|
||||
this._routingOverride = overridden ? isDiffusion : null;
|
||||
|
||||
if (overridden && this.useDefaultPath) {
|
||||
this.useDefaultPath = false;
|
||||
const toggleInput = document.getElementById('useDefaultPath');
|
||||
if (toggleInput) {
|
||||
toggleInput.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
this._syncRoutingOverrideToggle();
|
||||
|
||||
try {
|
||||
await this._populateModelRoots();
|
||||
await this.initializeFolderTree();
|
||||
this.updatePathSelectionUI();
|
||||
} catch (error) {
|
||||
showToast('toast.downloads.loadError', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether this download routes to the diffusion model (unet)
|
||||
* roots rather than the checkpoint roots. The backend owns the routing
|
||||
@@ -2554,13 +2638,13 @@ export class DownloadManager {
|
||||
const modelRoot = document.getElementById('modelRoot').value;
|
||||
const config = this.apiClient.apiConfig.config;
|
||||
|
||||
const subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : config.displayName;
|
||||
const subtypeDisplay = this._getEffectiveIsDiffusionModel() ? 'Diffusion Model' : config.displayName;
|
||||
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
||||
|
||||
if (modelRoot) {
|
||||
if (this.useDefaultPath) {
|
||||
try {
|
||||
const singularType = this._isDiffusionModel
|
||||
const singularType = this._getEffectiveIsDiffusionModel()
|
||||
? 'unet'
|
||||
: this.apiClient.modelType.replace(/s$/, '');
|
||||
const templates = state.global?.settings?.download_path_templates;
|
||||
|
||||
@@ -80,6 +80,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Model Root Selection (always visible) -->
|
||||
<div class="input-group routing-override-group" id="routingOverrideGroup" style="display: none;"
|
||||
title="{{ t('modals.download.routingOverride.tooltip') }}">
|
||||
<label id="routingOverrideLabel">{{ t('modals.download.routingOverride.label') }}</label>
|
||||
<div class="routing-override-toggle" role="group" aria-label="{{ t('modals.download.routingOverride.label') }}">
|
||||
<button type="button" class="routing-override-option" data-routing="checkpoint">{{ t('checkpoints.modelTypes.checkpoint') }}</button>
|
||||
<button type="button" class="routing-override-option" data-routing="diffusion">{{ t('checkpoints.modelTypes.diffusion_model') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="modelRoot" id="modelRootLabel">{{ t('modals.download.selectModelRoot') }}</label>
|
||||
<select id="modelRoot"></select>
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const {
|
||||
DOWNLOAD_MANAGER_MODULE,
|
||||
MODAL_MANAGER_MODULE,
|
||||
UI_HELPERS_MODULE,
|
||||
STATE_MODULE,
|
||||
LOADING_MANAGER_MODULE,
|
||||
API_FACTORY_MODULE,
|
||||
STORAGE_HELPERS_MODULE,
|
||||
FOLDER_TREE_MANAGER_MODULE,
|
||||
I18N_HELPERS_MODULE,
|
||||
SUMMARY_MODULE,
|
||||
OTHER_MODELS_MODULE,
|
||||
} = vi.hoisted(() => ({
|
||||
DOWNLOAD_MANAGER_MODULE: new URL('../../../static/js/managers/DownloadManager.js', import.meta.url).pathname,
|
||||
MODAL_MANAGER_MODULE: new URL('../../../static/js/managers/ModalManager.js', import.meta.url).pathname,
|
||||
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
|
||||
STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname,
|
||||
LOADING_MANAGER_MODULE: new URL('../../../static/js/managers/LoadingManager.js', import.meta.url).pathname,
|
||||
API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname,
|
||||
STORAGE_HELPERS_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname,
|
||||
FOLDER_TREE_MANAGER_MODULE: new URL('../../../static/js/components/FolderTreeManager.js', import.meta.url).pathname,
|
||||
I18N_HELPERS_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
|
||||
SUMMARY_MODULE: new URL('../../../static/js/components/DownloadBatchSummaryModal.js', import.meta.url).pathname,
|
||||
OTHER_MODELS_MODULE: new URL('../../../static/js/utils/otherModels.js', import.meta.url).pathname,
|
||||
}));
|
||||
|
||||
vi.mock(MODAL_MANAGER_MODULE, () => ({
|
||||
modalManager: { showModal: vi.fn(), closeModal: vi.fn() },
|
||||
}));
|
||||
vi.mock(UI_HELPERS_MODULE, () => ({
|
||||
showToast: vi.fn(),
|
||||
showActionToast: vi.fn(),
|
||||
setupAutoNewlineOnPaste: vi.fn(),
|
||||
}));
|
||||
vi.mock(STATE_MODULE, () => ({
|
||||
state: { global: { settings: {} }, loadingManager: {} },
|
||||
}));
|
||||
vi.mock(LOADING_MANAGER_MODULE, () => ({
|
||||
LoadingManager: vi.fn(() => ({})),
|
||||
}));
|
||||
vi.mock(API_FACTORY_MODULE, () => ({
|
||||
getModelApiClient: vi.fn(),
|
||||
resetAndReload: vi.fn(),
|
||||
}));
|
||||
vi.mock(STORAGE_HELPERS_MODULE, () => ({
|
||||
getStorageItem: vi.fn((_key, defaultValue) => defaultValue),
|
||||
setStorageItem: vi.fn(),
|
||||
}));
|
||||
vi.mock(FOLDER_TREE_MANAGER_MODULE, () => ({
|
||||
FolderTreeManager: vi.fn(() => ({})),
|
||||
}));
|
||||
vi.mock(I18N_HELPERS_MODULE, () => ({
|
||||
translate: vi.fn((_key, _vars, fallback) => fallback ?? ''),
|
||||
}));
|
||||
vi.mock(SUMMARY_MODULE, () => ({
|
||||
showDownloadBatchSummary: vi.fn(),
|
||||
}));
|
||||
vi.mock(OTHER_MODELS_MODULE, () => ({
|
||||
enableOtherModels: vi.fn(),
|
||||
openOtherModelsSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
const { DownloadManager } = await import(DOWNLOAD_MANAGER_MODULE);
|
||||
const { state } = await import(STATE_MODULE);
|
||||
const { setStorageItem } = await import(STORAGE_HELPERS_MODULE);
|
||||
|
||||
describe('DownloadManager checkpoint/diffusion routing override', () => {
|
||||
let manager;
|
||||
|
||||
function setupDom() {
|
||||
document.body.innerHTML = `
|
||||
<div id="routingOverrideGroup" style="display:none">
|
||||
<button class="routing-override-option" data-routing="checkpoint"></button>
|
||||
<button class="routing-override-option" data-routing="diffusion"></button>
|
||||
</div>
|
||||
<select id="modelRoot"></select>
|
||||
<label id="modelRootLabel"></label>
|
||||
<input id="folderPath" />
|
||||
<input type="checkbox" id="useDefaultPath" />
|
||||
<div id="manualPathSelection"></div>
|
||||
<div id="targetPathDisplay"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createManager(modelType, displayName = 'Model') {
|
||||
manager = new DownloadManager();
|
||||
manager.apiClient = {
|
||||
modelType,
|
||||
apiConfig: { config: { displayName } },
|
||||
fetchModelRoots: vi.fn(),
|
||||
};
|
||||
manager.selectedFile = null;
|
||||
manager.selectedFiles = [];
|
||||
manager.currentVersion = null;
|
||||
manager.initializeFolderTree = vi.fn().mockResolvedValue();
|
||||
manager.folderTreeManager = { init: vi.fn(), getSelectedPath: vi.fn(() => '') };
|
||||
manager.loadDefaultPathSetting = vi.fn();
|
||||
vi.spyOn(manager, '_resolveOtherSubType').mockResolvedValue(null);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setupDom();
|
||||
state.global.settings = {};
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('shows the toggle for diffusion-routed checkpoints and marks diffusion active', async () => {
|
||||
createManager('checkpoints', 'Checkpoint');
|
||||
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(true);
|
||||
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||
success: true,
|
||||
roots: ['/models/unet'],
|
||||
});
|
||||
state.global.settings.default_unet_root = '/models/unet';
|
||||
|
||||
await manager.proceedToLocationContent();
|
||||
|
||||
const group = document.getElementById('routingOverrideGroup');
|
||||
expect(group.style.display).not.toBe('none');
|
||||
const [checkpointBtn, diffusionBtn] = group.querySelectorAll('.routing-override-option');
|
||||
expect(diffusionBtn.classList.contains('active')).toBe(true);
|
||||
expect(diffusionBtn.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(checkpointBtn.classList.contains('active')).toBe(false);
|
||||
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith('diffusion_model');
|
||||
expect(document.getElementById('modelRoot').value).toBe('/models/unet');
|
||||
});
|
||||
|
||||
it('keeps the toggle hidden on non-checkpoints pages', async () => {
|
||||
createManager('loras', 'LoRA');
|
||||
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||
success: true,
|
||||
roots: ['/models/loras'],
|
||||
});
|
||||
|
||||
await manager.proceedToLocationContent();
|
||||
|
||||
expect(document.getElementById('routingOverrideGroup').style.display).toBe('none');
|
||||
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('re-fetches diffusion roots and preselects the unet default when overriding to diffusion', async () => {
|
||||
createManager('checkpoints', 'Checkpoint');
|
||||
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false);
|
||||
manager.apiClient.fetchModelRoots
|
||||
.mockResolvedValueOnce({ success: true, roots: ['/models/checkpoints'] })
|
||||
.mockResolvedValueOnce({ success: true, roots: ['/models/unet'] });
|
||||
state.global.settings.default_unet_root = '/models/unet';
|
||||
|
||||
await manager.proceedToLocationContent();
|
||||
expect(document.getElementById('modelRoot').value).toBe('/models/checkpoints');
|
||||
|
||||
await manager.handleRoutingOverrideChange(true);
|
||||
|
||||
expect(manager.apiClient.fetchModelRoots).toHaveBeenLastCalledWith('diffusion_model');
|
||||
const modelRoot = document.getElementById('modelRoot');
|
||||
expect(Array.from(modelRoot.options).map(o => o.value)).toEqual(['/models/unet']);
|
||||
expect(modelRoot.value).toBe('/models/unet');
|
||||
const group = document.getElementById('routingOverrideGroup');
|
||||
const [checkpointBtn, diffusionBtn] = group.querySelectorAll('.routing-override-option');
|
||||
expect(checkpointBtn.classList.contains('active')).toBe(false);
|
||||
expect(diffusionBtn.classList.contains('active')).toBe(true);
|
||||
});
|
||||
|
||||
it('turns off Use Default Path for the session without persisting when overriding', async () => {
|
||||
createManager('checkpoints', 'Checkpoint');
|
||||
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false);
|
||||
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||
success: true,
|
||||
roots: ['/models/checkpoints'],
|
||||
});
|
||||
|
||||
await manager.proceedToLocationContent();
|
||||
|
||||
manager.useDefaultPath = true;
|
||||
document.getElementById('useDefaultPath').checked = true;
|
||||
|
||||
await manager.handleRoutingOverrideChange(true);
|
||||
|
||||
expect(manager.useDefaultPath).toBe(false);
|
||||
expect(document.getElementById('useDefaultPath').checked).toBe(false);
|
||||
expect(setStorageItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears the override when switching back to the auto-classified side', async () => {
|
||||
createManager('checkpoints', 'Checkpoint');
|
||||
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false);
|
||||
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||
success: true,
|
||||
roots: ['/models/checkpoints'],
|
||||
});
|
||||
|
||||
await manager.proceedToLocationContent();
|
||||
await manager.handleRoutingOverrideChange(true);
|
||||
expect(manager._routingOverride).toBe(true);
|
||||
|
||||
manager.apiClient.fetchModelRoots.mockClear();
|
||||
await manager.handleRoutingOverrideChange(false);
|
||||
|
||||
expect(manager._routingOverride).toBeNull();
|
||||
// Back on the checkpoint group: no routing argument
|
||||
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user