diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx index 43afa5fdf4..45e5e76ba3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx @@ -4,6 +4,7 @@ import { EMPTY_ARRAY } from 'app/store/constants'; import { useAppStore } from 'app/store/storeHooks'; import { buildZodTypeGuard } from 'common/util/zodUtils'; import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared'; +import { canvasQueueItemDiscarded, selectDiscardedItems } from 'features/controlLayers/store/canvasStagingAreaSlice'; import type { ProgressImage } from 'features/nodes/types/common'; import type { Atom, MapStore, StoreValue, WritableAtom } from 'nanostores'; import { atom, computed, effect, map, subscribeKeys } from 'nanostores'; @@ -104,6 +105,7 @@ type CanvasSessionContextValue = { selectFirst: () => void; selectLast: () => void; onImageLoad: (itemId: number) => void; + discard: (itemId: number) => void; }; const CanvasSessionContext = createContext(null); @@ -138,14 +140,7 @@ export const CanvasSessionContextProvider = memo( * Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache * and kept in sync with it via a redux subscription. */ - const $allItems = useState(() => atom([]))[0]; - - /** - * All queue items for the current session, excluding canceled and failed items. - */ - const $items = useState(() => - computed([$allItems], (allItems) => allItems.filter(({ status }) => status !== 'canceled' && status !== 'failed')) - )[0]; + const $items = useState(() => atom([]))[0]; /** * Whether auto-switch is enabled. @@ -239,12 +234,27 @@ export const CanvasSessionContextProvider = memo( */ const selectQueueItems = useMemo( () => - createSelector(queueApi.endpoints.listAllQueueItems.select({ destination: session.id }), ({ data }) => { - return data ?? EMPTY_ARRAY; - }), + createSelector( + [queueApi.endpoints.listAllQueueItems.select({ destination: session.id }), selectDiscardedItems], + ({ data }, discardedItems) => { + if (!data) { + return EMPTY_ARRAY; + } + return data.filter( + ({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id) + ); + } + ), [session.id] ); + const discard = useCallback( + (itemId: number) => { + store.dispatch(canvasQueueItemDiscarded({ itemId })); + }, + [store] + ); + const selectNext = useCallback(() => { const selectedItemId = $selectedItemId.get(); if (selectedItemId === null) { @@ -350,22 +360,20 @@ export const CanvasSessionContextProvider = memo( // Set up state subscriptions and effects useEffect(() => { - let _prevAllItems: readonly S['SessionQueueItem'][] = []; - // Seed the $allItems atom with the initial query cache state - $allItems.set(selectQueueItems(store.getState())); + let _prevItems: readonly S['SessionQueueItem'][] = []; + // Seed the $items atom with the initial query cache state + $items.set(selectQueueItems(store.getState())); - // Manually keep the $allItems atom in sync as the query cache is updated + // Manually keep the $items atom in sync as the query cache is updated const unsubReduxSyncToItemsAtom = store.subscribe(() => { - const prevItems = $allItems.get(); + const prevItems = $items.get(); const items = selectQueueItems(store.getState()); if (items !== prevItems) { - _prevAllItems = prevItems; - $allItems.set(items); + _prevItems = prevItems; + $items.set(items); } }); - let _prevItems: readonly S['SessionQueueItem'][] = []; - // Handle cases that could result in a nonexistent queue item being selected. const unsubEnsureSelectedItemIdExists = effect( [$items, $selectedItemId, $lastStartedItemId], @@ -500,14 +508,13 @@ export const CanvasSessionContextProvider = memo( unsubReduxSyncToItemsAtom(); unsubEnsureSelectedItemIdExists(); unsubCleanUpProgressData(); - $allItems.set([]); + $items.set([]); $progressData.set({}); $selectedItemId.set(null); }; }, [ - $allItems, - $autoSwitch, $items, + $autoSwitch, $lastLoadedItemId, $lastStartedItemId, $progressData, @@ -535,6 +542,7 @@ export const CanvasSessionContextProvider = memo( selectFirst, selectLast, onImageLoad, + discard, }), [ $autoSwitch, @@ -553,6 +561,7 @@ export const CanvasSessionContextProvider = memo( selectFirst, selectLast, onImageLoad, + discard, ] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx index b73948584c..21305f1d51 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx @@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context'; import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem'; +import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiXBold } from 'react-icons/pi'; @@ -11,7 +11,7 @@ import { PiXBold } from 'react-icons/pi'; export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { isDisabled?: boolean }) => { const dispatch = useAppDispatch(); const ctx = useCanvasSessionContext(); - const deleteQueueItem = useDeleteQueueItem(); + const cancelQueueItem = useCancelQueueItem(); const selectedItemId = useStore(ctx.$selectedItemId); const { t } = useTranslation(); @@ -20,7 +20,8 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i if (selectedItemId === null) { return; } - await deleteQueueItem.trigger(selectedItemId, { withToast: false }); + ctx.discard(selectedItemId); + await cancelQueueItem.trigger(selectedItemId, { withToast: false }); const itemCount = ctx.$itemCount.get(); if (itemCount <= 1) { if (ctx.session.type === 'advanced') { @@ -30,7 +31,7 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i dispatch(generateSessionReset()); } } - }, [selectedItemId, deleteQueueItem, ctx.$itemCount, ctx.session.type, dispatch]); + }, [selectedItemId, ctx, cancelQueueItem, dispatch]); return ( ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 8c0da9c86f..3ac40b5a8a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -6,11 +6,13 @@ import { canvasReset } from 'features/controlLayers/store/actions'; type CanvasStagingAreaState = { generateSessionId: string | null; canvasSessionId: string | null; + canvasDiscardedQueueItems: number[]; }; const INITIAL_STATE: CanvasStagingAreaState = { generateSessionId: null, canvasSessionId: null, + canvasDiscardedQueueItems: [], }; const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE); @@ -26,12 +28,20 @@ export const canvasSessionSlice = createSlice({ generateSessionReset: (state) => { state.generateSessionId = null; }, + canvasQueueItemDiscarded: (state, action: PayloadAction<{ itemId: number }>) => { + const { itemId } = action.payload; + if (!state.canvasDiscardedQueueItems.includes(itemId)) { + state.canvasDiscardedQueueItems.push(itemId); + } + }, canvasSessionIdChanged: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; state.canvasSessionId = id; + state.canvasDiscardedQueueItems = []; }, canvasSessionReset: (state) => { state.canvasSessionId = null; + state.canvasDiscardedQueueItems = []; }, }, extraReducers(builder) { @@ -41,8 +51,13 @@ export const canvasSessionSlice = createSlice({ }, }); -export const { generateSessionIdChanged, generateSessionReset, canvasSessionIdChanged, canvasSessionReset } = - canvasSessionSlice.actions; +export const { + generateSessionIdChanged, + generateSessionReset, + canvasSessionIdChanged, + canvasSessionReset, + canvasQueueItemDiscarded, +} = canvasSessionSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { @@ -64,3 +79,7 @@ export const selectGenerateSessionId = createSelector( ({ generateSessionId }) => generateSessionId ); export const selectIsStaging = createSelector(selectCanvasSessionId, (canvasSessionId) => canvasSessionId !== null); +export const selectDiscardedItems = createSelector( + selectCanvasSessionSlice, + ({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems +);