diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 10197fef1ff..96bd9bce291 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -209,9 +209,13 @@ "pauseSucceeded": "Processor Paused", "pauseFailed": "Problem Pausing Processor", "cancel": "Cancel", + "cancelAllExceptCurrentQueueItemAlertDialog": "Canceling all queue items except the current one will stop pending items but allow the in-progress one to finish.", + "cancelAllExceptCurrentQueueItemAlertDialog2": "Are you sure you want to cancel all pending queue items?", + "cancelAllExceptCurrentTooltip": "Cancel All Except Current Item", "cancelTooltip": "Cancel Current Item", "cancelSucceeded": "Item Canceled", "cancelFailed": "Problem Canceling Item", + "confirm": "Confirm", "prune": "Prune", "pruneTooltip": "Prune {{item_count}} Completed Items", "pruneSucceeded": "Pruned {{item_count}} Completed Items from Queue", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 2902c344adb..7bef8d59a6d 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -23,6 +23,7 @@ import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModa import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal'; +import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog'; import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal'; @@ -97,6 +98,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => { + diff --git a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx new file mode 100644 index 00000000000..cdaaebcdf0e --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx @@ -0,0 +1,47 @@ +import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { buildUseBoolean } from 'common/hooks/useBoolean'; +import { useCancelAllExceptCurrentQueueItem } from 'features/queue/hooks/useCancelAllExceptCurrentQueueItem'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const [useCancelAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBoolean(false); + +export const useCancelAllExceptCurrentQueueItemDialog = () => { + const dialog = useCancelAllExceptCurrentQueueItemConfirmationAlertDialog(); + const { cancelAllExceptCurrentQueueItem, isLoading, isDisabled, queueStatus } = useCancelAllExceptCurrentQueueItem(); + + return { + cancelAllExceptCurrentQueueItem, + isOpen: dialog.isTrue, + openDialog: dialog.setTrue, + closeDialog: dialog.setFalse, + isLoading, + queueStatus, + isDisabled, + }; +}; + +export const CancelAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() => { + useAssertSingleton('CancelAllExceptCurrentQueueItemConfirmationAlertDialog'); + const { t } = useTranslation(); + const cancelAllExceptCurrentQueueItem = useCancelAllExceptCurrentQueueItemDialog(); + + return ( + + {t('queue.cancelAllExceptCurrentQueueItemAlertDialog')} +
+ {t('queue.cancelAllExceptCurrentQueueItemAlertDialog2')} +
+ ); +}); + +CancelAllExceptCurrentQueueItemConfirmationAlertDialog.displayName = + 'CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx index 27672cec839..2a9f89b0f33 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx @@ -1,6 +1,7 @@ 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 { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { QueueCountBadge } from 'features/queue/components/QueueCountBadge'; import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; @@ -10,7 +11,15 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiTrashSimpleBold, PiXBold } from 'react-icons/pi'; +import { + PiListBold, + PiPauseFill, + PiPlayFill, + PiQueueBold, + PiTrashSimpleBold, + PiXBold, + PiXCircle, +} from 'react-icons/pi'; export const QueueActionsMenuButton = memo(() => { const ref = useRef(null); @@ -18,6 +27,7 @@ export const QueueActionsMenuButton = memo(() => { const { t } = useTranslation(); const isPauseEnabled = useFeatureStatus('pauseQueue'); const isResumeEnabled = useFeatureStatus('resumeQueue'); + const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog(); const cancelCurrent = useCancelCurrentQueueItem(); const clearQueue = useClearQueueDialog(); const { @@ -52,6 +62,15 @@ export const QueueActionsMenuButton = memo(() => { > {t('queue.cancelTooltip')} + } + onClick={cancelAllExceptCurrent.openDialog} + isLoading={cancelAllExceptCurrent.isLoading} + isDisabled={cancelAllExceptCurrent.isDisabled} + > + {t('queue.cancelAllExceptCurrentTooltip')} + } diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts new file mode 100644 index 00000000000..3d388915457 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts @@ -0,0 +1,48 @@ +import { useStore } from '@nanostores/react'; +import { toast } from 'features/toast/toast'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCancelAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue'; +import { $isConnected } from 'services/events/stores'; + +export const useCancelAllExceptCurrentQueueItem = () => { + const { t } = useTranslation(); + const { data: queueStatus } = useGetQueueStatusQuery(); + const isConnected = useStore($isConnected); + const [trigger, { isLoading }] = useCancelAllExceptCurrentMutation({ + fixedCacheKey: 'cancelAllExceptCurrent', + }); + + const cancelAllExceptCurrentQueueItem = 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]); + + const isDisabled = useMemo( + () => !isConnected || !queueStatus?.queue.pending, + [isConnected, queueStatus?.queue.pending] + ); + + return { + cancelAllExceptCurrentQueueItem, + isLoading, + queueStatus, + isDisabled, + }; +}; diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts index c2af0d8aa9e..05e5862ee74 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts @@ -348,6 +348,25 @@ export const queueApi = api.injectEndpoints({ return ['SessionQueueStatus', 'BatchStatus', { type: 'QueueCountsByDestination', id: destination }]; }, }), + cancelAllExceptCurrent: build.mutation< + paths['/api/v1/queue/{queue_id}/cancel_all_except_current']['put']['responses']['200']['content']['application/json'], + void + >({ + query: () => ({ + url: buildQueueUrl('cancel_all_except_current'), + method: 'PUT', + }), + onQueryStarted: async (arg, api) => { + const { dispatch, queryFulfilled } = api; + try { + await queryFulfilled; + resetListQueryData(dispatch); + } catch { + // no-op + } + }, + invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination'], + }), listQueueItems: build.query< EntityState & { has_more: boolean; @@ -390,6 +409,7 @@ export const queueApi = api.injectEndpoints({ }); export const { + useCancelAllExceptCurrentMutation, useCancelByBatchIdsMutation, useEnqueueBatchMutation, usePauseProcessorMutation,