feat(ui): add cancel and clear all as toggleable app feature

This commit is contained in:
psychedelicious
2025-03-17 13:43:12 +10:00
parent 7adac4581a
commit b09008c530
7 changed files with 238 additions and 89 deletions

View File

@@ -27,7 +27,8 @@ export type AppFeature =
| 'bulkDownload'
| 'starterModels'
| 'hfToken'
| 'retryQueueItem';
| 'retryQueueItem'
| 'cancelAndClearAll';
/**
* A disable-able Stable Diffusion feature
*/

View File

@@ -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 (
<>
<Button
onClick={cancelAllExceptCurrent.openDialog}
isLoading={cancelAllExceptCurrent.isLoading}
isDisabled={cancelAllExceptCurrent.isDisabled}
tooltip={t('queue.cancelAllExceptCurrentTooltip')}
leftIcon={<PiXCircle />}
colorScheme="error"
data-testid={t('queue.clear')}
{...props}
>
{t('queue.clear')}
</Button>
</>
);
});
CancelAllExceptCurrentButton.displayName = 'CancelAllExceptCurrentButton';

View File

@@ -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 <CancelCurrentIconButton />;
}
if (isCancelAndClearAllEnabled) {
// Shift is pressed and cancel and clear all is enabled - show cancel and clear all
return <CancelAndClearAllIconButton />;
}
// Shift is pressed and cancel and clear all is disabled - show cancel all except current
return <CancelAllExceptCurrentIconButton />;
});
ClearQueueIconButton.displayName = 'ClearQueueIconButton';
const CancelCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
return (
<IconButton
size="lg"
isDisabled={shift ? clearQueue.isDisabled : cancelCurrentQueueItem.isDisabled}
isLoading={shift ? clearQueue.isLoading : cancelCurrentQueueItem.isLoading}
aria-label={shift ? t('queue.clear') : t('queue.cancel')}
tooltip={shift ? t('queue.clearTooltip') : t('queue.cancelTooltip')}
icon={shift ? <PiTrashSimpleBold /> : <PiXBold />}
isDisabled={cancelCurrentQueueItem.isDisabled}
isLoading={cancelCurrentQueueItem.isLoading}
aria-label={t('queue.cancel')}
tooltip={t('queue.cancelTooltip')}
icon={<PiXBold />}
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 (
<IconButton
size="lg"
isDisabled={clearQueue.isDisabled}
isLoading={clearQueue.isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.clearTooltip')}
icon={<PiTrashSimpleBold />}
colorScheme="error"
onClick={clearQueue.openDialog}
/>
);
});
CancelAndClearAllIconButton.displayName = 'CancelAndClearAllIconButton';
const CancelAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
return (
<IconButton
size="lg"
isDisabled={cancelAllExceptCurrent.isDisabled}
isLoading={cancelAllExceptCurrent.isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.cancelAllExceptCurrentTooltip')}
icon={<PiXCircle />}
colorScheme="error"
onClick={cancelAllExceptCurrent.openDialog}
/>
);
});
CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';

View File

