dashboard: theme all remaining dialogs (replace native confirm/alert/prompt)
Follow-up to the themed-modal component: route every remaining native browser dialog through modal.js so nothing falls back to the OS chrome. - modal.js: add themedPrompt (input dialog) + themedToast (non-blocking transient notification, info/error/ok) alongside openDialog/themedConfirm. - Migrate call sites: bindAsyncForms confirm/prompt/alerts (common.js), the answer-validation alert (call.js), the M0V3 reparent confirm + action toasts (tabs.js), and the schedule form/cancel/fire confirms + validation and error alerts (schedules.js). - UX: blocking modal for confirms/prompts; non-blocking toast for transient errors + validation. Destructive confirms keep the danger styling. - CSS for the toast stack + prompt input. common.js <-> modal.js is a safe deferred import cycle (usage is call-time only); esbuild bundles it clean.
This commit is contained in:
parent
ff3317eb20
commit
7c2de690ec
6 changed files with 141 additions and 32 deletions
|
|
@ -12,6 +12,7 @@
|
|||
// target-chip pickers. Render + format helpers come from util.js.
|
||||
|
||||
import { $, el } from './common.js';
|
||||
import { themedConfirm, themedToast } from './modal.js';
|
||||
import { paintAtomic, fmtAgo, fmtDuration } from './util.js';
|
||||
import { containersState } from './state.js';
|
||||
|
||||
|
|
@ -530,14 +531,14 @@ async function submitNewScheduleInline(tr, submitBtn) {
|
|||
const firstFireStr = String(tr.querySelector('input[name="new_first_fire"]')?.value || '');
|
||||
|
||||
if (!targets.length) {
|
||||
alert('schedule must have at least one target — tick at least one agent column');
|
||||
themedToast('schedule must have at least one target — tick at least one agent column', { type: 'error' });
|
||||
return;
|
||||
}
|
||||
if (!body) { alert('prompt body must be non-empty'); return; }
|
||||
if (!firstFireStr) { alert('first fire timestamp is required'); return; }
|
||||
if (!body) { themedToast('prompt body must be non-empty', { type: 'error' }); return; }
|
||||
if (!firstFireStr) { themedToast('first fire timestamp is required', { type: 'error' }); return; }
|
||||
const firstFireDate = new Date(firstFireStr);
|
||||
if (Number.isNaN(firstFireDate.getTime())) {
|
||||
alert('first fire is not a valid datetime'); return;
|
||||
themedToast('first fire is not a valid datetime', { type: 'error' }); return;
|
||||
}
|
||||
const first_fire_at_unix = Math.floor(firstFireDate.getTime() / 1000);
|
||||
|
||||
|
|
@ -554,7 +555,7 @@ async function submitNewScheduleInline(tr, submitBtn) {
|
|||
};
|
||||
const intervalTotal = part('d', 86400) + part('h', 3600) + part('m', 60) + part('s', 1);
|
||||
if (Number.isNaN(intervalTotal)) {
|
||||
alert('interval fields must be non-negative integers (or blank for one-shot)');
|
||||
themedToast('interval fields must be non-negative integers (or blank for one-shot)', { type: 'error' });
|
||||
return;
|
||||
}
|
||||
const interval_seconds = intervalTotal > 0 ? intervalTotal : null;
|
||||
|
|
@ -574,14 +575,14 @@ async function submitNewScheduleInline(tr, submitBtn) {
|
|||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text().catch(() => '');
|
||||
alert('schedule submit failed: http ' + resp.status + (text ? '\n\n' + text : ''));
|
||||
themedToast('schedule submit failed: http ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' });
|
||||
return;
|
||||
}
|
||||
// Reset carry so the next render shows an empty row.
|
||||
resetNewScheduleCarry();
|
||||
await refreshSchedules();
|
||||
} catch (err) {
|
||||
alert('schedule submit failed: ' + err);
|
||||
themedToast('schedule submit failed: ' + err, { type: 'error' });
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalLabel;
|
||||
|
|
@ -899,11 +900,11 @@ async function submitEditSchedule(originalSchedule, form_) {
|
|||
const newDescription = String(fd.get('description') || '').trim();
|
||||
const newNextFireStr = String(fd.get('next_fire') || '');
|
||||
|
||||
if (!newBody) { alert('body must be non-empty'); return; }
|
||||
if (!newNextFireStr) { alert('next-fire timestamp is required'); return; }
|
||||
if (!newBody) { themedToast('body must be non-empty', { type: 'error' }); return; }
|
||||
if (!newNextFireStr) { themedToast('next-fire timestamp is required', { type: 'error' }); return; }
|
||||
const newNextFireDate = new Date(newNextFireStr);
|
||||
if (Number.isNaN(newNextFireDate.getTime())) {
|
||||
alert('next-fire is not a valid datetime'); return;
|
||||
themedToast('next-fire is not a valid datetime', { type: 'error' }); return;
|
||||
}
|
||||
const newNextFireUnix = Math.floor(newNextFireDate.getTime() / 1000);
|
||||
|
||||
|
|
@ -911,7 +912,7 @@ async function submitEditSchedule(originalSchedule, form_) {
|
|||
// `edit_interval_*` names in this form).
|
||||
const intervalTotal = intervalSecondsFromFormData(fd, 'edit_interval_');
|
||||
if (Number.isNaN(intervalTotal)) {
|
||||
alert('interval fields must be non-negative integers');
|
||||
themedToast('interval fields must be non-negative integers', { type: 'error' });
|
||||
return;
|
||||
}
|
||||
const newIntervalSeconds = intervalTotal > 0 ? intervalTotal : null;
|
||||
|
|
@ -929,7 +930,7 @@ async function submitEditSchedule(originalSchedule, form_) {
|
|||
.map((t) => t.target),
|
||||
);
|
||||
if (!newTargets.size) {
|
||||
alert('schedule must have at least one target — uncheck submit, or use ✕ cancel all instead');
|
||||
themedToast('schedule must have at least one target — uncheck submit, or use ✕ cancel all instead', { type: 'error' });
|
||||
return;
|
||||
}
|
||||
const targetsAdd = [...newTargets].filter((t) => !originalActive.has(t));
|
||||
|
|
@ -971,14 +972,14 @@ async function submitEditSchedule(originalSchedule, form_) {
|
|||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text().catch(() => '');
|
||||
alert('edit failed: http ' + resp.status + (text ? '\n\n' + text : ''));
|
||||
themedToast('edit failed: http ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' });
|
||||
return;
|
||||
}
|
||||
editingSchedules.delete(s.id);
|
||||
scheduleEditCarry.delete(s.id);
|
||||
await refreshSchedules();
|
||||
} catch (err) {
|
||||
alert('edit failed: ' + err);
|
||||
themedToast('edit failed: ' + err, { type: 'error' });
|
||||
} finally {
|
||||
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalLabel; }
|
||||
}
|
||||
|
|
@ -992,7 +993,7 @@ async function fireScheduleNow(id, isOneShot, targets, btn) {
|
|||
: `fire schedule #${id} now to ${targetList}?\n\n`
|
||||
+ 'this is RECURRING — sends an extra pulse out-of-band. '
|
||||
+ 'the regular cadence keeps firing on schedule.';
|
||||
if (!confirm(prompt)) return;
|
||||
if (!(await themedConfirm({ message: prompt, danger: true }))) return;
|
||||
// Capture child nodes so we can restore on error, then replace
|
||||
// with DOM-built content (textContent + element children rather
|
||||
// than innerHTML — the format string only carries server-side
|
||||
|
|
@ -1017,7 +1018,7 @@ async function fireScheduleNow(id, isOneShot, targets, btn) {
|
|||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text().catch(() => '');
|
||||
alert('fire-now failed: http ' + resp.status + (text ? '\n\n' + text : ''));
|
||||
themedToast('fire-now failed: http ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' });
|
||||
restoreBtn();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1041,16 +1042,16 @@ async function fireScheduleNow(id, isOneShot, targets, btn) {
|
|||
// refresh wipes the row in place.
|
||||
setTimeout(refreshSchedules, 1500);
|
||||
} catch (err) {
|
||||
alert('fire-now failed: ' + err);
|
||||
themedToast('fire-now failed: ' + err, { type: 'error' });
|
||||
restoreBtn();
|
||||
}
|
||||
}
|
||||
async function cancelScheduleAll(id) {
|
||||
if (!confirm(`cancel schedule #${id}? this stops all future fires for every target.`)) return;
|
||||
if (!(await themedConfirm({ message: `cancel schedule #${id}? this stops all future fires for every target.`, danger: true }))) return;
|
||||
await postScheduleCancel(id, null);
|
||||
}
|
||||
async function cancelScheduleTargets(id, targets) {
|
||||
if (!confirm(`cancel schedule #${id} for ${targets.join(', ')}? other targets keep firing.`)) return;
|
||||
if (!(await themedConfirm({ message: `cancel schedule #${id} for ${targets.join(', ')}? other targets keep firing.`, danger: true }))) return;
|
||||
await postScheduleCancel(id, targets);
|
||||
}
|
||||
async function postScheduleCancel(id, targets) {
|
||||
|
|
@ -1063,12 +1064,12 @@ async function postScheduleCancel(id, targets) {
|
|||
const resp = await fetch('/api/schedules/' + encodeURIComponent(id) + '/cancel', opts);
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text().catch(() => '');
|
||||
alert('cancel failed: http ' + resp.status + (text ? '\n\n' + text : ''));
|
||||
themedToast('cancel failed: http ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' });
|
||||
return;
|
||||
}
|
||||
await refreshSchedules();
|
||||
} catch (err) {
|
||||
alert('cancel failed: ' + err);
|
||||
themedToast('cancel failed: ' + err, { type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue