Add Apps/Store tab, template WebUI/URL import, container RO volumes/device passthrough/run-as-user, and in-app confirm dialogs
Lint / ShellCheck (push) Successful in 14s
Lint / Validate .plg XML (push) Successful in 11s
Lint / EditorConfig (push) Successful in 6s

Replaces every native confirm() with a shared P.confirm() modal (a hung
native dialog was found live to block the whole tab, including
auto-refresh, and once even double-confirmed an unrelated deletion).
Also fixes Edit Container silently resetting to Bridge/blanking the
Static IP for any container on a custom network, and a context menu
losing its anchor to a mid-read auto-refresh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 19:24:32 +00:00
co-authored by Claude Sonnet 5
parent 9d46547ef4
commit 7e2ed451e3
14 changed files with 911 additions and 134 deletions
+66 -6
View File
@@ -129,9 +129,9 @@ window.Podman = (function () {
// --- Toast notifications ----------------------------------------------------
//
// Replaces alert() for one-way feedback ("Saved.", "Removed 3 image(s)",
// "Save failed: ..."). Confirmations stay native confirm() — a toast is
// for telling the user something happened, not for asking them a
// yes/no question. Command output that can run to hundreds of lines
// "Save failed: ..."). Confirmations use confirmModal() below instead —
// a toast is for telling the user something happened, not for asking
// them a yes/no question. Command output that can run to hundreds of lines
// (e.g. `podman compose up`) stays in the existing openLogModal()
// pattern instead — a toast has to stay short and auto-dismiss, which
// doesn't fit a scrolling log.
@@ -180,6 +180,60 @@ window.Podman = (function () {
setTimeout(dismiss, TOAST_DURATION_MS[kind]);
}
// --- Confirm dialog ----------------------------------------------------
/**
* In-app replacement for browser-native confirm() — a native confirm()
* blocks the entire tab (including this plugin's own ~2s auto-refresh,
* and any browser automation driving the page) until dismissed, can't
* be styled/themed, and — found live — is easy to lose track of: a
* hung dialog blocked screenshots/JS entirely, and a follow-up
* keypress meant to dismiss just one of them ended up confirming a
* second, unrelated one too. Returns a Promise<boolean> (true =
* confirmed) instead of blocking synchronously.
*
* @param {string} message
* @param {object} [opts]
* @param {string} [opts.title='Confirm']
* @param {string} [opts.confirmLabel='Confirm']
* @param {string} [opts.cancelLabel='Cancel']
* @param {boolean} [opts.danger=false] solid red confirm button, for
* destructive/data-losing actions (delete, remove, format, ...).
*/
function confirmModal(message, opts) {
opts = opts || {};
return new Promise(function (resolve) {
const backdrop = document.createElement('div');
backdrop.className = 'podman-modal-backdrop';
backdrop.innerHTML = '' +
'<div class="podman-modal" role="alertdialog" aria-modal="true">' +
'<div class="podman-modal-head"><h3>' + escapeHtml(opts.title || 'Confirm') + '</h3></div>' +
'<div class="podman-modal-body"><p class="podman-confirm-message"></p></div>' +
'<div class="podman-modal-actions">' +
'<button type="button" class="podman-btn podman-btn-ghost" data-role="cancel">' + escapeHtml(opts.cancelLabel || 'Cancel') + '</button>' +
'<button type="button" class="podman-btn ' + (opts.danger ? 'podman-btn-ghost podman-btn-danger' : 'podman-btn-primary') + '" data-role="confirm">' +
escapeHtml(opts.confirmLabel || 'Confirm') + '</button>' +
'</div></div>';
backdrop.querySelector('.podman-confirm-message').textContent = message;
(document.querySelector('.podman-plugin') || document.body).appendChild(backdrop);
backdrop.querySelector('[data-role="confirm"]').focus();
function settle(result) {
backdrop.remove();
document.removeEventListener('keydown', onKey);
resolve(result);
}
function onKey(e) {
if (e.key === 'Escape') settle(false);
}
backdrop.querySelector('[data-role="cancel"]').addEventListener('click', function () { settle(false); });
backdrop.querySelector('[data-role="confirm"]').addEventListener('click', function () { settle(true); });
backdrop.addEventListener('click', function (e) { if (e.target === backdrop) settle(false); });
document.addEventListener('keydown', onKey);
});
}
// --- Modal form dialog -----------------------------------------------------
/**
@@ -480,14 +534,19 @@ window.Podman = (function () {
// Only panels that opt in via `autoRefresh: true` (Dashboard, Containers)
// get polled — most panels (Settings, Compose, Terminal, ...) have
// in-progress forms or connections an unexpected refresh would disrupt.
// Paused while the tab is hidden (nothing to look at) and while any
// modal is open (a full-panel re-render mid-edit would be jarring),
// rather than fighting those cases with more state.
// Paused while the tab is hidden (nothing to look at), while any modal
// is open (a full-panel re-render mid-edit would be jarring), and while
// a context menu is open — found live: a re-render replaces every row's
// DOM node wholesale, so a menu opened from a row (its anchor button)
// still LOOKS open but is now anchored to a detached element; opening a
// submenu from it (e.g. "Move to Folder") then positions itself
// relative to that stale anchor instead of anywhere sensible.
const AUTO_REFRESH_INTERVAL_MS = 2000;
function autoRefreshTick() {
if (document.hidden) return;
if (document.querySelector('.podman-modal-backdrop')) return;
if (document.querySelector('.podman-context-menu')) return;
const activeBtn = document.querySelector('.podman-subnav button.active');
if (!activeBtn) return;
@@ -512,6 +571,7 @@ window.Podman = (function () {
loadingRow: loadingRow,
errorRow: errorRow,
toast: toast,
confirm: confirmModal,
openFormModal: openFormModal,
openLogModal: openLogModal,
openContextMenu: openContextMenu,