fix: block empty import confirmations

Disable and guard host selection confirmation whenever the current preflight produces zero jobs, preserving pending files and plan state for empty destination and fully size-limited selections.
This commit is contained in:
Sucukdeluxe
2026-08-16 22:16:57 +02:00
parent 3da1b08514
commit 287363aee7
2 changed files with 12 additions and 2 deletions
+6 -2
View File
@@ -1285,7 +1285,7 @@ function closeHosterModal() {
} }
async function applyHosterSelection() { async function applyHosterSelection() {
if (_pendingImportInspections > 0) return false; if (isImportConfirmationBlocked()) return false;
selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked')) selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'))
.map(input => input.dataset.hosterModal); .map(input => input.dataset.hosterModal);
// Move pending files to selectedFiles on confirm // Move pending files to selectedFiles on confirm
@@ -1613,9 +1613,13 @@ function getImportPlanSummary() {
}); });
} }
function isImportConfirmationBlocked() {
return _pendingImportInspections > 0 || getImportPlanSummary()?.jobCount === 0;
}
function syncImportConfirmationState() { function syncImportConfirmationState() {
const confirmButton = document.getElementById('confirmHosterModalBtn'); const confirmButton = document.getElementById('confirmHosterModalBtn');
if (confirmButton) confirmButton.disabled = _pendingImportInspections > 0; if (confirmButton) confirmButton.disabled = isImportConfirmationBlocked();
} }
function renderImportPlanSummary() { function renderImportPlanSummary() {
+6
View File
@@ -1581,6 +1581,12 @@ setTimeout(async () => {
check('Configured size-limit pairs never create preview jobs and fully ineligible files leave no selected-file residue', importPreflightAdmission.selected.length === 2 && importPreflightAdmission.selected.includes('Existing.720p.bin') && importPreflightAdmission.selected.includes('Episode.720p.mkv') && !importPreflightAdmission.selected.includes('Large.720p.custom') && importPreflightAdmission.jobs.length === 2 && importPreflightAdmission.jobs.every(job => job.file !== 'Large.720p.custom' && job.hoster === filenameFilterImport.available[0] && job.status === 'preview')); check('Configured size-limit pairs never create preview jobs and fully ineligible files leave no selected-file residue', importPreflightAdmission.selected.length === 2 && importPreflightAdmission.selected.includes('Existing.720p.bin') && importPreflightAdmission.selected.includes('Episode.720p.mkv') && !importPreflightAdmission.selected.includes('Large.720p.custom') && importPreflightAdmission.jobs.length === 2 && importPreflightAdmission.jobs.every(job => job.file !== 'Large.720p.custom' && job.hoster === filenameFilterImport.available[0] && job.status === 'preview'));
const explicitEmptyHosterSelection = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); selectedFiles = []; _pendingFiles = []; _pendingImportInspection = null; queueJobs = []; rebuildJobIndex(); selectedUploadHosters = getAvailableHosters().slice(0, 1).map(item => item.name); const inspect = entries => ({ candidateCount: entries.length, duplicateCount: 0, filteredCount: 0, unavailableCount: 0, acceptedCount: entries.length, accepted: entries.map(entry => ({ path: entry.path, name: entry.name, size: entry.size })), duplicates: [], filtered: [], unavailable: [] }); await coordinateImportEntries([{ path: fixtures.floatingAccepted, name: "Floating.720p.mkv", size: 7 }], async entries => inspect(entries)); const selected = document.querySelector("input[data-hoster-modal]:checked"); selected?.click(); let releaseSecond; const second = coordinateImportEntries([{ path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7 }], async entries => new Promise(resolve => { releaseSecond = () => resolve(inspect(entries)); })); const disabledWhilePending = document.getElementById("confirmHosterModalBtn").disabled; for (let index = 0; index < 20 && !releaseSecond; index++) await Promise.resolve(); releaseSecond?.(); await second; return { disabledWhilePending, checked: [...document.querySelectorAll("input[data-hoster-modal]:checked")].map(input => input.dataset.hosterModal), targets: Number(document.getElementById("importPlanTargets").textContent) }; })()'); const explicitEmptyHosterSelection = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); selectedFiles = []; _pendingFiles = []; _pendingImportInspection = null; queueJobs = []; rebuildJobIndex(); selectedUploadHosters = getAvailableHosters().slice(0, 1).map(item => item.name); const inspect = entries => ({ candidateCount: entries.length, duplicateCount: 0, filteredCount: 0, unavailableCount: 0, acceptedCount: entries.length, accepted: entries.map(entry => ({ path: entry.path, name: entry.name, size: entry.size })), duplicates: [], filtered: [], unavailable: [] }); await coordinateImportEntries([{ path: fixtures.floatingAccepted, name: "Floating.720p.mkv", size: 7 }], async entries => inspect(entries)); const selected = document.querySelector("input[data-hoster-modal]:checked"); selected?.click(); let releaseSecond; const second = coordinateImportEntries([{ path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7 }], async entries => new Promise(resolve => { releaseSecond = () => resolve(inspect(entries)); })); const disabledWhilePending = document.getElementById("confirmHosterModalBtn").disabled; for (let index = 0; index < 20 && !releaseSecond; index++) await Promise.resolve(); releaseSecond?.(); await second; return { disabledWhilePending, checked: [...document.querySelectorAll("input[data-hoster-modal]:checked")].map(input => input.dataset.hosterModal), targets: Number(document.getElementById("importPlanTargets").textContent) }; })()');
check('A later inspection preserves an explicitly empty hoster selection and confirmation stays disabled while it is pending', explicitEmptyHosterSelection.disabledWhilePending === true && explicitEmptyHosterSelection.checked.length === 0 && explicitEmptyHosterSelection.targets === 0); check('A later inspection preserves an explicitly empty hoster selection and confirmation stays disabled while it is pending', explicitEmptyHosterSelection.disabledWhilePending === true && explicitEmptyHosterSelection.checked.length === 0 && explicitEmptyHosterSelection.targets === 0);
const zeroDestinationImportGuard = await wc.executeJavaScript('(async () => { const snapshot = () => ({ pending: _pendingFiles.map(file => ({ path: file.path, name: file.name, size: file.size })), inspection: _pendingImportInspection ? JSON.parse(JSON.stringify(_pendingImportInspection)) : null, summary: getImportPlanSummary(), checked: [...document.querySelectorAll("input[data-hoster-modal]")].map(input => ({ hoster: input.dataset.hosterModal, checked: input.checked })), modal: document.getElementById("hosterModal")?.style.display, selected: selectedFiles.map(file => file.path), jobs: queueJobs.map(job => job.id) }); const disabled = document.getElementById("confirmHosterModalBtn").disabled; const before = snapshot(); const result = await applyHosterSelection(); const after = snapshot(); return { disabled, result, before, after }; })()');
if (!(zeroDestinationImportGuard.disabled === true && zeroDestinationImportGuard.result === false && JSON.stringify(zeroDestinationImportGuard.after) === JSON.stringify(zeroDestinationImportGuard.before))) console.log('Zero destination import guard: ' + JSON.stringify(zeroDestinationImportGuard));
check('Zero-destination import confirmation is disabled and direct invocation preserves the pending modal state', zeroDestinationImportGuard.disabled === true && zeroDestinationImportGuard.result === false && zeroDestinationImportGuard.before.summary?.jobCount === 0 && zeroDestinationImportGuard.before.summary?.targetCount === 0 && JSON.stringify(zeroDestinationImportGuard.after) === JSON.stringify(zeroDestinationImportGuard.before));
const allSizeLimitedImportGuard = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); selectedFiles = []; _pendingFiles = []; _pendingImportInspection = null; queueJobs = []; rebuildJobIndex(); const available = getAvailableHosters().slice(0, 1).map(item => item.name); selectedUploadHosters = available; hosterSettings = { ...hosterSettings, [available[0]]: { ...(hosterSettings[available[0]] || {}), maxSizeMb: 1 } }; await addPathsToQueue([fixtures.large]); const snapshot = () => ({ pending: _pendingFiles.map(file => ({ path: file.path, name: file.name, size: file.size })), inspection: _pendingImportInspection ? JSON.parse(JSON.stringify(_pendingImportInspection)) : null, summary: getImportPlanSummary(), checked: [...document.querySelectorAll("input[data-hoster-modal]")].map(input => ({ hoster: input.dataset.hosterModal, checked: input.checked })), modal: document.getElementById("hosterModal")?.style.display, selected: selectedFiles.map(file => file.path), jobs: queueJobs.map(job => job.id) }); const disabled = document.getElementById("confirmHosterModalBtn").disabled; const before = snapshot(); const result = await applyHosterSelection(); const after = snapshot(); return { disabled, result, before, after }; })()');
if (!(allSizeLimitedImportGuard.disabled === true && allSizeLimitedImportGuard.result === false && JSON.stringify(allSizeLimitedImportGuard.after) === JSON.stringify(allSizeLimitedImportGuard.before))) console.log('All size-limited import guard: ' + JSON.stringify(allSizeLimitedImportGuard));
check('Fully size-limited import confirmation is disabled and direct invocation preserves the pending modal state', allSizeLimitedImportGuard.disabled === true && allSizeLimitedImportGuard.result === false && allSizeLimitedImportGuard.before.summary?.jobCount === 0 && allSizeLimitedImportGuard.before.summary?.targetCount === 1 && allSizeLimitedImportGuard.before.summary?.sizeLimitedJobCount === 1 && JSON.stringify(allSizeLimitedImportGuard.after) === JSON.stringify(allSizeLimitedImportGuard.before));
const cancelledImportGeneration = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); selectedFiles = []; _pendingFiles = []; _pendingImportInspection = null; queueJobs = []; rebuildJobIndex(); selectedUploadHosters = getAvailableHosters().slice(0, 1).map(item => item.name); const result = entry => ({ candidateCount: 1, duplicateCount: 0, filteredCount: 0, unavailableCount: 0, acceptedCount: 1, accepted: [{ path: entry.path, name: entry.name, size: entry.size }], duplicates: [], filtered: [], unavailable: [] }); let runningCalls = 0; let queuedCalls = 0; let releaseRunning; const runningEntry = { path: fixtures.floatingAccepted, name: "Floating.720p.mkv", size: 7 }; const queuedEntry = { path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7 }; const running = coordinateImportEntries([runningEntry], async () => { runningCalls++; return new Promise(resolve => { releaseRunning = () => resolve(result(runningEntry)); }); }); for (let index = 0; index < 20 && !releaseRunning; index++) await Promise.resolve(); const queued = coordinateImportEntries([queuedEntry], async () => { queuedCalls++; return result(queuedEntry); }); const disabledWhilePending = document.getElementById("confirmHosterModalBtn").disabled; cancelHosterModal(); releaseRunning?.(); await Promise.all([running, queued]); await new Promise(resolve => setTimeout(resolve, 25)); return { runningCalls, queuedCalls, disabledWhilePending, modal: document.getElementById("hosterModal").style.display, pending: _pendingFiles.length, inspection: _pendingImportInspection }; })()'); const cancelledImportGeneration = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); selectedFiles = []; _pendingFiles = []; _pendingImportInspection = null; queueJobs = []; rebuildJobIndex(); selectedUploadHosters = getAvailableHosters().slice(0, 1).map(item => item.name); const result = entry => ({ candidateCount: 1, duplicateCount: 0, filteredCount: 0, unavailableCount: 0, acceptedCount: 1, accepted: [{ path: entry.path, name: entry.name, size: entry.size }], duplicates: [], filtered: [], unavailable: [] }); let runningCalls = 0; let queuedCalls = 0; let releaseRunning; const runningEntry = { path: fixtures.floatingAccepted, name: "Floating.720p.mkv", size: 7 }; const queuedEntry = { path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7 }; const running = coordinateImportEntries([runningEntry], async () => { runningCalls++; return new Promise(resolve => { releaseRunning = () => resolve(result(runningEntry)); }); }); for (let index = 0; index < 20 && !releaseRunning; index++) await Promise.resolve(); const queued = coordinateImportEntries([queuedEntry], async () => { queuedCalls++; return result(queuedEntry); }); const disabledWhilePending = document.getElementById("confirmHosterModalBtn").disabled; cancelHosterModal(); releaseRunning?.(); await Promise.all([running, queued]); await new Promise(resolve => setTimeout(resolve, 25)); return { runningCalls, queuedCalls, disabledWhilePending, modal: document.getElementById("hosterModal").style.display, pending: _pendingFiles.length, inspection: _pendingImportInspection }; })()');
check('Cancelling invalidates running and queued results from the old import generation', cancelledImportGeneration.runningCalls === 1 && cancelledImportGeneration.queuedCalls === 0 && cancelledImportGeneration.disabledWhilePending === true && cancelledImportGeneration.modal !== 'flex' && cancelledImportGeneration.pending === 0 && cancelledImportGeneration.inspection === null); check('Cancelling invalidates running and queued results from the old import generation', cancelledImportGeneration.runningCalls === 1 && cancelledImportGeneration.queuedCalls === 0 && cancelledImportGeneration.disabledWhilePending === true && cancelledImportGeneration.modal !== 'flex' && cancelledImportGeneration.pending === 0 && cancelledImportGeneration.inspection === null);
const filenameFilterDropPaths = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); await addDropTargetEntries([{ path: fixtures.floatingAccepted }, { path: fixtures.floatingFiltered }]); const floating = _pendingFiles.map(file => file.name); cancelHosterModal(); await addDroppedFiles([{ path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7, type: "video/x-matroska" }, { path: fixtures.desktopFiltered, name: "Desktop.1080p.mkv", size: 7, type: "video/x-matroska" }]); const desktop = _pendingFiles.map(file => file.name); cancelHosterModal(); return { floating, desktop }; })()'); const filenameFilterDropPaths = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); await addDropTargetEntries([{ path: fixtures.floatingAccepted }, { path: fixtures.floatingFiltered }]); const floating = _pendingFiles.map(file => file.name); cancelHosterModal(); await addDroppedFiles([{ path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7, type: "video/x-matroska" }, { path: fixtures.desktopFiltered, name: "Desktop.1080p.mkv", size: 7, type: "video/x-matroska" }]); const desktop = _pendingFiles.map(file => file.name); cancelHosterModal(); return { floating, desktop }; })()');