Ask with Unraid's own dialog, not the browser's
Every confirm() and prompt() in the plugin could be switched off from inside itself — one tick of "prevent this page from creating additional dialogs" and all 32 of them returned false while drawing nothing, across every tab, until a full reload. swal is already global on every webGUI page and core uses it 370 times without a single confirm(), so this costs no new dependency. vvConfirmRun() is the one that mattered: it returned a boolean to three callers testing !it, and an unawaited promise is always truthy, so leaving those alone would have run every job without asking. The wrapper's callback is a classic function expression on purpose — SweetAlert only calls back on cancel when the callback's own source declares a parameter, and an arrow would have hung the promise forever.
This commit is contained in:
@@ -104,6 +104,82 @@ function vvSafeUrl(u) {
|
||||
if (/^\/(?!\/)/.test(s)) return s;
|
||||
return '';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// DIALOGS — never the browser's own
|
||||
//
|
||||
// confirm(), alert() and prompt() all carry a checkbox inside the dialog reading "prevent this
|
||||
// page from creating additional dialogs". The moment it is ticked, every later call from that
|
||||
// document returns false and draws nothing. Guards written as `if (!confirm(x)) return;` then
|
||||
// decline silently and the button reads as dead — and because the webGUI swaps tabs by AJAX
|
||||
// without tearing down the document, the suppression follows the operator across tabs and
|
||||
// survives until a full reload. That happened on 2026-08-09: dismissing several repair findings
|
||||
// in a row produced the checkbox, and the AI tab's buttons went dead until history was cleared.
|
||||
//
|
||||
// swal is Unraid's own dialog, defined in webGui/javascript/dynamix.js and loaded on every page
|
||||
// by DefaultPageLayout — core calls it around 370 times and uses confirm() exactly never. So
|
||||
// this is not a new dependency, and these dialogs look like the rest of the machine.
|
||||
//
|
||||
// THE TRAP, and why the callback below is not an arrow function:
|
||||
// On cancel, SweetAlert 1.x only invokes the callback if the callback's own source declares a
|
||||
// parameter — it literally does String(fn).replace(/\s/g,'') and checks that it starts with
|
||||
// "function(" and that the next character is not ")". An arrow function stringifies as "ok=>…"
|
||||
// and fails that test, so cancel would never resolve this promise and the awaiting caller
|
||||
// would hang forever with no dialog on screen. Which is the exact bug this file is replacing,
|
||||
// wearing a different hat. Classic function expression, one named parameter, deliberately.
|
||||
function vvConfirm(text, opts) {
|
||||
const o = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
// Without swal there is nothing better than the browser's own dialog. It carries the bug
|
||||
// described above, but the alternative is a guard that answers neither yes nor no.
|
||||
if (typeof swal !== 'function') { resolve(window.confirm(text)); return; }
|
||||
swal({
|
||||
title: o.title || 'Are you sure?',
|
||||
text: String(text ?? ''),
|
||||
type: o.type || 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: o.confirmText || 'Yes',
|
||||
cancelButtonText: o.cancelText || 'Cancel',
|
||||
confirmButtonColor: o.danger === false ? '#3085d6' : '#d9534f',
|
||||
closeOnConfirm: true,
|
||||
}, function (ok) { resolve(ok !== false); });
|
||||
});
|
||||
}
|
||||
|
||||
// Telling the operator something, with no question attached. Returns a promise so a caller can
|
||||
// sequence on it, but nothing has to await it.
|
||||
function vvAlert(text, opts) {
|
||||
const o = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
if (typeof swal !== 'function') { window.alert(text); resolve(true); return; }
|
||||
swal({
|
||||
title: o.title || '',
|
||||
text: String(text ?? ''),
|
||||
type: o.type || 'info',
|
||||
confirmButtonText: o.confirmText || 'OK',
|
||||
}, function (ok) { resolve(true); });
|
||||
});
|
||||
}
|
||||
|
||||
// Asking for a value. Resolves to the string, or null when cancelled — prompt()'s own contract,
|
||||
// so call sites keep reading the same way. The empty string is a real answer and is not null.
|
||||
function vvPrompt(text, def, opts) {
|
||||
const o = opts || {};
|
||||
return new Promise(function (resolve) {
|
||||
if (typeof swal !== 'function') { resolve(window.prompt(text, def || '')); return; }
|
||||
swal({
|
||||
title: o.title || '',
|
||||
text: String(text ?? ''),
|
||||
type: 'input',
|
||||
inputValue: def || '',
|
||||
inputPlaceholder: o.placeholder || '',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: o.confirmText || 'OK',
|
||||
cancelButtonText: o.cancelText || 'Cancel',
|
||||
closeOnConfirm: true,
|
||||
}, function (val) { resolve(val === false ? null : String(val)); });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
|
||||
Reference in New Issue
Block a user