From b09008c530d5499ffd16aeba02151bf5ba38a232 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 17 Mar 2025 13:43:12 +1000
Subject: [PATCH] feat(ui): add cancel and clear all as toggleable app feature
---
.../frontend/web/src/app/types/invokeai.ts | 3 +-
.../CancelAllExceptCurrentButton.tsx | 32 ++++
.../queue/components/ClearQueueIconButton.tsx | 88 +++++++--
.../components/QueueActionsMenuButton.tsx | 21 ++-
.../components/QueueTabQueueControls.tsx | 6 +-
.../src/features/ui/components/AppContent.tsx | 2 +-
.../FloatingParametersPanelButtons.tsx | 175 ++++++++++++------
7 files changed, 238 insertions(+), 89 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentButton.tsx
diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts
index 237d213ce5..1a540d6743 100644
--- a/invokeai/frontend/web/src/app/types/invokeai.ts
+++ b/invokeai/frontend/web/src/app/types/invokeai.ts
@@ -27,7 +27,8 @@ export type AppFeature =
| 'bulkDownload'
| 'starterModels'
| 'hfToken'
- | 'retryQueueItem';
+ | 'retryQueueItem'
+ | 'cancelAndClearAll';
/**
* A disable-able Stable Diffusion feature
*/
diff --git a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentButton.tsx b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentButton.tsx
new file mode 100644
index 0000000000..25b518296b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentButton.tsx
@@ -0,0 +1,32 @@
+import type { ButtonProps } from '@invoke-ai/ui-library';
+import { Button } 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';
+
+type Props = ButtonProps;
+
+export const CancelAllExceptCurrentButton = memo((props: Props) => {
+ const { t } = useTranslation();
+ const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
+
+ return (
+ <>
+ }
+ colorScheme="error"
+ data-testid={t('queue.clear')}
+ {...props}
+ >
+ {t('queue.clear')}
+
+ >
+ );
+});
+
+CancelAllExceptCurrentButton.displayName = 'CancelAllExceptCurrentButton';
diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
index 51715d0b67..348dd38ada 100644
--- a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx
@@ -1,33 +1,89 @@
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
+import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
+import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
+import { PiTrashSimpleBold, PiXBold, PiXCircle } from 'react-icons/pi';
import { useClearQueueDialog } from './ClearQueueConfirmationAlertDialog';
-export const ClearQueueIconButton = memo((_) => {
- const { t } = useTranslation();
- const clearQueue = useClearQueueDialog();
- const cancelCurrentQueueItem = useCancelCurrentQueueItem();
-
- // Show the single item clear button when shift is pressed
- // Otherwise show the clear queue button
+export const ClearQueueIconButton = memo(() => {
+ const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
const shift = useShiftModifier();
+ if (!shift) {
+ // Shift is not pressed - show cancel current
+ return ;
+ }
+
+ if (isCancelAndClearAllEnabled) {
+ // Shift is pressed and cancel and clear all is enabled - show cancel and clear all
+ return ;
+ }
+
+ // Shift is pressed and cancel and clear all is disabled - show cancel all except current
+ return ;
+});
+
+ClearQueueIconButton.displayName = 'ClearQueueIconButton';
+
+const CancelCurrentIconButton = memo(() => {
+ const { t } = useTranslation();
+ const cancelCurrentQueueItem = useCancelCurrentQueueItem();
+
return (
: }
+ isDisabled={cancelCurrentQueueItem.isDisabled}
+ isLoading={cancelCurrentQueueItem.isLoading}
+ aria-label={t('queue.cancel')}
+ tooltip={t('queue.cancelTooltip')}
+ icon={}
colorScheme="error"
- onClick={shift ? clearQueue.openDialog : cancelCurrentQueueItem.cancelQueueItem}
- data-testid={shift ? t('queue.clear') : t('queue.cancel')}
+ onClick={cancelCurrentQueueItem.cancelQueueItem}
/>
);
});
-ClearQueueIconButton.displayName = 'ClearQueueIconButton';
+CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
+
+const CancelAndClearAllIconButton = memo(() => {
+ const { t } = useTranslation();
+ const clearQueue = useClearQueueDialog();
+
+ return (
+ }
+ colorScheme="error"
+ onClick={clearQueue.openDialog}
+ />
+ );
+});
+
+CancelAndClearAllIconButton.displayName = 'CancelAndClearAllIconButton';
+
+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/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
index 2a9f89b0f3..42b8d704bd 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
@@ -27,6 +27,7 @@ export const QueueActionsMenuButton = memo(() => {
const { t } = useTranslation();
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
+ const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
const cancelCurrent = useCancelCurrentQueueItem();
const clearQueue = useClearQueueDialog();
@@ -71,15 +72,17 @@ export const QueueActionsMenuButton = memo(() => {
>
{t('queue.cancelAllExceptCurrentTooltip')}
- }
- onClick={clearQueue.openDialog}
- isLoading={clearQueue.isLoading}
- isDisabled={clearQueue.isDisabled}
- >
- {t('queue.clearTooltip')}
-
+ {isCancelAndClearAllEnabled && (
+ }
+ onClick={clearQueue.openDialog}
+ isLoading={clearQueue.isLoading}
+ isDisabled={clearQueue.isDisabled}
+ >
+ {t('queue.clearTooltip')}
+
+ )}
{isResumeEnabled && (
}
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
index 59bd02a737..7e679caf74 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx
@@ -1,5 +1,6 @@
/* eslint-disable i18next/no-literal-string */
import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
+import { CancelAllExceptCurrentButton } from 'features/queue/components/CancelAllExceptCurrentButton';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
@@ -12,6 +13,8 @@ import ResumeProcessorButton from './ResumeProcessorButton';
const QueueTabQueueControls = () => {
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
+ const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
+
return (
@@ -25,7 +28,8 @@ const QueueTabQueueControls = () => {
)}
-
+ {isCancelAndClearAllEnabled && }
+ {!isCancelAndClearAllEnabled && }
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index bbcfdf2ad9..665ac99db0 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -141,7 +141,7 @@ export const AppContent = memo(() => {
)}
- {withLeftPanel && }
+ {withLeftPanel && }
{withRightPanel && }
{withRightPanel && (
diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
index bf09f47c34..f0de83b2af 100644
--- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx
@@ -3,11 +3,12 @@ import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
+import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
-import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
+import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -18,22 +19,53 @@ import {
PiSparkleFill,
PiTrashSimpleBold,
PiXBold,
+ PiXCircle,
} from 'react-icons/pi';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
type Props = {
- panelApi: UsePanelReturn;
+ togglePanel: () => void;
};
-const FloatingSidePanelButtons = (props: Props) => {
+const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
+ const { t } = useTranslation();
+ const tab = useAppSelector(selectActiveTab);
+ const imageViewer = useImageViewer();
+ const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
+
+ return (
+
+ {tab === 'canvas' && !imageViewer.isOpen && (
+
+
+
+ )}
+
+
+ }
+ flexGrow={1}
+ />
+
+
+
+ {/* Show the cancel all except current button instead of cancel and clear all when it is disabled */}
+ {isCancelAndClearAllEnabled && }
+ {!isCancelAndClearAllEnabled && }
+
+
+ );
+};
+
+export default memo(FloatingSidePanelButtons);
+
+const InvokeIconButton = memo(() => {
const { t } = useTranslation();
const queue = useInvoke();
const shift = useShiftModifier();
- const tab = useAppSelector(selectActiveTab);
- const imageViewer = useImageViewer();
- const clearQueue = useClearQueueDialog();
const { data: queueStatus } = useGetQueueStatusQuery();
- const cancelCurrent = useCancelCurrentQueueItem();
const queueButtonIcon = useMemo(() => {
const isProcessing = (queueStatus?.queue.in_progress ?? 0) > 0;
@@ -47,59 +79,80 @@ const FloatingSidePanelButtons = (props: Props) => {
}, [queue.isDisabled, queueStatus?.queue.in_progress, shift]);
return (
-
- {tab === 'canvas' && !imageViewer.isOpen && (
-
-
-
- )}
-
-
- }
- flexGrow={1}
- />
-
-
-
-
-
- }
- onClick={cancelCurrent.cancelQueueItem}
- colorScheme="error"
- flexGrow={1}
- />
-
-
-
- }
- colorScheme="error"
- onClick={clearQueue.openDialog}
- data-testid={t('queue.clear')}
- flexGrow={1}
- />
-
-
-
+
+
+
);
-};
+});
+InvokeIconButton.displayName = 'InvokeIconButton';
-export default memo(FloatingSidePanelButtons);
+const CancelCurrentIconButton = memo(() => {
+ const { t } = useTranslation();
+ const cancelCurrentQueueItem = useCancelCurrentQueueItem();
+
+ return (
+
+ }
+ onClick={cancelCurrentQueueItem.cancelQueueItem}
+ colorScheme="error"
+ flexGrow={1}
+ />
+
+ );
+});
+
+CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
+
+const CancelAndClearAllIconButton = memo(() => {
+ const { t } = useTranslation();
+ const clearQueue = useClearQueueDialog();
+
+ return (
+
+ }
+ colorScheme="error"
+ onClick={clearQueue.openDialog}
+ flexGrow={1}
+ />
+
+ );
+});
+
+CancelAndClearAllIconButton.displayName = 'CancelAndClearAllIconButton';
+
+const CancelAllExceptCurrentIconButton = memo(() => {
+ const { t } = useTranslation();
+ const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
+
+ return (
+
+ }
+ colorScheme="error"
+ onClick={cancelAllExceptCurrent.openDialog}
+ flexGrow={1}
+ />
+
+ );
+});
+
+CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';