@@ -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')}
</MenuItem>
<MenuItem
isDestructive
icon={<PiTrashSimpleBold />}
onClick={clearQueue.openDialog}
isLoading={clearQueue.isLoading}
isDisabled={clearQueue.isDisabled}
>
{t('queue.clearTooltip')}
</MenuItem>
{isCancelAndClearAllEnabled && (
<MenuItem
isDestructive
icon={<PiTrashSimpleBold />}
onClick={clearQueue.openDialog}
isLoading={clearQueue.isLoading}
isDisabled={clearQueue.isDisabled}
>
{t('queue.clearTooltip')}
</MenuItem>
)}
{isResumeEnabled && (
<MenuItem
icon={<PiPlayFill />}

View File

@@ -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 (
<Flex flexDir="column" layerStyle="first" borderRadius="base" p={2} gap={2}>
<Flex gap={2}>
@@ -25,7 +28,8 @@ const QueueTabQueueControls = () => {
)}
<ButtonGroup w={28} orientation="vertical" size="sm">
<PruneQueueButton />
<ClearQueueButton />
{isCancelAndClearAllEnabled && <ClearQueueButton />}
{!isCancelAndClearAllEnabled && <CancelAllExceptCurrentButton />}
</ButtonGroup>
</Flex>
<ClearModelCacheButton />

View File

@@ -141,7 +141,7 @@ export const AppContent = memo(() => {
)}
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
<MainPanelContent />
{withLeftPanel && <FloatingParametersPanelButtons panelApi={leftPanel} />}
{withLeftPanel && <FloatingParametersPanelButtons togglePanel={leftPanel.toggle} />}
{withRightPanel && <FloatingGalleryButton panelApi={rightPanel} />}
</Panel>
{withRightPanel && (

View File

@@ -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 (
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
{tab === 'canvas' && !imageViewer.isOpen && (
<CanvasManagerProviderGate>
<ToolChooser />
</CanvasManagerProviderGate>
)}
<ButtonGroup orientation="vertical" h={48}>
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
<IconButton
aria-label={t('accessibility.toggleLeftPanel')}
onClick={togglePanel}
icon={<PiSlidersHorizontalBold />}
flexGrow={1}
/>
</Tooltip>
<InvokeIconButton />
<CancelCurrentIconButton />
{/* Show the cancel all except current button instead of cancel and clear all when it is disabled */}
{isCancelAndClearAllEnabled && <CancelAndClearAllIconButton />}
{!isCancelAndClearAllEnabled && <CancelAllExceptCurrentIconButton />}
</ButtonGroup>
</Flex>
);
};
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 (
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
{tab === 'canvas' && !imageViewer.isOpen && (
<CanvasManagerProviderGate>
<ToolChooser />
</CanvasManagerProviderGate>
)}
<ButtonGroup orientation="vertical" h={48}>
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
<IconButton
aria-label={t('accessibility.toggleLeftPanel')}
onClick={props.panelApi.toggle}
icon={<PiSlidersHorizontalBold />}
flexGrow={1}
/>
</Tooltip>
<InvokeButtonTooltip prepend={shift} placement="end">
<IconButton
aria-label={t('queue.queueBack')}
onClick={shift ? queue.queueFront : queue.queueBack}
isLoading={queue.isLoading}
isDisabled={queue.isDisabled}
icon={queueButtonIcon}
colorScheme="invokeYellow"
flexGrow={1}
/>
</InvokeButtonTooltip>
<Tooltip label={t('queue.cancelTooltip')} placement="end">
<IconButton
isDisabled={cancelCurrent.isDisabled}
isLoading={cancelCurrent.isLoading}
aria-label={t('queue.cancelTooltip')}
icon={<PiXBold />}
onClick={cancelCurrent.cancelQueueItem}
colorScheme="error"
flexGrow={1}
/>
</Tooltip>
<Tooltip label={t('queue.clearTooltip')} placement="end">
<IconButton
isDisabled={clearQueue.isDisabled}
isLoading={clearQueue.isLoading}
aria-label={t('queue.clearTooltip')}
icon={<PiTrashSimpleBold />}
colorScheme="error"
onClick={clearQueue.openDialog}
data-testid={t('queue.clear')}
flexGrow={1}
/>
</Tooltip>
</ButtonGroup>
</Flex>
<InvokeButtonTooltip prepend={shift} placement="end">
<IconButton
aria-label={t('queue.queueBack')}
onClick={shift ? queue.queueFront : queue.queueBack}
isLoading={queue.isLoading}
isDisabled={queue.isDisabled}
icon={queueButtonIcon}
colorScheme="invokeYellow"
flexGrow={1}
/>
</InvokeButtonTooltip>
);
};
});
InvokeIconButton.displayName = 'InvokeIconButton';
export default memo(FloatingSidePanelButtons);
const CancelCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
return (
<Tooltip label={t('queue.cancelTooltip')} placement="end">
<IconButton
isDisabled={cancelCurrentQueueItem.isDisabled}
isLoading={cancelCurrentQueueItem.isLoading}
aria-label={t('queue.cancelTooltip')}
icon={<PiXBold />}
onClick={cancelCurrentQueueItem.cancelQueueItem}
colorScheme="error"
flexGrow={1}
/>
</Tooltip>
);
});
CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
const CancelAndClearAllIconButton = memo(() => {
const { t } = useTranslation();
const clearQueue = useClearQueueDialog();
return (
<Tooltip label={t('queue.clearTooltip')} placement="end">
<IconButton
isDisabled={clearQueue.isDisabled}
isLoading={clearQueue.isLoading}
aria-label={t('queue.clearTooltip')}
icon={<PiTrashSimpleBold />}
colorScheme="error"
onClick={clearQueue.openDialog}
flexGrow={1}
/>
</Tooltip>
);
});
CancelAndClearAllIconButton.displayName = 'CancelAndClearAllIconButton';
const CancelAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
return (
<Tooltip label={t('queue.cancelAllExceptCurrentTooltip')} placement="end">
<IconButton
isDisabled={cancelAllExceptCurrent.isDisabled}
isLoading={cancelAllExceptCurrent.isLoading}
aria-label={t('queue.clear')}
icon={<PiXCircle />}
colorScheme="error"
onClick={cancelAllExceptCurrent.openDialog}
flexGrow={1}
/>
</Tooltip>
);
});
CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';