From 2ddcde13ffc67acde1fd86f1ab70365bd8dfc304 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:29:43 +1000 Subject: [PATCH] refactor(ui): migrate from canceling queue items to deleteing, make queue hook APIs consistent --- invokeai/app/api/routers/session_queue.py | 13 +++ .../session_queue/session_queue_base.py | 6 ++ .../session_queue/session_queue_common.py | 6 ++ .../session_queue/session_queue_sqlite.py | 32 +++++++ .../app/components/GlobalModalIsolator.tsx | 2 + .../web/src/common/hooks/useGlobalHotkeys.ts | 22 ++--- .../components/SimpleSession/context.tsx | 86 ++++++------------- .../StagingAreaToolbarAcceptButton.tsx | 18 ++-- .../StagingAreaToolbarDiscardAllButton.tsx | 11 +-- ...tagingAreaToolbarDiscardSelectedButton.tsx | 9 +- .../CancelAllExceptCurrentIconButton.tsx | 25 ++++++ ...urrentQueueItemConfirmationAlertDialog.tsx | 11 ++- .../components/ClearInvocationCacheButton.tsx | 8 +- .../ClearQueueConfirmationAlertDialog.tsx | 11 ++- .../queue/components/ClearQueueIconButton.tsx | 58 ------------- ...urrentQueueItemConfirmationAlertDialog.tsx | 46 ++++++++++ .../DeleteCurrentQueueItemIconButton.tsx | 25 ++++++ .../queue/components/PauseProcessorButton.tsx | 8 +- .../queue/components/PruneQueueButton.tsx | 11 +-- .../components/QueueActionsMenuButton.tsx | 44 ++++------ .../queue/components/QueueControls.tsx | 19 +++- .../QueueList/QueueItemComponent.tsx | 26 +++--- .../components/QueueList/QueueItemDetail.tsx | 42 ++++++--- .../components/QueueTabQueueControls.tsx | 4 +- .../components/ResumeProcessorButton.tsx | 8 +- .../ToggleInvocationCacheButton.tsx | 24 +++--- .../queue/hooks/useBatchIsCanceled.ts | 20 +++++ .../useCancelAllExceptCurrentQueueItem.ts | 22 ++--- .../features/queue/hooks/useCancelBatch.ts | 60 +++++-------- .../queue/hooks/useCancelCurrentQueueItem.ts | 47 +++------- .../queue/hooks/useCancelQueueItem.ts | 41 +++++---- .../hooks/useCancelQueueItemsByDestination.ts | 33 +++++++ .../queue/hooks/useClearInvocationCache.ts | 18 ++-- .../src/features/queue/hooks/useClearQueue.ts | 19 ++-- .../queue/hooks/useCurrentDestination.ts | 11 --- .../queue/hooks/useCurrentQueueItemId.ts | 11 +++ .../useDeleteAllExceptCurrentQueueItem.ts | 38 ++++++++ .../queue/hooks/useDeleteCurrentQueueItem.ts | 20 +++++ .../queue/hooks/useDeleteQueueItem.ts | 33 +++++++ .../hooks/useDeleteQueueItemsByDestination.ts | 33 +++++++ .../queue/hooks/useDisableInvocationCache.ts | 26 +++--- .../queue/hooks/useEnableInvocationCache.ts | 25 +++--- .../features/queue/hooks/usePauseProcessor.ts | 19 ++-- .../src/features/queue/hooks/usePruneQueue.ts | 45 +++++----- .../queue/hooks/useResumeProcessor.ts | 19 ++-- .../features/queue/hooks/useRetryQueueItem.ts | 45 +++++----- .../system/components/ProgressBar.tsx | 14 +-- .../components/FloatingLeftPanelButtons.tsx | 32 +++---- .../web/src/services/api/endpoints/queue.ts | 20 ++++- .../frontend/web/src/services/api/schema.ts | 63 ++++++++++++++ 50 files changed, 773 insertions(+), 516 deletions(-) create mode 100644 invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentIconButton.tsx delete mode 100644 invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx create mode 100644 invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.tsx create mode 100644 invokeai/frontend/web/src/features/queue/components/DeleteCurrentQueueItemIconButton.tsx create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useBatchIsCanceled.ts create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts delete mode 100644 invokeai/frontend/web/src/features/queue/hooks/useCurrentDestination.ts create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useDeleteCurrentQueueItem.ts create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts create mode 100644 invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItemsByDestination.ts diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py index f19f4bc47c..5902a492d4 100644 --- a/invokeai/app/api/routers/session_queue.py +++ b/invokeai/app/api/routers/session_queue.py @@ -14,6 +14,7 @@ from invokeai.app.services.session_queue.session_queue_common import ( CancelByBatchIDsResult, CancelByDestinationResult, ClearResult, + DeleteAllExceptCurrentResult, DeleteByDestinationResult, EnqueueBatchResult, FieldIdentifier, @@ -146,6 +147,18 @@ async def cancel_all_except_current( return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id) +@session_queue_router.put( + "/{queue_id}/delete_all_except_current", + operation_id="delete_all_except_current", + responses={200: {"model": DeleteAllExceptCurrentResult}}, +) +async def delete_all_except_current( + queue_id: str = Path(description="The queue id to perform this operation on"), +) -> DeleteAllExceptCurrentResult: + """Immediately deletes all queue items except in-processing items""" + return ApiDependencies.invoker.services.session_queue.delete_all_except_current(queue_id=queue_id) + + @session_queue_router.put( "/{queue_id}/cancel_by_batch_ids", operation_id="cancel_by_batch_ids", diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py index 17c31e77a7..aa1126576d 100644 --- a/invokeai/app/services/session_queue/session_queue_base.py +++ b/invokeai/app/services/session_queue/session_queue_base.py @@ -10,6 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import ( CancelByDestinationResult, CancelByQueueIDResult, ClearResult, + DeleteAllExceptCurrentResult, DeleteByDestinationResult, EnqueueBatchResult, IsEmptyResult, @@ -129,6 +130,11 @@ class SessionQueueBase(ABC): """Cancels all queue items except in-progress items""" pass + @abstractmethod + def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult: + """Deletes all queue items except in-progress items""" + pass + @abstractmethod def list_queue_items( self, diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index d41fb44533..9075732901 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -369,6 +369,12 @@ class DeleteByDestinationResult(BaseModel): deleted: int = Field(..., description="Number of queue items deleted") +class DeleteAllExceptCurrentResult(DeleteByDestinationResult): + """Result of deleting all except current""" + + pass + + class CancelByQueueIDResult(CancelByBatchIDsResult): """Result of canceling by queue id""" diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index c31226581a..36b4f05cf3 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -17,6 +17,7 @@ from invokeai.app.services.session_queue.session_queue_common import ( CancelByDestinationResult, CancelByQueueIDResult, ClearResult, + DeleteAllExceptCurrentResult, DeleteByDestinationResult, EnqueueBatchResult, IsEmptyResult, @@ -489,6 +490,37 @@ class SqliteSessionQueue(SessionQueueBase): raise return DeleteByDestinationResult(deleted=count) + def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult: + try: + cursor = self._conn.cursor() + where = """--sql + WHERE + queue_id == ? + AND status == 'pending' + """ + cursor.execute( + f"""--sql + SELECT COUNT(*) + FROM session_queue + {where}; + """, + (queue_id,), + ) + count = cursor.fetchone()[0] + cursor.execute( + f"""--sql + DELETE + FROM session_queue + {where}; + """, + (queue_id,), + ) + self._conn.commit() + except Exception: + self._conn.rollback() + raise + return DeleteAllExceptCurrentResult(deleted=count) + def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult: try: cursor = self._conn.cursor() diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx index ae83f0c5c2..782b338fe4 100644 --- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx @@ -16,6 +16,7 @@ import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal'; import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; +import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog'; import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog'; import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal'; import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal'; @@ -40,6 +41,7 @@ export const GlobalModalIsolator = memo(() => { + diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index 76c2db9a6d..88fc2bfdfb 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -1,6 +1,6 @@ import { useAppDispatch } from 'app/store/storeHooks'; -import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; +import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem'; import { useInvoke } from 'features/queue/hooks/useInvoke'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; @@ -35,34 +35,30 @@ export const useGlobalHotkeys = () => { dependencies: [queue], }); - const { - cancelQueueItem, - isDisabled: isDisabledCancelQueueItem, - isLoading: isLoadingCancelQueueItem, - } = useCancelCurrentQueueItem(); + const deleteCurrentQueueItem = useDeleteCurrentQueueItem(); useRegisteredHotkeys({ id: 'cancelQueueItem', category: 'app', - callback: cancelQueueItem, + callback: deleteCurrentQueueItem.trigger, options: { - enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem, + enabled: !deleteCurrentQueueItem.isDisabled && !deleteCurrentQueueItem.isLoading, preventDefault: true, }, - dependencies: [cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem], + dependencies: [deleteCurrentQueueItem], }); - const { clearQueue, isDisabled: isDisabledClearQueue, isLoading: isLoadingClearQueue } = useClearQueue(); + const clearQueue = useClearQueue(); useRegisteredHotkeys({ id: 'clearQueue', category: 'app', - callback: clearQueue, + callback: clearQueue.trigger, options: { - enabled: !isDisabledClearQueue && !isLoadingClearQueue, + enabled: !clearQueue.isDisabled && !clearQueue.isLoading, preventDefault: true, }, - dependencies: [clearQueue, isDisabledClearQueue, isLoadingClearQueue], + dependencies: [clearQueue], }); useRegisteredHotkeys({ 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 33b5fc9cc1..daf843f947 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx @@ -67,34 +67,6 @@ const setProgress = ($progressData: WritableAtom>, } }; -const clearProgressEvent = ($progressData: WritableAtom>, itemId: number) => { - const progressData = $progressData.get(); - const current = progressData[itemId]; - if (!current) { - return; - } - const next = { ...current }; - next.progressEvent = null; - $progressData.set({ - ...progressData, - [itemId]: next, - }); -}; - -const clearProgressImage = ($progressData: WritableAtom>, itemId: number) => { - const progressData = $progressData.get(); - const current = progressData[itemId]; - if (!current) { - return; - } - const next = { ...current }; - next.progressImage = null; - $progressData.set({ - ...progressData, - [itemId]: next, - }); -}; - type CanvasSessionContextValue = { session: { id: string; type: 'simple' | 'advanced' }; $items: Atom; @@ -132,15 +104,11 @@ export const CanvasSessionContextProvider = memo( const socket = useStore($socket); /** - * Manually-synced atom containing the queue items for the current session. + * 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 $items = useState(() => atom([]))[0]; - /** - * Manually-synced atom containing the queue items for the current session. - */ - const $prevItems = useState(() => atom([]))[0]; - /** * Whether auto-switch is enabled. */ @@ -294,28 +262,16 @@ export const CanvasSessionContextProvider = memo( setProgress($progressData, data); }; - const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => { - if (data.destination !== session.id) { - return; - } - - if (data.status === 'canceled' || data.status === 'failed') { - clearProgressEvent($progressData, data.item_id); - clearProgressImage($progressData, data.item_id); - } - }; - socket.on('invocation_progress', onProgress); - socket.on('queue_item_status_changed', onQueueItemStatusChanged); return () => { socket.off('invocation_progress', onProgress); - socket.off('queue_item_status_changed', onQueueItemStatusChanged); }; }, [$autoSwitch, $progressData, $selectedItemId, session.id, socket]); // Set up state subscriptions and effects useEffect(() => { + let _prevItems: readonly S['SessionQueueItem'][] = []; // Seed the $items atom with the initial query cache state $items.set(selectQueueItems(store.getState())); @@ -324,7 +280,7 @@ export const CanvasSessionContextProvider = memo( const prevItems = $items.get(); const items = selectQueueItems(store.getState()); if (items !== prevItems) { - $prevItems.set(prevItems); + _prevItems = prevItems; $items.set(items); } }); @@ -344,7 +300,7 @@ export const CanvasSessionContextProvider = memo( // If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll // the above case, selecting the first item if there are any. if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) { - let prevIndex = $prevItems.get().findIndex(({ item_id }) => item_id === selectedItemId); + let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId); if (prevIndex >= items.length) { prevIndex = items.length - 1; } @@ -357,19 +313,37 @@ export const CanvasSessionContextProvider = memo( // Clean up the progress data when a queue item is discarded. const unsubCleanUpProgressData = $items.listen((items) => { const progressData = $progressData.get(); + const toDelete: number[] = []; + const toClear: number[] = []; + for (const datum of Object.values(progressData)) { - if (items.findIndex(({ item_id }) => item_id === datum.itemId) === -1) { + const item = items.find(({ item_id }) => item_id === datum.itemId); + if (!item) { toDelete.push(datum.itemId); + } else if (item.status === 'canceled' || item.status === 'failed') { + toClear.push(datum.itemId); } } + if (toDelete.length === 0) { return; } + const newProgressData = { ...progressData }; + for (const itemId of toDelete) { delete newProgressData[itemId]; } + + for (const itemId of toClear) { + const current = newProgressData[itemId]; + if (current) { + current.progressEvent = null; + current.progressImage = null; + } + } + $progressData.set(newProgressData); }); @@ -408,17 +382,7 @@ export const CanvasSessionContextProvider = memo( $progressData.set({}); $selectedItemId.set(null); }; - }, [ - $autoSwitch, - $items, - $lastLoadedItemId, - $prevItems, - $progressData, - $selectedItemId, - selectQueueItems, - session.id, - store, - ]); + }, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]); const value = useMemo( () => ({ diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx index b7280327eb..1272887abc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx @@ -9,11 +9,11 @@ import { canvasSessionGenerationFinished } from 'features/controlLayers/store/ca import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageNameToImageObject } from 'features/controlLayers/store/util'; +import { useDeleteQueueItemsByDestination } from 'features/queue/hooks/useDeleteQueueItemsByDestination'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; -import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue'; export const StagingAreaToolbarAcceptButton = memo(() => { const ctx = useCanvasSessionContext(); @@ -24,7 +24,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => { const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage); const isCanvasFocused = useIsRegionFocused('canvas'); const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName); - const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation(); + const deleteQueueItemsByDestination = useDeleteQueueItemsByDestination(); const { t } = useTranslation(); @@ -41,8 +41,15 @@ export const StagingAreaToolbarAcceptButton = memo(() => { dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' })); dispatch(canvasSessionGenerationFinished()); - deleteByDestination({ destination: ctx.session.id }); - }, [selectedItemImageName, bboxRect, dispatch, selectedEntityIdentifier?.type, deleteByDestination, ctx.session.id]); + deleteQueueItemsByDestination.trigger(ctx.session.id); + }, [ + selectedItemImageName, + bboxRect, + dispatch, + selectedEntityIdentifier?.type, + deleteQueueItemsByDestination, + ctx.session.id, + ]); useHotkeys( ['enter'], @@ -61,7 +68,8 @@ export const StagingAreaToolbarAcceptButton = memo(() => { icon={} onClick={acceptSelected} colorScheme="invokeBlue" - isDisabled={!selectedItemImageName || !shouldShowStagedImage} + isDisabled={!selectedItemImageName || !shouldShowStagedImage || deleteQueueItemsByDestination.isDisabled} + isLoading={deleteQueueItemsByDestination.isLoading} /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx index d3cc4b20e4..9c7f653898 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx @@ -2,21 +2,21 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context'; import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useDeleteQueueItemsByDestination } from 'features/queue/hooks/useDeleteQueueItemsByDestination'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; -import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue'; export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisabled?: boolean }) => { const ctx = useCanvasSessionContext(); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation(); + const deleteQueueItemsByDestination = useDeleteQueueItemsByDestination(); const discardAll = useCallback(() => { - deleteByDestination({ destination: ctx.session.id }); + deleteQueueItemsByDestination.trigger(ctx.session.id); dispatch(canvasSessionGenerationFinished()); - }, [deleteByDestination, ctx.session.id, dispatch]); + }, [deleteQueueItemsByDestination, ctx.session.id, dispatch]); return ( ); }); 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 e1c0738fe9..b6fc9d934a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx @@ -3,15 +3,15 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context'; import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiXBold } from 'react-icons/pi'; -import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue'; export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { isDisabled?: boolean }) => { const dispatch = useAppDispatch(); const ctx = useCanvasSessionContext(); - const [deleteQueueItem] = useDeleteQueueItemMutation(); + const deleteQueueItem = useDeleteQueueItem(); const selectedItemId = useStore(ctx.$selectedItemId); const { t } = useTranslation(); @@ -21,10 +21,10 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i return; } const itemCount = ctx.$itemCount.get(); - deleteQueueItem({ item_id: selectedItemId }); if (itemCount <= 1) { dispatch(canvasSessionGenerationFinished()); } + deleteQueueItem.trigger(selectedItemId); }, [selectedItemId, ctx.$itemCount, deleteQueueItem, dispatch]); return ( @@ -35,7 +35,8 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i onClick={discardSelected} colorScheme="invokeBlue" fontSize={16} - isDisabled={selectedItemId === null || isDisabled} + isDisabled={selectedItemId === null || deleteQueueItem.isDisabled || isDisabled} + isLoading={deleteQueueItem.isLoading} /> ); }); diff --git a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentIconButton.tsx new file mode 100644 index 0000000000..1d83b409b6 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentIconButton.tsx @@ -0,0 +1,25 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiXCircle } from 'react-icons/pi'; + +export const CancelAllExceptCurrentIconButton = memo(() => { + const { t } = useTranslation(); + const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog(); + + return ( + } + colorScheme="error" + onClick={cancelAllExceptCurrent.openDialog} + /> + ); +}); + +CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton'; diff --git a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx index cdaaebcdf0..a27914a4c1 100644 --- a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx @@ -9,16 +9,15 @@ const [useCancelAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBool export const useCancelAllExceptCurrentQueueItemDialog = () => { const dialog = useCancelAllExceptCurrentQueueItemConfirmationAlertDialog(); - const { cancelAllExceptCurrentQueueItem, isLoading, isDisabled, queueStatus } = useCancelAllExceptCurrentQueueItem(); + const cancelAllExceptCurrentQueueItem = useCancelAllExceptCurrentQueueItem(); return { - cancelAllExceptCurrentQueueItem, + trigger: cancelAllExceptCurrentQueueItem.trigger, isOpen: dialog.isTrue, openDialog: dialog.setTrue, closeDialog: dialog.setFalse, - isLoading, - queueStatus, - isDisabled, + isLoading: cancelAllExceptCurrentQueueItem.isLoading, + isDisabled: cancelAllExceptCurrentQueueItem.isDisabled, }; }; @@ -32,7 +31,7 @@ export const CancelAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() => isOpen={cancelAllExceptCurrentQueueItem.isOpen} onClose={cancelAllExceptCurrentQueueItem.closeDialog} title={t('queue.cancelAllExceptCurrentTooltip')} - acceptCallback={cancelAllExceptCurrentQueueItem.cancelAllExceptCurrentQueueItem} + acceptCallback={cancelAllExceptCurrentQueueItem.trigger} acceptButtonText={t('queue.confirm')} useInert={false} > diff --git a/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx index 52b1b237e7..6a9f157361 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx @@ -5,10 +5,14 @@ import { useTranslation } from 'react-i18next'; const ClearInvocationCacheButton = () => { const { t } = useTranslation(); - const { clearInvocationCache, isDisabled, isLoading } = useClearInvocationCache(); + const clearInvocationCache = useClearInvocationCache(); return ( - ); diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx index 9de86f9eb7..be1bf7ccb5 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx @@ -9,16 +9,15 @@ const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false); const useClearQueueDialog = () => { const dialog = useClearQueueConfirmationAlertDialog(); - const { clearQueue, isLoading, isDisabled, queueStatus } = useClearQueue(); + const clearQueue = useClearQueue(); return { - clearQueue, isOpen: dialog.isTrue, openDialog: dialog.setTrue, closeDialog: dialog.setFalse, - isLoading, - queueStatus, - isDisabled, + trigger: clearQueue.trigger, + isLoading: clearQueue.isLoading, + isDisabled: clearQueue.isDisabled, }; }; @@ -32,7 +31,7 @@ export const ClearQueueConfirmationsAlertDialog = memo(() => { isOpen={clearQueue.isOpen} onClose={clearQueue.closeDialog} title={t('queue.clearTooltip')} - acceptCallback={clearQueue.clearQueue} + acceptCallback={clearQueue.trigger} acceptButtonText={t('queue.clear')} useInert={false} > diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx deleted file mode 100644 index 9f7a4290fa..0000000000 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { IconButton, useShiftModifier } from '@invoke-ai/ui-library'; -import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; -import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiXBold, PiXCircle } from 'react-icons/pi'; - -export const ClearQueueIconButton = memo(() => { - const shift = useShiftModifier(); - - if (!shift) { - return ; - } - - return ; -}); - -ClearQueueIconButton.displayName = 'ClearQueueIconButton'; - -const CancelCurrentIconButton = memo(() => { - const { t } = useTranslation(); - const cancelCurrentQueueItem = useCancelCurrentQueueItem(); - - return ( - } - colorScheme="error" - onClick={cancelCurrentQueueItem.cancelQueueItem} - /> - ); -}); - -CancelCurrentIconButton.displayName = 'CancelCurrentIconButton'; - -const CancelAllExceptCurrentIconButton = memo(() => { - const { t } = useTranslation(); - const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog(); - - return ( - } - colorScheme="error" - onClick={cancelAllExceptCurrent.openDialog} - /> - ); -}); - -CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton'; diff --git a/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.tsx new file mode 100644 index 0000000000..ac5dc4ef93 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.tsx @@ -0,0 +1,46 @@ +import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { buildUseBoolean } from 'common/hooks/useBoolean'; +import { useDeleteAllExceptCurrentQueueItem } from 'features/queue/hooks/useDeleteAllExceptCurrentQueueItem'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const [useDeleteAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBoolean(false); + +export const useDeleteAllExceptCurrentQueueItemDialog = () => { + const dialog = useDeleteAllExceptCurrentQueueItemConfirmationAlertDialog(); + const deleteAllExceptCurrentQueueItem = useDeleteAllExceptCurrentQueueItem(); + + return { + trigger: deleteAllExceptCurrentQueueItem.trigger, + isOpen: dialog.isTrue, + openDialog: dialog.setTrue, + closeDialog: dialog.setFalse, + isLoading: deleteAllExceptCurrentQueueItem.isLoading, + isDisabled: deleteAllExceptCurrentQueueItem.isDisabled, + }; +}; + +export const DeleteAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() => { + useAssertSingleton('DeleteAllExceptCurrentQueueItemConfirmationAlertDialog'); + const { t } = useTranslation(); + const deleteAllExceptCurrentQueueItem = useDeleteAllExceptCurrentQueueItemDialog(); + + return ( + + {t('queue.cancelAllExceptCurrentQueueItemAlertDialog')} +
+ {t('queue.cancelAllExceptCurrentQueueItemAlertDialog2')} +
+ ); +}); + +DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.displayName = + 'DeleteAllExceptCurrentQueueItemConfirmationAlertDialog'; diff --git a/invokeai/frontend/web/src/features/queue/components/DeleteCurrentQueueItemIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/DeleteCurrentQueueItemIconButton.tsx new file mode 100644 index 0000000000..22009632ac --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/DeleteCurrentQueueItemIconButton.tsx @@ -0,0 +1,25 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiXBold } from 'react-icons/pi'; + +export const DeleteCurrentQueueItemIconButton = memo(() => { + const { t } = useTranslation(); + const deleteCurrentQueueItem = useDeleteCurrentQueueItem(); + + return ( + } + colorScheme="error" + /> + ); +}); + +DeleteCurrentQueueItemIconButton.displayName = 'DeleteCurrentQueueItemIconButton'; diff --git a/invokeai/frontend/web/src/features/queue/components/PauseProcessorButton.tsx b/invokeai/frontend/web/src/features/queue/components/PauseProcessorButton.tsx index 45a95951f9..36d07a1c59 100644 --- a/invokeai/frontend/web/src/features/queue/components/PauseProcessorButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/PauseProcessorButton.tsx @@ -11,17 +11,17 @@ type Props = { const PauseProcessorButton = ({ asIconButton }: Props) => { const { t } = useTranslation(); - const { pauseProcessor, isLoading, isDisabled } = usePauseProcessor(); + const pauseProcessor = usePauseProcessor(); return ( } - onClick={pauseProcessor} + onClick={pauseProcessor.trigger} colorScheme="gold" /> ); diff --git a/invokeai/frontend/web/src/features/queue/components/PruneQueueButton.tsx b/invokeai/frontend/web/src/features/queue/components/PruneQueueButton.tsx index 8ecf7a60c9..21f818b9ba 100644 --- a/invokeai/frontend/web/src/features/queue/components/PruneQueueButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/PruneQueueButton.tsx @@ -1,4 +1,4 @@ -import { usePruneQueue } from 'features/queue/hooks/usePruneQueue'; +import { useFinishedCount, usePruneQueue } from 'features/queue/hooks/usePruneQueue'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiBroomBold } from 'react-icons/pi'; @@ -11,17 +11,18 @@ type Props = { const PruneQueueButton = ({ asIconButton }: Props) => { const { t } = useTranslation(); - const { pruneQueue, isLoading, finishedCount, isDisabled } = usePruneQueue(); + const pruneQueue = usePruneQueue(); + const finishedCount = useFinishedCount(); return ( } - onClick={pruneQueue} colorScheme="invokeBlue" /> ); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx index e9a81c4c19..fa7546a608 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx @@ -1,9 +1,9 @@ import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { SessionMenuItems } from 'common/components/SessionMenuItems'; -import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; +import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog'; import { QueueCountBadge } from 'features/queue/components/QueueCountBadge'; -import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; +import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem'; import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor'; import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; @@ -18,18 +18,10 @@ export const QueueActionsMenuButton = memo(() => { const { t } = useTranslation(); const isPauseEnabled = useFeatureStatus('pauseQueue'); const isResumeEnabled = useFeatureStatus('resumeQueue'); - const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog(); - const cancelCurrent = useCancelCurrentQueueItem(); - const { - resumeProcessor, - isLoading: isLoadingResumeProcessor, - isDisabled: isDisabledResumeProcessor, - } = useResumeProcessor(); - const { - pauseProcessor, - isLoading: isLoadingPauseProcessor, - isDisabled: isDisabledPauseProcessor, - } = usePauseProcessor(); + const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog(); + const deleteCurrentQueueItem = useDeleteCurrentQueueItem(); + const resumeProcessor = useResumeProcessor(); + const pauseProcessor = usePauseProcessor(); const openQueue = useCallback(() => { dispatch(setActiveTab('queue')); }, [dispatch]); @@ -46,27 +38,27 @@ export const QueueActionsMenuButton = memo(() => { } - onClick={cancelCurrent.cancelQueueItem} - isLoading={cancelCurrent.isLoading} - isDisabled={cancelCurrent.isDisabled} + onClick={deleteCurrentQueueItem.trigger} + isLoading={deleteCurrentQueueItem.isLoading} + isDisabled={deleteCurrentQueueItem.isDisabled} > {t('queue.cancelTooltip')} } - onClick={cancelAllExceptCurrent.openDialog} - isLoading={cancelAllExceptCurrent.isLoading} - isDisabled={cancelAllExceptCurrent.isDisabled} + onClick={deleteAllExceptCurrent.openDialog} + isLoading={deleteAllExceptCurrent.isLoading} + isDisabled={deleteAllExceptCurrent.isDisabled} > {t('queue.cancelAllExceptCurrentTooltip')} {isResumeEnabled && ( } - onClick={resumeProcessor} - isLoading={isLoadingResumeProcessor} - isDisabled={isDisabledResumeProcessor} + onClick={resumeProcessor.trigger} + isLoading={resumeProcessor.isLoading} + isDisabled={resumeProcessor.isDisabled} > {t('queue.resumeTooltip')} @@ -74,9 +66,9 @@ export const QueueActionsMenuButton = memo(() => { {isPauseEnabled && ( } - onClick={pauseProcessor} - isLoading={isLoadingPauseProcessor} - isDisabled={isDisabledPauseProcessor} + onClick={pauseProcessor.trigger} + isLoading={pauseProcessor.isLoading} + isDisabled={pauseProcessor.isDisabled} > {t('queue.pauseTooltip')} diff --git a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx index d3f05cb30b..040ecc6de9 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx @@ -1,5 +1,6 @@ -import { Flex, Spacer } from '@invoke-ai/ui-library'; -import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton'; +import { Flex, Spacer, useShiftModifier } from '@invoke-ai/ui-library'; +import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog'; +import { DeleteCurrentQueueItemIconButton } from 'features/queue/components/DeleteCurrentQueueItemIconButton'; import { QueueActionsMenuButton } from 'features/queue/components/QueueActionsMenuButton'; import ProgressBar from 'features/system/components/ProgressBar'; import { memo } from 'react'; @@ -13,7 +14,7 @@ const QueueControls = () => { - + @@ -21,3 +22,15 @@ const QueueControls = () => { }; export default memo(QueueControls); + +export const DeleteIconButton = memo(() => { + const shift = useShiftModifier(); + + if (!shift) { + return ; + } + + return ; +}); + +DeleteIconButton.displayName = 'DeleteIconButton'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx index d645dcbfe5..1f6cea985a 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx @@ -3,7 +3,7 @@ import { Badge, ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge'; import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText'; import { useOriginText } from 'features/queue/components/QueueList/useOriginText'; -import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem'; +import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem'; import { useRetryQueueItem } from 'features/queue/hooks/useRetryQueueItem'; import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; @@ -38,21 +38,21 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => { const handleToggle = useCallback(() => { context.toggleQueueItem(item.item_id); }, [context, item.item_id]); - const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item.item_id); - const handleCancelQueueItem = useCallback( + const deleteQueueItem = useDeleteQueueItem(); + const onClickDeleteQueueItem = useCallback( (e: MouseEvent) => { e.stopPropagation(); - cancelQueueItem(); + deleteQueueItem.trigger(item.item_id); }, - [cancelQueueItem] + [deleteQueueItem, item.item_id] ); - const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item.item_id); - const handleRetryQueueItem = useCallback( + const retryQueueItem = useRetryQueueItem(); + const onClickRetryQueueItem = useCallback( (e: MouseEvent) => { e.stopPropagation(); - retryQueueItem(); + retryQueueItem.trigger(item.item_id); }, - [retryQueueItem] + [item.item_id, retryQueueItem] ); const isOpen = useMemo(() => context.openQueueItems.includes(item.item_id), [context.openQueueItems, item.item_id]); @@ -135,17 +135,17 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => { {(!isFailed || !isRetryEnabled || isValidationRun) && ( } /> )} {isFailed && isRetryEnabled && !isValidationRun && ( } /> diff --git a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx index a06e82ac5b..f4fc5f7c53 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemDetail.tsx @@ -2,14 +2,15 @@ import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText'; import { useOriginText } from 'features/queue/components/QueueList/useOriginText'; +import { useBatchIsCanceled } from 'features/queue/hooks/useBatchIsCanceled'; import { useCancelBatch } from 'features/queue/hooks/useCancelBatch'; -import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem'; +import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem'; import { useRetryQueueItem } from 'features/queue/hooks/useRetryQueueItem'; import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { get } from 'lodash-es'; import type { ReactNode } from 'react'; -import { memo, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiXBold } from 'react-icons/pi'; import type { S } from 'services/api/types'; @@ -22,9 +23,10 @@ const QueueItemComponent = ({ queueItem }: Props) => { const { session_id, batch_id, item_id, origin, destination } = queueItem; const { t } = useTranslation(); const isRetryEnabled = useFeatureStatus('retryQueueItem'); - const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled: isBatchCanceled } = useCancelBatch(batch_id); - const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item_id); - const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item_id); + const isBatchCanceled = useBatchIsCanceled(batch_id); + const cancelBatch = useCancelBatch(); + const deleteQueueItem = useDeleteQueueItem(); + const retryQueueItem = useRetryQueueItem(); const originText = useOriginText(origin); const destinationText = useDestinationText(destination); @@ -50,6 +52,18 @@ const QueueItemComponent = ({ queueItem }: Props) => { const isFailed = useMemo(() => !!queueItem && ['canceled', 'failed'].includes(queueItem.status), [queueItem]); + const onCancelBatch = useCallback(() => { + cancelBatch.trigger(batch_id); + }, [cancelBatch, batch_id]); + + const onCancelQueueItem = useCallback(() => { + deleteQueueItem.trigger(item_id); + }, [deleteQueueItem, item_id]); + + const onRetryQueueItem = useCallback(() => { + retryQueueItem.trigger(item_id); + }, [retryQueueItem, item_id]); + return ( { {(!isFailed || !isRetryEnabled) && ( )} ); } return ( - ); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useBatchIsCanceled.ts b/invokeai/frontend/web/src/features/queue/hooks/useBatchIsCanceled.ts new file mode 100644 index 0000000000..2fd82c5b3b --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useBatchIsCanceled.ts @@ -0,0 +1,20 @@ +import { useGetBatchStatusQuery } from 'services/api/endpoints/queue'; + +export const useBatchIsCanceled = (batch_id: string) => { + const { isCanceled } = useGetBatchStatusQuery( + { batch_id }, + { + selectFromResult: ({ data }) => { + if (!data) { + return { isCanceled: true }; + } + + return { + isCanceled: data?.in_progress === 0 && data?.pending === 0, + }; + }, + } + ); + + return isCanceled; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts index 3d38891545..8e6c79b96a 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useCancelAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; @@ -9,17 +9,17 @@ export const useCancelAllExceptCurrentQueueItem = () => { const { t } = useTranslation(); const { data: queueStatus } = useGetQueueStatusQuery(); const isConnected = useStore($isConnected); - const [trigger, { isLoading }] = useCancelAllExceptCurrentMutation({ + const [_trigger, { isLoading }] = useCancelAllExceptCurrentMutation({ fixedCacheKey: 'cancelAllExceptCurrent', }); - const cancelAllExceptCurrentQueueItem = useCallback(async () => { + const trigger = useCallback(async () => { if (!queueStatus?.queue.pending) { return; } try { - await trigger().unwrap(); + await _trigger().unwrap(); toast({ id: 'QUEUE_CANCEL_SUCCEEDED', title: t('queue.cancelSucceeded'), @@ -32,17 +32,7 @@ export const useCancelAllExceptCurrentQueueItem = () => { status: 'error', }); } - }, [queueStatus?.queue.pending, trigger, t]); + }, [queueStatus?.queue.pending, _trigger, t]); - const isDisabled = useMemo( - () => !isConnected || !queueStatus?.queue.pending, - [isConnected, queueStatus?.queue.pending] - ); - - return { - cancelAllExceptCurrentQueueItem, - isLoading, - queueStatus, - isDisabled, - }; + return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.pending } as const; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts index 92d0cbb5a6..ce9202f3cf 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts @@ -2,48 +2,34 @@ import { useStore } from '@nanostores/react'; import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCancelByBatchIdsMutation, useGetBatchStatusQuery } from 'services/api/endpoints/queue'; +import { useCancelByBatchIdsMutation } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; -export const useCancelBatch = (batch_id: string) => { +export const useCancelBatch = () => { const isConnected = useStore($isConnected); - const { isCanceled } = useGetBatchStatusQuery( - { batch_id }, - { - selectFromResult: ({ data }) => { - if (!data) { - return { isCanceled: true }; - } - - return { - isCanceled: data?.in_progress === 0 && data?.pending === 0, - }; - }, - } - ); - const [trigger, { isLoading }] = useCancelByBatchIdsMutation({ + const [_trigger, { isLoading }] = useCancelByBatchIdsMutation({ fixedCacheKey: 'cancelByBatchIds', }); const { t } = useTranslation(); - const cancelBatch = useCallback(async () => { - if (isCanceled) { - return; - } - try { - await trigger({ batch_ids: [batch_id] }).unwrap(); - toast({ - id: 'CANCEL_BATCH_SUCCEEDED', - title: t('queue.cancelBatchSucceeded'), - status: 'success', - }); - } catch { - toast({ - id: 'CANCEL_BATCH_FAILED', - title: t('queue.cancelBatchFailed'), - status: 'error', - }); - } - }, [batch_id, isCanceled, t, trigger]); + const trigger = useCallback( + async (batch_id: string) => { + try { + await _trigger({ batch_ids: [batch_id] }).unwrap(); + toast({ + id: 'CANCEL_BATCH_SUCCEEDED', + title: t('queue.cancelBatchSucceeded'), + status: 'success', + }); + } catch { + toast({ + id: 'CANCEL_BATCH_FAILED', + title: t('queue.cancelBatchFailed'), + status: 'error', + }); + } + }, + [t, _trigger] + ); - return { cancelBatch, isLoading, isCanceled, isDisabled: !isConnected }; + return { trigger, isLoading, isDisabled: !isConnected }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts index 0f9a5c48fe..509efd8a75 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts @@ -1,43 +1,20 @@ -import { useStore } from '@nanostores/react'; -import { toast } from 'features/toast/toast'; -import { isNil } from 'lodash-es'; -import { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useCancelQueueItemMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue'; -import { $isConnected } from 'services/events/stores'; +import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem'; +import { useCurrentQueueItemId } from 'features/queue/hooks/useCurrentQueueItemId'; +import { useCallback } from 'react'; export const useCancelCurrentQueueItem = () => { - const isConnected = useStore($isConnected); - const { data: queueStatus } = useGetQueueStatusQuery(); - const [trigger, { isLoading }] = useCancelQueueItemMutation(); - const { t } = useTranslation(); - const currentQueueItemId = useMemo(() => queueStatus?.queue.item_id, [queueStatus?.queue.item_id]); - const cancelQueueItem = useCallback(async () => { - if (currentQueueItemId !== null || currentQueueItemId !== undefined) { + const currentQueueItemId = useCurrentQueueItemId(); + const cancelQueueItem = useCancelQueueItem(); + const trigger = useCallback(() => { + if (currentQueueItemId === null) { return; } - try { - await trigger({ item_id: currentQueueItemId }).unwrap(); - toast({ - id: 'QUEUE_CANCEL_SUCCEEDED', - title: t('queue.cancelSucceeded'), - status: 'success', - }); - } catch { - toast({ - id: 'QUEUE_CANCEL_FAILED', - title: t('queue.cancelFailed'), - status: 'error', - }); - } - }, [currentQueueItemId, t, trigger]); - - const isDisabled = useMemo(() => !isConnected || isNil(currentQueueItemId), [isConnected, currentQueueItemId]); + cancelQueueItem.trigger(currentQueueItemId); + }, [currentQueueItemId, cancelQueueItem]); return { - cancelQueueItem, - isLoading, - currentQueueItemId, - isDisabled, + trigger, + isLoading: cancelQueueItem.isLoading, + isDisabled: cancelQueueItem.isDisabled || currentQueueItemId === null, }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts index 1e59f8ec46..15de37e030 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts @@ -5,26 +5,29 @@ import { useTranslation } from 'react-i18next'; import { useCancelQueueItemMutation } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; -export const useCancelQueueItem = (item_id: number) => { +export const useCancelQueueItem = () => { const isConnected = useStore($isConnected); - const [trigger, { isLoading }] = useCancelQueueItemMutation(); + const [_trigger, { isLoading }] = useCancelQueueItemMutation(); const { t } = useTranslation(); - const cancelQueueItem = useCallback(async () => { - try { - await trigger({ item_id }).unwrap(); - toast({ - id: 'QUEUE_CANCEL_SUCCEEDED', - title: t('queue.cancelSucceeded'), - status: 'success', - }); - } catch { - toast({ - id: 'QUEUE_CANCEL_FAILED', - title: t('queue.cancelFailed'), - status: 'error', - }); - } - }, [item_id, t, trigger]); + const trigger = useCallback( + async (item_id: number) => { + try { + await _trigger({ item_id }).unwrap(); + toast({ + id: 'QUEUE_CANCEL_SUCCEEDED', + title: t('queue.cancelSucceeded'), + status: 'success', + }); + } catch { + toast({ + id: 'QUEUE_CANCEL_FAILED', + title: t('queue.cancelFailed'), + status: 'error', + }); + } + }, + [t, _trigger] + ); - return { cancelQueueItem, isLoading, isDisabled: !isConnected }; + return { trigger, isLoading, isDisabled: !isConnected }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts new file mode 100644 index 0000000000..0d85a933c7 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItemsByDestination.ts @@ -0,0 +1,33 @@ +import { useStore } from '@nanostores/react'; +import { toast } from 'features/toast/toast'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCancelQueueItemsByDestinationMutation } from 'services/api/endpoints/queue'; +import { $isConnected } from 'services/events/stores'; + +export const useCancelQueueItemsByDestination = () => { + const isConnected = useStore($isConnected); + const [_trigger, { isLoading }] = useCancelQueueItemsByDestinationMutation(); + const { t } = useTranslation(); + const trigger = useCallback( + async (destination: string) => { + try { + await _trigger({ destination }).unwrap(); + toast({ + id: 'QUEUE_CANCEL_SUCCEEDED', + title: t('queue.cancelSucceeded'), + status: 'success', + }); + } catch { + toast({ + id: 'QUEUE_CANCEL_FAILED', + title: t('queue.cancelFailed'), + status: 'error', + }); + } + }, + [t, _trigger] + ); + + return { trigger, isLoading, isDisabled: !isConnected }; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts index 19ab3cf45f..90c5a0dd3a 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useClearInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo'; import { $isConnected } from 'services/events/stores'; @@ -9,19 +9,13 @@ export const useClearInvocationCache = () => { const { t } = useTranslation(); const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); const isConnected = useStore($isConnected); - const [trigger, { isLoading }] = useClearInvocationCacheMutation({ + const [_trigger, { isLoading }] = useClearInvocationCacheMutation({ fixedCacheKey: 'clearInvocationCache', }); - const isDisabled = useMemo(() => !cacheStatus?.size || !isConnected, [cacheStatus?.size, isConnected]); - - const clearInvocationCache = useCallback(async () => { - if (isDisabled) { - return; - } - + const trigger = useCallback(async () => { try { - await trigger().unwrap(); + await _trigger().unwrap(); toast({ id: 'INVOCATION_CACHE_CLEAR_SUCCEEDED', title: t('invocationCache.clearSucceeded'), @@ -34,7 +28,7 @@ export const useClearInvocationCache = () => { status: 'error', }); } - }, [isDisabled, trigger, t]); + }, [_trigger, t]); - return { clearInvocationCache, isLoading, cacheStatus, isDisabled }; + return { trigger, isLoading, isDisabled: !isConnected || !cacheStatus?.size }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts index 5d5f7713f2..9c24448ed4 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; @@ -12,17 +12,17 @@ export const useClearQueue = () => { const dispatch = useAppDispatch(); const { data: queueStatus } = useGetQueueStatusQuery(); const isConnected = useStore($isConnected); - const [trigger, { isLoading }] = useClearQueueMutation({ + const [_trigger, { isLoading }] = useClearQueueMutation({ fixedCacheKey: 'clearQueue', }); - const clearQueue = useCallback(async () => { + const trigger = useCallback(async () => { if (!queueStatus?.queue.total) { return; } try { - await trigger().unwrap(); + await _trigger().unwrap(); toast({ id: 'QUEUE_CLEAR_SUCCEEDED', title: t('queue.clearSucceeded'), @@ -37,14 +37,7 @@ export const useClearQueue = () => { status: 'error', }); } - }, [queueStatus?.queue.total, trigger, dispatch, t]); + }, [queueStatus?.queue.total, _trigger, dispatch, t]); - const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]); - - return { - clearQueue, - isLoading, - queueStatus, - isDisabled, - }; + return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.total }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCurrentDestination.ts b/invokeai/frontend/web/src/features/queue/hooks/useCurrentDestination.ts deleted file mode 100644 index 773d966634..0000000000 --- a/invokeai/frontend/web/src/features/queue/hooks/useCurrentDestination.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useGetCurrentQueueItemQuery } from 'services/api/endpoints/queue'; - -export const useCurrentDestination = () => { - const { destination } = useGetCurrentQueueItemQuery(undefined, { - selectFromResult: ({ data }) => ({ - destination: data ? data.destination : null, - }), - }); - - return destination; -}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts b/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts new file mode 100644 index 0000000000..daa3a82704 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useCurrentQueueItemId.ts @@ -0,0 +1,11 @@ +import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; + +export const useCurrentQueueItemId = () => { + const { currentQueueItemId } = useGetQueueStatusQuery(undefined, { + selectFromResult: ({ data }) => ({ + currentQueueItemId: data?.queue.item_id ?? null, + }), + }); + + return currentQueueItemId; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts new file mode 100644 index 0000000000..1f34a76d24 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteAllExceptCurrentQueueItem.ts @@ -0,0 +1,38 @@ +import { useStore } from '@nanostores/react'; +import { toast } from 'features/toast/toast'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDeleteAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue'; +import { $isConnected } from 'services/events/stores'; + +export const useDeleteAllExceptCurrentQueueItem = () => { + const { t } = useTranslation(); + const { data: queueStatus } = useGetQueueStatusQuery(); + const isConnected = useStore($isConnected); + const [_trigger, { isLoading }] = useDeleteAllExceptCurrentMutation({ + fixedCacheKey: 'deleteAllExceptCurrent', + }); + + const trigger = useCallback(async () => { + if (!queueStatus?.queue.pending) { + return; + } + + try { + await _trigger().unwrap(); + toast({ + id: 'QUEUE_CANCEL_SUCCEEDED', + title: t('queue.cancelSucceeded'), + status: 'success', + }); + } catch { + toast({ + id: 'QUEUE_CANCEL_FAILED', + title: t('queue.cancelFailed'), + status: 'error', + }); + } + }, [queueStatus?.queue.pending, _trigger, t]); + + return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.pending } as const; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteCurrentQueueItem.ts new file mode 100644 index 0000000000..d119311235 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteCurrentQueueItem.ts @@ -0,0 +1,20 @@ +import { useCurrentQueueItemId } from 'features/queue/hooks/useCurrentQueueItemId'; +import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem'; +import { useCallback } from 'react'; + +export const useDeleteCurrentQueueItem = () => { + const currentQueueItemId = useCurrentQueueItemId(); + const deleteQueueItem = useDeleteQueueItem(); + const trigger = useCallback(() => { + if (currentQueueItemId === null) { + return; + } + deleteQueueItem.trigger(currentQueueItemId); + }, [currentQueueItemId, deleteQueueItem]); + + return { + trigger, + isLoading: deleteQueueItem.isLoading, + isDisabled: deleteQueueItem.isDisabled || currentQueueItemId === null, + }; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts new file mode 100644 index 0000000000..8e37d376b4 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItem.ts @@ -0,0 +1,33 @@ +import { useStore } from '@nanostores/react'; +import { toast } from 'features/toast/toast'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue'; +import { $isConnected } from 'services/events/stores'; + +export const useDeleteQueueItem = () => { + const isConnected = useStore($isConnected); + const [_trigger, { isLoading }] = useDeleteQueueItemMutation(); + const { t } = useTranslation(); + const trigger = useCallback( + async (item_id: number) => { + try { + await _trigger({ item_id }).unwrap(); + toast({ + id: 'QUEUE_CANCEL_SUCCEEDED', + title: t('queue.cancelSucceeded'), + status: 'success', + }); + } catch { + toast({ + id: 'QUEUE_CANCEL_FAILED', + title: t('queue.cancelFailed'), + status: 'error', + }); + } + }, + [t, _trigger] + ); + + return { trigger, isLoading, isDisabled: !isConnected }; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItemsByDestination.ts b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItemsByDestination.ts new file mode 100644 index 0000000000..376063cb44 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useDeleteQueueItemsByDestination.ts @@ -0,0 +1,33 @@ +import { useStore } from '@nanostores/react'; +import { toast } from 'features/toast/toast'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue'; +import { $isConnected } from 'services/events/stores'; + +export const useDeleteQueueItemsByDestination = () => { + const isConnected = useStore($isConnected); + const [_trigger, { isLoading }] = useDeleteQueueItemsByDestinationMutation(); + const { t } = useTranslation(); + const trigger = useCallback( + async (destination: string) => { + try { + await _trigger({ destination }).unwrap(); + toast({ + id: 'QUEUE_CANCEL_SUCCEEDED', + title: t('queue.cancelSucceeded'), + status: 'success', + }); + } catch { + toast({ + id: 'QUEUE_CANCEL_FAILED', + title: t('queue.cancelFailed'), + status: 'error', + }); + } + }, + [t, _trigger] + ); + + return { trigger, isLoading, isDisabled: !isConnected }; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts index 703cf8d4cb..17ac6bec44 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useDisableInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo'; import { $isConnected } from 'services/events/stores'; @@ -9,22 +9,13 @@ export const useDisableInvocationCache = () => { const { t } = useTranslation(); const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); const isConnected = useStore($isConnected); - const [trigger, { isLoading }] = useDisableInvocationCacheMutation({ + const [_trigger, { isLoading }] = useDisableInvocationCacheMutation({ fixedCacheKey: 'disableInvocationCache', }); - const isDisabled = useMemo( - () => !cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0, - [cacheStatus?.enabled, cacheStatus?.max_size, isConnected] - ); - - const disableInvocationCache = useCallback(async () => { - if (isDisabled) { - return; - } - + const trigger = useCallback(async () => { try { - await trigger().unwrap(); + await _trigger().unwrap(); toast({ id: 'INVOCATION_CACHE_DISABLE_SUCCEEDED', title: t('invocationCache.disableSucceeded'), @@ -37,7 +28,12 @@ export const useDisableInvocationCache = () => { status: 'error', }); } - }, [isDisabled, trigger, t]); + }, [_trigger, t]); - return { disableInvocationCache, isLoading, cacheStatus, isDisabled }; + return { + trigger, + isLoading, + cacheStatus, + isDisabled: !cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0, + }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts index 2589d50717..02fcf444fa 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useEnableInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo'; import { $isConnected } from 'services/events/stores'; @@ -9,22 +9,13 @@ export const useEnableInvocationCache = () => { const { t } = useTranslation(); const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); const isConnected = useStore($isConnected); - const [trigger, { isLoading }] = useEnableInvocationCacheMutation({ + const [_trigger, { isLoading }] = useEnableInvocationCacheMutation({ fixedCacheKey: 'enableInvocationCache', }); - const isDisabled = useMemo( - () => cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0, - [cacheStatus?.enabled, cacheStatus?.max_size, isConnected] - ); - - const enableInvocationCache = useCallback(async () => { - if (isDisabled) { - return; - } - + const trigger = useCallback(async () => { try { - await trigger().unwrap(); + await _trigger().unwrap(); toast({ id: 'INVOCATION_CACHE_ENABLE_SUCCEEDED', title: t('invocationCache.enableSucceeded'), @@ -37,7 +28,11 @@ export const useEnableInvocationCache = () => { status: 'error', }); } - }, [isDisabled, trigger, t]); + }, [_trigger, t]); - return { enableInvocationCache, isLoading, cacheStatus, isDisabled }; + return { + trigger, + isLoading, + isDisabled: cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0, + }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts index d4712ad2b8..9e82576a4f 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery, usePauseProcessorMutation } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; @@ -9,18 +9,13 @@ export const usePauseProcessor = () => { const { t } = useTranslation(); const isConnected = useStore($isConnected); const { data: queueStatus } = useGetQueueStatusQuery(); - const [trigger, { isLoading }] = usePauseProcessorMutation({ + const [_trigger, { isLoading }] = usePauseProcessorMutation({ fixedCacheKey: 'pauseProcessor', }); - const isStarted = useMemo(() => Boolean(queueStatus?.processor.is_started), [queueStatus?.processor.is_started]); - - const pauseProcessor = useCallback(async () => { - if (!isStarted) { - return; - } + const trigger = useCallback(async () => { try { - await trigger().unwrap(); + await _trigger().unwrap(); toast({ id: 'PAUSE_SUCCEEDED', title: t('queue.pauseSucceeded'), @@ -33,9 +28,7 @@ export const usePauseProcessor = () => { status: 'error', }); } - }, [isStarted, trigger, t]); + }, [_trigger, t]); - const isDisabled = useMemo(() => !isConnected || !isStarted, [isConnected, isStarted]); - - return { pauseProcessor, isLoading, isStarted, isDisabled }; + return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts b/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts index 09e77e23d6..c186db96df 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery, usePruneQueueMutation } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; @@ -11,27 +11,14 @@ export const usePruneQueue = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const isConnected = useStore($isConnected); - const [trigger, { isLoading }] = usePruneQueueMutation({ + const finishedCount = useFinishedCount(); + const [_trigger, { isLoading }] = usePruneQueueMutation({ fixedCacheKey: 'pruneQueue', }); - const { finishedCount } = useGetQueueStatusQuery(undefined, { - selectFromResult: ({ data }) => { - if (!data) { - return { finishedCount: 0 }; - } - return { - finishedCount: data.queue.completed + data.queue.canceled + data.queue.failed, - }; - }, - }); - - const pruneQueue = useCallback(async () => { - if (!finishedCount) { - return; - } + const trigger = useCallback(async () => { try { - const data = await trigger().unwrap(); + const data = await _trigger().unwrap(); toast({ id: 'PRUNE_SUCCEEDED', title: t('queue.pruneSucceeded', { item_count: data.deleted }), @@ -46,9 +33,23 @@ export const usePruneQueue = () => { status: 'error', }); } - }, [finishedCount, trigger, dispatch, t]); + }, [_trigger, dispatch, t]); - const isDisabled = useMemo(() => !isConnected || !finishedCount, [finishedCount, isConnected]); - - return { pruneQueue, isLoading, finishedCount, isDisabled }; + return { trigger, isLoading, isDisabled: !isConnected || !finishedCount }; +}; + +export const useFinishedCount = () => { + const { finishedCount } = useGetQueueStatusQuery(undefined, { + selectFromResult: ({ data }) => { + if (!data) { + return { finishedCount: 0 }; + } + + return { + finishedCount: data.queue.completed + data.queue.canceled + data.queue.failed, + }; + }, + }); + + return finishedCount; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts index 058a3b2b3e..baa02ece03 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery, useResumeProcessorMutation } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; @@ -9,18 +9,13 @@ export const useResumeProcessor = () => { const isConnected = useStore($isConnected); const { data: queueStatus } = useGetQueueStatusQuery(); const { t } = useTranslation(); - const [trigger, { isLoading }] = useResumeProcessorMutation({ + const [_trigger, { isLoading }] = useResumeProcessorMutation({ fixedCacheKey: 'resumeProcessor', }); - const isStarted = useMemo(() => Boolean(queueStatus?.processor.is_started), [queueStatus?.processor.is_started]); - - const resumeProcessor = useCallback(async () => { - if (isStarted) { - return; - } + const trigger = useCallback(async () => { try { - await trigger().unwrap(); + await _trigger().unwrap(); toast({ id: 'PROCESSOR_RESUMED', title: t('queue.resumeSucceeded'), @@ -33,9 +28,7 @@ export const useResumeProcessor = () => { status: 'error', }); } - }, [isStarted, trigger, t]); + }, [_trigger, t]); - const isDisabled = useMemo(() => !isConnected || isStarted, [isConnected, isStarted]); - - return { resumeProcessor, isLoading, isStarted, isDisabled }; + return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useRetryQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useRetryQueueItem.ts index cf3442d448..580f88f9ad 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useRetryQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useRetryQueueItem.ts @@ -5,29 +5,32 @@ import { useTranslation } from 'react-i18next'; import { useRetryItemsByIdMutation } from 'services/api/endpoints/queue'; import { $isConnected } from 'services/events/stores'; -export const useRetryQueueItem = (item_id: number) => { +export const useRetryQueueItem = () => { const isConnected = useStore($isConnected); - const [trigger, { isLoading }] = useRetryItemsByIdMutation(); + const [_trigger, { isLoading }] = useRetryItemsByIdMutation(); const { t } = useTranslation(); - const retryQueueItem = useCallback(async () => { - try { - const result = await trigger([item_id]).unwrap(); - if (!result.retried_item_ids.includes(item_id)) { - throw new Error('Failed to retry item'); + const trigger = useCallback( + async (item_id: number) => { + try { + const result = await _trigger([item_id]).unwrap(); + if (!result.retried_item_ids.includes(item_id)) { + throw new Error('Failed to retry item'); + } + toast({ + id: 'QUEUE_RETRY_SUCCEEDED', + title: t('queue.retrySucceeded'), + status: 'success', + }); + } catch { + toast({ + id: 'QUEUE_RETRY_FAILED', + title: t('queue.retryFailed'), + status: 'error', + }); } - toast({ - id: 'QUEUE_RETRY_SUCCEEDED', - title: t('queue.retrySucceeded'), - status: 'success', - }); - } catch { - toast({ - id: 'QUEUE_RETRY_FAILED', - title: t('queue.retryFailed'), - status: 'error', - }); - } - }, [item_id, t, trigger]); + }, + [t, _trigger] + ); - return { retryQueueItem, isLoading, isDisabled: !isConnected }; + return { trigger, isLoading, isDisabled: !isConnected }; }; diff --git a/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx b/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx index 5b688097f3..218ca382b8 100644 --- a/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx +++ b/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx @@ -1,6 +1,5 @@ import { Progress } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { useCurrentDestination } from 'features/queue/hooks/useCurrentDestination'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; @@ -8,7 +7,6 @@ import { $isConnected, $lastProgressEvent } from 'services/events/stores'; const ProgressBar = () => { const { t } = useTranslation(); - const destination = useCurrentDestination(); const { data: queueStatus } = useGetQueueStatusQuery(); const isConnected = useStore($isConnected); const lastProgressEvent = useStore($lastProgressEvent); @@ -39,16 +37,6 @@ const ProgressBar = () => { return false; }, [isConnected, lastProgressEvent, queueStatus?.queue.in_progress]); - const colorScheme = useMemo(() => { - if (destination === 'canvas') { - return 'invokeGreen'; - } else if (destination === 'gallery') { - return 'invokeBlue'; - } else { - return 'base'; - } - }, [destination]); - return ( { isIndeterminate={isIndeterminate} h={2} w="full" - colorScheme={colorScheme} + colorScheme="invokeBlue" /> ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx index 973fb1de87..8630476015 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx @@ -3,9 +3,9 @@ import { useAppSelector } from 'app/store/storeHooks'; import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; +import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog'; import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip'; -import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; +import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem'; import { useInvoke } from 'features/queue/hooks/useInvoke'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; @@ -34,8 +34,8 @@ export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) = - - + + ); @@ -103,18 +103,18 @@ const InvokeIconButtonIcon = memo(() => { }); InvokeIconButtonIcon.displayName = 'InvokeIconButtonIcon'; -const CancelCurrentIconButton = memo(() => { +const DeleteCurrentIconButton = memo(() => { const { t } = useTranslation(); - const cancelCurrentQueueItem = useCancelCurrentQueueItem(); + const deleteCurrentQueueItem = useDeleteCurrentQueueItem(); return ( } - onClick={cancelCurrentQueueItem.cancelQueueItem} colorScheme="error" flexGrow={1} /> @@ -122,25 +122,25 @@ const CancelCurrentIconButton = memo(() => { ); }); -CancelCurrentIconButton.displayName = 'CancelCurrentIconButton'; +DeleteCurrentIconButton.displayName = 'DeleteCurrentIconButton'; -const CancelAllExceptCurrentIconButton = memo(() => { +const DeleteAllExceptCurrentIconButton = memo(() => { const { t } = useTranslation(); - const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog(); + const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog(); return ( } colorScheme="error" - onClick={cancelAllExceptCurrent.openDialog} + onClick={deleteAllExceptCurrent.openDialog} flexGrow={1} /> ); }); -CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton'; +DeleteAllExceptCurrentIconButton.displayName = 'DeleteAllExceptCurrentIconButton'; diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts index 2239a5df3a..75ff92e58d 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts @@ -224,7 +224,7 @@ export const queueApi = api.injectEndpoints({ ]; }, }), - cancelByDestination: build.mutation< + cancelQueueItemsByDestination: build.mutation< paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['responses']['200']['content']['application/json'], paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['parameters']['query'] >({ @@ -256,6 +256,16 @@ export const queueApi = api.injectEndpoints({ }), invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination', 'SessionQueueItem'], }), + deleteAllExceptCurrent: build.mutation< + paths['/api/v1/queue/{queue_id}/delete_all_except_current']['put']['responses']['200']['content']['application/json'], + void + >({ + query: () => ({ + url: buildQueueUrl('delete_all_except_current'), + method: 'PUT', + }), + invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination', 'SessionQueueItem'], + }), retryItemsById: build.mutation< paths['/api/v1/queue/{queue_id}/retry_items_by_id']['put']['responses']['200']['content']['application/json'], paths['/api/v1/queue/{queue_id}/retry_items_by_id']['put']['requestBody']['content']['application/json'] @@ -329,7 +339,11 @@ export const queueApi = api.injectEndpoints({ url: buildQueueUrl(`i/${item_id}`), method: 'DELETE', }), - invalidatesTags: (result, error, { item_id }) => [{ type: 'SessionQueueItem', id: item_id }], + invalidatesTags: (result, error, { item_id }) => [ + { type: 'SessionQueueItem', id: item_id }, + { type: 'SessionQueueItem', id: LIST_TAG }, + { type: 'SessionQueueItem', id: LIST_ALL_TAG }, + ], }), deleteQueueItemsByDestination: build.mutation({ query: ({ destination }) => ({ @@ -366,8 +380,10 @@ export const { useGetQueueStatusQuery, useListQueueItemsQuery, useCancelQueueItemMutation, + useCancelQueueItemsByDestinationMutation, useDeleteQueueItemMutation, useDeleteQueueItemsByDestinationMutation, + useDeleteAllExceptCurrentMutation, useGetBatchStatusQuery, useGetCurrentQueueItemQuery, useGetQueueCountsByDestinationQuery, diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index e167ba36fd..20a5903edb 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -1244,6 +1244,26 @@ export type paths = { patch?: never; trace?: never; }; + "/api/v1/queue/{queue_id}/delete_all_except_current": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Delete All Except Current + * @description Immediately deletes all queue items except in-processing items + */ + put: operations["delete_all_except_current"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/queue/{queue_id}/cancel_by_batch_ids": { parameters: { query?: never; @@ -5885,6 +5905,17 @@ export type components = { */ type: "dw_openpose_detection"; }; + /** + * DeleteAllExceptCurrentResult + * @description Result of deleting all except current + */ + DeleteAllExceptCurrentResult: { + /** + * Deleted + * @description Number of queue items deleted + */ + deleted: number; + }; /** DeleteBoardResult */ DeleteBoardResult: { /** @@ -24570,6 +24601,38 @@ export interface operations { }; }; }; + delete_all_except_current: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The queue id to perform this operation on */ + queue_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteAllExceptCurrentResult"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; cancel_by_batch_ids: { parameters: { query?: never;