feat(ui): prevent creating new canvases while staging

Disable these items while staging:
- New Canvas From Image context menu
- Edit image hook & launchpad button
- Generate from Text launchpad button (only while on canvas tab)
- Use a Layout Image launchpad button
This commit is contained in:
psychedelicious
2025-08-05 08:38:05 +10:00
parent f65dc2c081
commit 68e30a9864
5 changed files with 44 additions and 13 deletions

View File

@@ -2,6 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
@@ -17,6 +18,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
const store = useAppStore();
const imageDTO = useImageDTOContext();
const isBusy = useCanvasIsBusySafe();
const isStaging = useCanvasIsStaging();
const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => {
const { dispatch, getState } = store;
@@ -97,27 +99,31 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
<SubMenuButtonContent label={t('controlLayers.newCanvasFromImage')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithRasterLayerFromImage}
isDisabled={isStaging || isBusy}
>
{t('controlLayers.asRasterLayer')}
</MenuItem>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithRasterLayerFromImageWithResize}
isDisabled={isBusy}
isDisabled={isStaging || isBusy}
>
{t('controlLayers.asRasterLayerResize')}
</MenuItem>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithControlLayerFromImage}
isDisabled={isBusy}
isDisabled={isStaging || isBusy}
>
{t('controlLayers.asControlLayer')}
</MenuItem>
<MenuItem
icon={<PiFileBold />}
onClickCapture={onClickNewCanvasWithControlLayerFromImageWithResize}
isDisabled={isBusy}
isDisabled={isStaging || isBusy}
>
{t('controlLayers.asControlLayerResize')}
</MenuItem>

View File

@@ -1,5 +1,6 @@
import { useAppStore } from 'app/store/storeHooks';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { navigationApi } from 'features/ui/layouts/navigation-api';
@@ -13,13 +14,17 @@ export const useEditImage = (imageDTO?: ImageDTO | null) => {
const { getState, dispatch } = useAppStore();
const canvasManager = useCanvasManagerSafe();
const isStaging = useCanvasIsStaging();
const isEnabled = useMemo(() => {
if (!imageDTO) {
return false;
}
if (isStaging) {
return false;
}
return true;
}, [imageDTO]);
}, [imageDTO, isStaging]);
const edit = useCallback(async () => {
if (!imageDTO) {

View File

@@ -1,6 +1,7 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/storeHooks';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
@@ -17,6 +18,7 @@ const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
export const LaunchpadEditImageButton = memo((props: { extraAction?: () => void }) => {
const { t } = useTranslation();
const { getState, dispatch } = useAppStore();
const isStaging = useCanvasIsStaging();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
@@ -28,17 +30,22 @@ export const LaunchpadEditImageButton = memo((props: { extraAction?: () => void
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
return (
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8} isDisabled={isStaging}>
<Icon as={PiPencilBold} boxSize={8} color="base.500" />
<Flex flexDir="column" alignItems="flex-start" gap={2}>
<Heading size="sm">{t('ui.launchpad.editImage.title')}</Heading>
<Text color="base.300">{t('ui.launchpad.editImage.description')}</Text>
<Text>{t('ui.launchpad.editImage.description')}</Text>
</Flex>
<Flex position="absolute" right={3} bottom={3}>
<PiUploadBold />
<input {...uploadApi.getUploadInputProps()} />
</Flex>
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
<DndDropTarget
dndTarget={newCanvasFromImageDndTarget}
dndTargetData={dndTargetData}
label="Drop"
isDisabled={isStaging}
/>
</LaunchpadButton>
);
});

View File

@@ -1,5 +1,8 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { LaunchpadButton } from 'features/ui/layouts/LaunchpadButton';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi';
@@ -14,16 +17,19 @@ const focusOnPrompt = () => {
export const LaunchpadGenerateFromTextButton = memo((props: { extraAction?: () => void }) => {
const { t } = useTranslation();
const tab = useAppSelector(selectActiveTab);
const isStaging = useCanvasIsStaging();
const onClick = useCallback(() => {
focusOnPrompt();
props.extraAction?.();
}, [props]);
return (
<LaunchpadButton onClick={onClick} position="relative" gap={8}>
<LaunchpadButton onClick={onClick} position="relative" gap={8} isDisabled={tab === 'canvas' && isStaging}>
<Icon as={PiTextAaBold} boxSize={8} color="base.500" />
<Flex flexDir="column" alignItems="flex-start" gap={2}>
<Heading size="sm">{t('ui.launchpad.generateFromText.title')}</Heading>
<Text color="base.300">{t('ui.launchpad.generateFromText.description')}</Text>
<Text>{t('ui.launchpad.generateFromText.description')}</Text>
</Flex>
<Flex position="absolute" right={3} bottom={3}>
<PiCursorTextBold />

View File

@@ -1,6 +1,7 @@
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/storeHooks';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
@@ -18,6 +19,7 @@ const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
export const LaunchpadUseALayoutImageButton = memo((props: { extraAction?: () => void }) => {
const { t } = useTranslation();
const { getState, dispatch } = useAppStore();
const isStaging = useCanvasIsStaging();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
@@ -29,17 +31,22 @@ export const LaunchpadUseALayoutImageButton = memo((props: { extraAction?: () =>
const uploadApi = useImageUploadButton({ allowMultiple: false, onUpload });
return (
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8}>
<LaunchpadButton {...uploadApi.getUploadButtonProps()} position="relative" gap={8} isDisabled={isStaging}>
<Icon as={PiRectangleDashedBold} boxSize={8} color="base.500" />
<Flex flexDir="column" alignItems="flex-start" gap={2}>
<Heading size="sm">{t('ui.launchpad.useALayoutImage.title')}</Heading>
<Text color="base.300">{t('ui.launchpad.useALayoutImage.description')}</Text>
<Text>{t('ui.launchpad.useALayoutImage.description')}</Text>
</Flex>
<Flex position="absolute" right={3} bottom={3}>
<PiUploadBold />
<input {...uploadApi.getUploadInputProps()} />
</Flex>
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
<DndDropTarget
dndTarget={newCanvasFromImageDndTarget}
dndTargetData={dndTargetData}
label="Drop"
isDisabled={isStaging}
/>
</LaunchpadButton>
);
});