fix(images): show every Show in folder outcome above the viewer and report opener failures (#537)

Closes #533
This commit is contained in:
Vito Sansevero
2026-10-30 16:52:38 -07:00
parent 0f2f88d48c
commit db1c7ef20d
11 changed files with 520 additions and 238 deletions
+4 -4
View File
@@ -26,9 +26,9 @@ const CATEGORY_PREFIX = {image: 'Image category: ', positive: 'Positive prompt c
const CATEGORY_ORIGIN = {image: 'this image', positive: 'prompt', pair: 'recipe'};
const REVEAL_URL = '/prompt_manager/images/reveal';
// D38 Show in folder: the route's answers, in the maintainer's words.
const REVEAL_OPENED = 'Opened the folder on this computer.';
const REVEAL_NOT_LOCAL = 'Show in folder works only in a browser on the computer running ComfyUI.';
const REVEAL_UNAVAILABLE = 'No file manager is available on this computer.';
const REVEAL_OPENED = 'Opened the folder on the computer running ComfyUI.';
const REVEAL_REFUSED = "The folder was not opened: this page's address does not match the server's.";
const REVEAL_UNAVAILABLE = 'No file manager is available on the computer running ComfyUI.';
const REVEAL_BUSY = 'The folder is already opening.';
const REVEAL_FAILED = 'The folder could not be opened. Try again.';
const FACTS = [['seed', 'Seed'], ['steps', 'Steps'], ['cfg', 'CFG'], ['sampler', 'Sampler'], ['scheduler', 'Scheduler'], ['model', 'Model']];
@@ -109,7 +109,7 @@ export function revealTarget(identity) {
/** The toast for a reveal outcome: null is success, otherwise the request's error (D38). */
export function revealOutcome(error) {
if (error === null) return REVEAL_OPENED;
if (error?.status === 403 && error.code === 'not-local') return REVEAL_NOT_LOCAL;
if (error?.status === 403 && error.code === 'not-same-origin') return REVEAL_REFUSED;
if (error?.status === 501) return REVEAL_UNAVAILABLE;
if (error?.status === 429 && error.code === 'busy') return REVEAL_BUSY;
return REVEAL_FAILED;
+32 -1
View File
@@ -5,6 +5,34 @@ export const LINK_TOAST_MS = 20000;
let toastTimer;
// Stops waiting for the pointer or focus to leave a toast that a newer one replaced.
let releaseWait = null;
// The toast and the page element it lives in when no modal is open.
let toastElement = null;
let toastHome = null;
/**
* The open modal the user is in. With several, the top layer decides, not page order (a viewer can sit earlier in the
* page than the dialog it opened from): only the top one can be hit, the rest being inert.
*/
function topModal() {
const modals = [...document.querySelectorAll('dialog')].filter(dialog => dialog.matches(':modal'));
if (modals.length < 2) return modals[0] ?? null;
const hit = document.elementFromPoint?.(window.innerWidth / 2, window.innerHeight / 2)?.closest('dialog');
return modals.includes(hit) ? hit : modals.at(-1);
}
/**
* #533: a modal covers and inerts the rest of the page, so the toast shows inside the topmost one (D176) and
* follows it out when it closes. Called by that dialog's `close` too; the same listener is only ever added once.
*/
function placeToast() {
const toast = toastElement;
const parent = toast.parentNode;
if (parent && parent.localName !== 'dialog') toastHome = parent;
const modal = topModal();
const host = modal ?? toastHome;
if (host && parent !== host) host.append(toast);
modal?.addEventListener('close', placeToast);
}
/** Whether the pointer or keyboard focus rests on the toast. */
const held = toast => typeof toast.matches === 'function' && toast.matches(':hover, :focus-within');
@@ -41,8 +69,11 @@ function scheduleClear(toast, delay) {
* for LINK_TOAST_MS and while the pointer or focus is on it.
*/
export function showWorkspaceToast(message) {
const toast = document.getElementById('workspace-toast');
// Null only when a dialog left the page with the toast still in it.
const toast = document.getElementById('workspace-toast') ?? toastElement;
if (!toast) return;
toastElement = toast;
placeToast();
releaseWait?.();
if (Array.isArray(message)) {
toast.replaceChildren(...message.map(part => {