refactor(web): unify download action handlers
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Window } from 'happy-dom';
|
||||
import { bindDownloadActionHandlers } from '../web/resolver/utils/download_action_handlers.js';
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, '..');
|
||||
const queueMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/actions/queue_methods.js'),
|
||||
'utf8'
|
||||
);
|
||||
const resolveDownloadMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/actions/resolve_download_methods.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
function extractMethod(source, methodName, paramsPattern = '[^)]*') {
|
||||
const signatureRegex = new RegExp(`\\n\\s+${methodName}\\s*\\(${paramsPattern}\\)\\s*\\{`);
|
||||
const match = signatureRegex.exec(source);
|
||||
assert.ok(match, `Could not find ${methodName}`);
|
||||
const parenStart = source.indexOf('(', match.index);
|
||||
const parenEnd = source.indexOf(')', parenStart);
|
||||
const braceStart = source.indexOf('{', parenEnd);
|
||||
let depth = 0;
|
||||
for (let index = braceStart; index < source.length; index += 1) {
|
||||
const char = source[index];
|
||||
if (char === '{') depth += 1;
|
||||
if (char === '}') depth -= 1;
|
||||
if (depth === 0) {
|
||||
const params = source.slice(parenStart + 1, parenEnd);
|
||||
return `function ${methodName}(${params}) ${source.slice(braceStart, index + 1)}`;
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not parse ${methodName}`);
|
||||
}
|
||||
|
||||
const wireDownloadsPanelControls = eval(`(${extractMethod(queueMethodsSource, 'wireDownloadsPanelControls')})`);
|
||||
const attachDownloadActionHandlers = eval(`(${extractMethod(resolveDownloadMethodsSource, 'attachDownloadActionHandlers')})`);
|
||||
|
||||
function createActionMarkup(window, classes) {
|
||||
const root = window.document.createElement('div');
|
||||
for (const className of classes) {
|
||||
const button = window.document.createElement('button');
|
||||
button.className = className;
|
||||
button.dataset.downloadId = 'download-1';
|
||||
root.appendChild(button);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function createDownloadDialog(queueList, calls) {
|
||||
return {
|
||||
queueList,
|
||||
activeDownloads: {
|
||||
'download-1': {
|
||||
lastProgress: { status: 'downloading' },
|
||||
},
|
||||
},
|
||||
getDownloadWorkflowLabel: () => 'Test workflow',
|
||||
getDownloadQueueContext: () => ({ widget_name: 'test-model' }),
|
||||
cancelDownload: id => calls.push(['cancel', id]),
|
||||
pauseDownload: id => calls.push(['pause', id]),
|
||||
resumeDownload: id => calls.push(['resume', id]),
|
||||
openContainingFolder: context => calls.push(['folder', context.widget_name]),
|
||||
switchToDownloadWorkflow: context => calls.push(['workflow', context.widget_name]),
|
||||
setQueueDownloadsTab: () => {},
|
||||
clearDownloadHistory: () => {},
|
||||
removeDownloadHistoryEntry: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchPointerAndClick(window, button) {
|
||||
button.dispatchEvent(new window.Event('pointerdown', { bubbles: true, cancelable: true }));
|
||||
button.dispatchEvent(new window.Event('click', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
test('queue download controls trigger each action once and keep more as click-only', () => {
|
||||
const window = new Window();
|
||||
globalThis.window = window;
|
||||
const calls = [];
|
||||
const queueList = createActionMarkup(window, [
|
||||
'mr-download-queue-cancel',
|
||||
'mr-download-queue-pause',
|
||||
'mr-download-queue-resume',
|
||||
'mr-download-queue-open-folder',
|
||||
'mr-download-queue-switch-workflow',
|
||||
'mr-download-queue-more',
|
||||
]);
|
||||
const contextMenuCalls = [];
|
||||
window.MLOpenContextMenu = (event, item) => contextMenuCalls.push([event.type, item]);
|
||||
const item = window.document.createElement('div');
|
||||
item.className = 'mr-download-queue-item';
|
||||
item.appendChild(queueList);
|
||||
|
||||
const dialog = createDownloadDialog(item, calls);
|
||||
dialog.queueList = item;
|
||||
wireDownloadsPanelControls.call(dialog);
|
||||
|
||||
const buttons = item.querySelectorAll('button');
|
||||
dispatchPointerAndClick(window, buttons[0]);
|
||||
dispatchPointerAndClick(window, buttons[1]);
|
||||
dispatchPointerAndClick(window, buttons[2]);
|
||||
dispatchPointerAndClick(window, buttons[3]);
|
||||
dispatchPointerAndClick(window, buttons[4]);
|
||||
buttons[5].dispatchEvent(new window.Event('click', { bubbles: true, cancelable: true }));
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
['cancel', 'download-1'],
|
||||
['pause', 'download-1'],
|
||||
['resume', 'download-1'],
|
||||
['folder', 'test-model'],
|
||||
['workflow', 'test-model'],
|
||||
]);
|
||||
assert.equal(contextMenuCalls.length, 1);
|
||||
assert.equal(contextMenuCalls[0][0], 'click');
|
||||
assert.equal(contextMenuCalls[0][1], item);
|
||||
});
|
||||
|
||||
test('progress download controls support pending buttons and fallback download ids', () => {
|
||||
const window = new Window();
|
||||
const calls = [];
|
||||
const progressDiv = createActionMarkup(window, [
|
||||
'cancel-download-btn-pending',
|
||||
'pause-download-btn',
|
||||
'resume-download-btn',
|
||||
'mr-download-queue-open-folder',
|
||||
'mr-download-queue-switch-workflow',
|
||||
'mr-download-queue-more',
|
||||
]);
|
||||
const item = window.document.createElement('div');
|
||||
item.className = 'mr-download-queue-item';
|
||||
item.appendChild(progressDiv);
|
||||
|
||||
const dialog = createDownloadDialog(progressDiv, calls);
|
||||
dialog.activeDownloads['fallback-download'] = dialog.activeDownloads['download-1'];
|
||||
dialog.getDownloadFolderContext = () => ({ widget_name: 'folder-fallback' });
|
||||
globalThis.window = window;
|
||||
attachDownloadActionHandlers.call(dialog, item, 'fallback-download');
|
||||
|
||||
const buttons = item.querySelectorAll('button');
|
||||
buttons[0].dataset.downloadId = '';
|
||||
buttons[1].dataset.downloadId = '';
|
||||
buttons[2].dataset.downloadId = '';
|
||||
buttons[3].dataset.downloadId = '';
|
||||
buttons[4].dataset.downloadId = '';
|
||||
buttons[5].dataset.downloadId = '';
|
||||
dispatchPointerAndClick(window, buttons[0]);
|
||||
dispatchPointerAndClick(window, buttons[1]);
|
||||
dispatchPointerAndClick(window, buttons[2]);
|
||||
dispatchPointerAndClick(window, buttons[3]);
|
||||
dispatchPointerAndClick(window, buttons[4]);
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
['cancel', 'fallback-download'],
|
||||
['pause', 'fallback-download'],
|
||||
['resume', 'fallback-download'],
|
||||
['folder', 'test-model'],
|
||||
['workflow', 'test-model'],
|
||||
]);
|
||||
});
|
||||
@@ -2,7 +2,8 @@ import { app } from "../../../../../scripts/app.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { startSplitterDrag } from "../utils/splitter_drag.js";
|
||||
import { bindInstantAction, syncElementAttributes } from "../utils/dom_patch_utils.js";
|
||||
import { syncElementAttributes } from "../utils/dom_patch_utils.js";
|
||||
import { bindDownloadActionHandlers } from "../utils/download_action_handlers.js";
|
||||
const FOOTER_VERSION_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||
const FOOTER_VERSION_FAILURE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
@@ -1135,51 +1136,27 @@ export const queueMethods = {
|
||||
});
|
||||
});
|
||||
|
||||
this.queueList.querySelectorAll('.mr-download-queue-cancel').forEach(button => {
|
||||
bindInstantAction(button, () => {
|
||||
const downloadId = button.dataset.downloadId;
|
||||
if (downloadId) this.cancelDownload(downloadId);
|
||||
});
|
||||
});
|
||||
|
||||
this.queueList.querySelectorAll('.mr-download-queue-pause').forEach(button => {
|
||||
bindInstantAction(button, () => {
|
||||
const downloadId = button.dataset.downloadId;
|
||||
if (downloadId) this.pauseDownload(downloadId);
|
||||
});
|
||||
});
|
||||
|
||||
this.queueList.querySelectorAll('.mr-download-queue-resume').forEach(button => {
|
||||
bindInstantAction(button, () => {
|
||||
const downloadId = button.dataset.downloadId;
|
||||
if (downloadId) this.resumeDownload(downloadId);
|
||||
});
|
||||
});
|
||||
|
||||
this.queueList.querySelectorAll('.mr-download-queue-open-folder').forEach(button => {
|
||||
bindInstantAction(button, () => {
|
||||
const context = getDownloadContext(button);
|
||||
if (context) this.openContainingFolder?.(context);
|
||||
});
|
||||
});
|
||||
|
||||
this.queueList.querySelectorAll('.mr-download-queue-switch-workflow').forEach(button => {
|
||||
bindInstantAction(button, () => {
|
||||
const context = getDownloadContext(button);
|
||||
if (context) this.switchToDownloadWorkflow?.(context);
|
||||
});
|
||||
});
|
||||
|
||||
this.queueList.querySelectorAll('.mr-download-queue-more').forEach(button => {
|
||||
if (button._hasListener) return;
|
||||
button._hasListener = true;
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (button.disabled) return;
|
||||
bindDownloadActionHandlers(this.queueList, {
|
||||
selectors: {
|
||||
cancel: '.mr-download-queue-cancel',
|
||||
pause: '.mr-download-queue-pause',
|
||||
resume: '.mr-download-queue-resume',
|
||||
openFolder: '.mr-download-queue-open-folder',
|
||||
switchWorkflow: '.mr-download-queue-switch-workflow',
|
||||
more: '.mr-download-queue-more',
|
||||
},
|
||||
getDownloadId: button => button?.dataset?.downloadId || '',
|
||||
getContext: getDownloadContext,
|
||||
onCancel: downloadId => this.cancelDownload(downloadId),
|
||||
onPause: downloadId => this.pauseDownload(downloadId),
|
||||
onResume: downloadId => this.resumeDownload(downloadId),
|
||||
onOpenFolder: context => this.openContainingFolder?.(context),
|
||||
onSwitchWorkflow: context => this.switchToDownloadWorkflow?.(context),
|
||||
moreBinding: 'click',
|
||||
onMore: (event, button) => {
|
||||
const item = button.closest('.mr-download-queue-item');
|
||||
if (item) window.MLOpenContextMenu?.(event, item);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const clearHistoryButton = this.queueList.querySelector('.mr-download-history-clear');
|
||||
|
||||
@@ -4,7 +4,8 @@ import { getModelCardUrl, parseHuggingFaceFileUrl } from "../utils/url_utils.js"
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
import { normalizeSha256 } from "../utils/hash_utils.js";
|
||||
import { normalizePathIdentity } from "../utils/html_utils.js";
|
||||
import { bindEventOnce, bindInstantAction, syncElementAttributes } from "../utils/dom_patch_utils.js";
|
||||
import { bindEventOnce, syncElementAttributes } from "../utils/dom_patch_utils.js";
|
||||
import { bindDownloadActionHandlers } from "../utils/download_action_handlers.js";
|
||||
import { normalizeSourceKey } from "../utils/source_labels.js";
|
||||
const log = createModuleLogger('resolve_download_methods');
|
||||
|
||||
@@ -1132,24 +1133,6 @@ export const resolveDownloadMethods = {
|
||||
|
||||
attachDownloadActionHandlers(progressDiv, downloadId) {
|
||||
if (!progressDiv) return;
|
||||
progressDiv.querySelectorAll('.cancel-download-btn, .cancel-download-btn-pending, .mr-download-queue-cancel').forEach(cancelBtn => {
|
||||
bindInstantAction(cancelBtn, () => {
|
||||
const targetDownloadId = cancelBtn.dataset.downloadId || downloadId;
|
||||
if (targetDownloadId) this.cancelDownload(targetDownloadId);
|
||||
});
|
||||
});
|
||||
progressDiv.querySelectorAll('.pause-download-btn, .mr-download-queue-pause').forEach(pauseBtn => {
|
||||
bindInstantAction(pauseBtn, () => {
|
||||
const targetDownloadId = pauseBtn.dataset.downloadId || downloadId;
|
||||
if (targetDownloadId) this.pauseDownload(targetDownloadId);
|
||||
});
|
||||
});
|
||||
progressDiv.querySelectorAll('.resume-download-btn, .mr-download-queue-resume').forEach(resumeBtn => {
|
||||
bindInstantAction(resumeBtn, () => {
|
||||
const targetDownloadId = resumeBtn.dataset.downloadId || downloadId;
|
||||
if (targetDownloadId) this.resumeDownload(targetDownloadId);
|
||||
});
|
||||
});
|
||||
const getDownloadContext = (button) => {
|
||||
const targetDownloadId = button?.dataset?.downloadId || downloadId;
|
||||
const info = this.activeDownloads?.[targetDownloadId];
|
||||
@@ -1162,23 +1145,26 @@ export const resolveDownloadMethods = {
|
||||
|| this.getDownloadFolderContext?.(progress, info)
|
||||
|| null;
|
||||
};
|
||||
progressDiv.querySelectorAll('.mr-download-queue-open-folder').forEach(folderBtn => {
|
||||
bindInstantAction(folderBtn, () => {
|
||||
const context = getDownloadContext(folderBtn);
|
||||
if (context) this.openContainingFolder?.(context);
|
||||
});
|
||||
});
|
||||
progressDiv.querySelectorAll('.mr-download-queue-switch-workflow').forEach(workflowBtn => {
|
||||
bindInstantAction(workflowBtn, () => {
|
||||
const context = getDownloadContext(workflowBtn);
|
||||
if (context) this.switchToDownloadWorkflow?.(context);
|
||||
});
|
||||
});
|
||||
progressDiv.querySelectorAll('.mr-download-queue-more').forEach(moreBtn => {
|
||||
bindInstantAction(moreBtn, (event) => {
|
||||
const item = moreBtn.closest('.mr-download-queue-item');
|
||||
bindDownloadActionHandlers(progressDiv, {
|
||||
fallbackDownloadId: downloadId,
|
||||
selectors: {
|
||||
cancel: '.cancel-download-btn, .cancel-download-btn-pending, .mr-download-queue-cancel',
|
||||
pause: '.pause-download-btn, .mr-download-queue-pause',
|
||||
resume: '.resume-download-btn, .mr-download-queue-resume',
|
||||
openFolder: '.mr-download-queue-open-folder',
|
||||
switchWorkflow: '.mr-download-queue-switch-workflow',
|
||||
more: '.mr-download-queue-more',
|
||||
},
|
||||
getContext: getDownloadContext,
|
||||
onCancel: targetDownloadId => this.cancelDownload(targetDownloadId),
|
||||
onPause: targetDownloadId => this.pauseDownload(targetDownloadId),
|
||||
onResume: targetDownloadId => this.resumeDownload(targetDownloadId),
|
||||
onOpenFolder: context => this.openContainingFolder?.(context),
|
||||
onSwitchWorkflow: context => this.switchToDownloadWorkflow?.(context),
|
||||
onMore: (event, button) => {
|
||||
const item = button.closest('.mr-download-queue-item');
|
||||
if (item) window.MLOpenContextMenu?.(event, item);
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { bindInstantAction } from "./dom_patch_utils.js";
|
||||
|
||||
function bindClickAction(button, handler) {
|
||||
if (!button || button._hasListener) return false;
|
||||
button._hasListener = true;
|
||||
button.addEventListener('click', (event) => {
|
||||
event?.preventDefault?.();
|
||||
event?.stopPropagation?.();
|
||||
if (button.disabled) return;
|
||||
handler(event);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function bindActionButtons(root, selector, handler) {
|
||||
if (!selector) return;
|
||||
root.querySelectorAll(selector).forEach((button) => {
|
||||
bindInstantAction(button, (event) => handler(button, event));
|
||||
});
|
||||
}
|
||||
|
||||
export function bindDownloadActionHandlers(root, {
|
||||
fallbackDownloadId = '',
|
||||
getDownloadId = button => button?.dataset?.downloadId || fallbackDownloadId,
|
||||
getContext = null,
|
||||
selectors = {},
|
||||
onCancel = null,
|
||||
onPause = null,
|
||||
onResume = null,
|
||||
onOpenFolder = null,
|
||||
onSwitchWorkflow = null,
|
||||
onMore = null,
|
||||
moreBinding = 'instant',
|
||||
} = {}) {
|
||||
if (!root?.querySelectorAll) return;
|
||||
|
||||
const resolveDownloadId = button => getDownloadId(button) || '';
|
||||
const resolveContext = button => getContext?.(button, resolveDownloadId(button)) || null;
|
||||
const bindIdAction = (selector, action) => {
|
||||
if (!action) return;
|
||||
bindActionButtons(root, selector, (button) => {
|
||||
const downloadId = resolveDownloadId(button);
|
||||
if (downloadId) action(downloadId);
|
||||
});
|
||||
};
|
||||
|
||||
bindIdAction(selectors.cancel, onCancel);
|
||||
bindIdAction(selectors.pause, onPause);
|
||||
bindIdAction(selectors.resume, onResume);
|
||||
|
||||
bindActionButtons(root, selectors.openFolder, (button) => {
|
||||
const context = resolveContext(button);
|
||||
if (context) onOpenFolder?.(context);
|
||||
});
|
||||
bindActionButtons(root, selectors.switchWorkflow, (button) => {
|
||||
const context = resolveContext(button);
|
||||
if (context) onSwitchWorkflow?.(context);
|
||||
});
|
||||
|
||||
if (selectors.more && onMore) {
|
||||
const bindMore = moreBinding === 'click' ? bindClickAction : bindInstantAction;
|
||||
root.querySelectorAll(selectors.more).forEach((button) => {
|
||||
bindMore(button, event => onMore(event, button, resolveDownloadId(button)));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user