diff --git a/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx b/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx index 1b1a97df78..541c7c2575 100644 --- a/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx +++ b/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx @@ -5,7 +5,7 @@ import { faTrashXmark, faXmark, } from "@fortawesome/pro-solid-svg-icons"; -import { Fragment, useEffect, useRef } from "react"; +import { Fragment, useEffect, useRef, useState } from "react"; import { twMerge } from "tailwind-merge"; import { BaseSelectorImage, ImageBundle } from "./types"; import { Tooltip } from "@storyteller/ui-tooltip"; @@ -21,6 +21,7 @@ interface HistoryStackProps { onClear: () => void; onImageSelect?: (image: BaseSelectorImage) => void; onImageRemove?: (image: BaseSelectorImage) => void; + onPendingRemove?: (id: string) => void; imageBundles: ImageBundle[]; pendingPlaceholders?: { id: string; count: number }[]; selectedImageToken?: string; @@ -31,6 +32,7 @@ export const HistoryStack = ({ onClear, onImageSelect = () => {}, onImageRemove = () => {}, + onPendingRemove = () => {}, imageBundles, pendingPlaceholders = [], selectedImageToken, @@ -69,6 +71,66 @@ export const HistoryStack = ({ onImageRemove(baseImage); }; + // Tracks which pending tile (if any) currently has its remove-confirmation + // modal open, so we can auto-close the modal if the underlying generation + // finishes loading before the user confirms. + const [pendingRemoveTargetId, setPendingRemoveTargetId] = useState< + string | null + >(null); + + const handlePendingRemoveClick = (id: string) => { + setPendingRemoveTargetId(id); + showActionReminder({ + reminderType: "default", + title: "Remove pending generation", + primaryActionIcon: faTrashXmark, + primaryActionBtnClassName: "bg-red hover:bg-red/80", + message: ( + <> +
+ Stop showing this generation in the history stack? This action + cannot be undone. +
++ The image may still finish in the background and will be available + in your library when it does. +
+ > + ), + primaryActionText: "Remove", + onPrimaryAction: () => { + onPendingRemove(id); + setPendingRemoveTargetId(null); + isActionReminderOpen.value = false; + }, + }); + }; + + // If the pending generation we opened the confirm modal for resolves + // before the user clicks confirm, close the modal — there's nothing left + // to dismiss. + useEffect(() => { + if (pendingRemoveTargetId === null) return; + const stillPending = pendingPlaceholders.some( + (p) => p.id === pendingRemoveTargetId, + ); + if (!stillPending) { + isActionReminderOpen.value = false; + setPendingRemoveTargetId(null); + } + }, [pendingPlaceholders, pendingRemoveTargetId]); + + // If the modal is dismissed by other means (Escape, backdrop click), clear + // our target so we don't try to manage a modal that's already gone. + // Uses the signal's subscribe API so we react to external dismissals. + useEffect(() => { + if (pendingRemoveTargetId === null) return; + const unsubscribe = isActionReminderOpen.subscribe((isOpen) => { + if (!isOpen) setPendingRemoveTargetId(null); + }); + return unsubscribe; + }, [pendingRemoveTargetId]); + // Scroll to top when new pending placeholders are added (after enqueue) useEffect(() => { const current = pendingPlaceholders.length; @@ -126,7 +188,7 @@ export const HistoryStack = ({ (_, i) => (