mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into feat/model-events
This commit is contained in:
commit
6ae10798b0
@ -1,6 +1,8 @@
|
|||||||
from typing import Literal
|
from os.path import exists
|
||||||
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from pydantic.fields import Field
|
import numpy as np
|
||||||
|
from pydantic import Field, validator
|
||||||
|
|
||||||
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext
|
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext
|
||||||
from dynamicprompts.generators import RandomPromptGenerator, CombinatorialPromptGenerator
|
from dynamicprompts.generators import RandomPromptGenerator, CombinatorialPromptGenerator
|
||||||
@ -55,3 +57,41 @@ class DynamicPromptInvocation(BaseInvocation):
|
|||||||
prompts = generator.generate(self.prompt, num_images=self.max_prompts)
|
prompts = generator.generate(self.prompt, num_images=self.max_prompts)
|
||||||
|
|
||||||
return PromptCollectionOutput(prompt_collection=prompts, count=len(prompts))
|
return PromptCollectionOutput(prompt_collection=prompts, count=len(prompts))
|
||||||
|
|
||||||
|
|
||||||
|
class PromptsFromFileInvocation(BaseInvocation):
|
||||||
|
'''Loads prompts from a text file'''
|
||||||
|
# fmt: off
|
||||||
|
type: Literal['prompt_from_file'] = 'prompt_from_file'
|
||||||
|
|
||||||
|
# Inputs
|
||||||
|
file_path: str = Field(description="Path to prompt text file")
|
||||||
|
pre_prompt: Optional[str] = Field(description="String to prepend to each prompt")
|
||||||
|
post_prompt: Optional[str] = Field(description="String to append to each prompt")
|
||||||
|
start_line: int = Field(default=1, ge=1, description="Line in the file to start start from")
|
||||||
|
max_prompts: int = Field(default=1, ge=0, description="Max lines to read from file (0=all)")
|
||||||
|
#fmt: on
|
||||||
|
|
||||||
|
@validator("file_path")
|
||||||
|
def file_path_exists(cls, v):
|
||||||
|
if not exists(v):
|
||||||
|
raise ValueError(FileNotFoundError)
|
||||||
|
return v
|
||||||
|
|
||||||
|
def promptsFromFile(self, file_path: str, pre_prompt: str, post_prompt: str, start_line: int, max_prompts: int):
|
||||||
|
prompts = []
|
||||||
|
start_line -= 1
|
||||||
|
end_line = start_line + max_prompts
|
||||||
|
if max_prompts <= 0:
|
||||||
|
end_line = np.iinfo(np.int32).max
|
||||||
|
with open(file_path) as f:
|
||||||
|
for i, line in enumerate(f):
|
||||||
|
if i >= start_line and i < end_line:
|
||||||
|
prompts.append((pre_prompt or '') + line.strip() + (post_prompt or ''))
|
||||||
|
if i >= end_line:
|
||||||
|
break
|
||||||
|
return prompts
|
||||||
|
|
||||||
|
def invoke(self, context: InvocationContext) -> PromptCollectionOutput:
|
||||||
|
prompts = self.promptsFromFile(self.file_path, self.pre_prompt, self.post_prompt, self.start_line, self.max_prompts)
|
||||||
|
return PromptCollectionOutput(prompt_collection=prompts, count=len(prompts))
|
||||||
|
@ -68,7 +68,7 @@ class TextualInversionModel(ModelBase):
|
|||||||
return None # diffusers-ti
|
return None # diffusers-ti
|
||||||
|
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
if any([path.endswith(f".{ext}") for ext in ["safetensors", "ckpt", "pt"]]):
|
if any([path.endswith(f".{ext}") for ext in ["safetensors", "ckpt", "pt", "bin"]]):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
raise InvalidModelException(f"Not a valid model: {path}")
|
raise InvalidModelException(f"Not a valid model: {path}")
|
||||||
|
@ -572,6 +572,7 @@
|
|||||||
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
|
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
|
||||||
"downloadImageStarted": "Image Download Started",
|
"downloadImageStarted": "Image Download Started",
|
||||||
"imageCopied": "Image Copied",
|
"imageCopied": "Image Copied",
|
||||||
|
"problemCopyingImage": "Unable to Copy Image",
|
||||||
"imageLinkCopied": "Image Link Copied",
|
"imageLinkCopied": "Image Link Copied",
|
||||||
"problemCopyingImageLink": "Unable to Copy Image Link",
|
"problemCopyingImageLink": "Unable to Copy Image Link",
|
||||||
"imageNotLoaded": "No Image Loaded",
|
"imageNotLoaded": "No Image Loaded",
|
||||||
|
@ -21,6 +21,7 @@ import { ImageDTO } from 'services/api/types';
|
|||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
import IAIDraggable from './IAIDraggable';
|
import IAIDraggable from './IAIDraggable';
|
||||||
import IAIDroppable from './IAIDroppable';
|
import IAIDroppable from './IAIDroppable';
|
||||||
|
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||||
|
|
||||||
type IAIDndImageProps = {
|
type IAIDndImageProps = {
|
||||||
imageDTO: ImageDTO | undefined;
|
imageDTO: ImageDTO | undefined;
|
||||||
@ -96,7 +97,10 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ImageContextMenu imageDTO={imageDTO}>
|
||||||
|
{(ref) => (
|
||||||
<Flex
|
<Flex
|
||||||
|
ref={ref}
|
||||||
sx={{
|
sx={{
|
||||||
width: 'full',
|
width: 'full',
|
||||||
height: 'full',
|
height: 'full',
|
||||||
@ -209,6 +213,8 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
)}
|
||||||
|
</ImageContextMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ import IAICanvasRedoButton from './IAICanvasRedoButton';
|
|||||||
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
||||||
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
||||||
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
||||||
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
|
|
||||||
export const selector = createSelector(
|
export const selector = createSelector(
|
||||||
[systemSelector, canvasSelector, isStagingSelector],
|
[systemSelector, canvasSelector, isStagingSelector],
|
||||||
@ -79,6 +80,7 @@ const IAICanvasToolbar = () => {
|
|||||||
const canvasBaseLayer = getCanvasBaseLayer();
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
|
||||||
|
|
||||||
const { openUploader } = useImageUploader();
|
const { openUploader } = useImageUploader();
|
||||||
|
|
||||||
@ -136,10 +138,10 @@ const IAICanvasToolbar = () => {
|
|||||||
handleCopyImageToClipboard();
|
handleCopyImageToClipboard();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: () => !isStaging,
|
enabled: () => !isStaging && isClipboardAPIAvailable,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[canvasBaseLayer, isProcessing]
|
[canvasBaseLayer, isProcessing, isClipboardAPIAvailable]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@ -189,6 +191,9 @@ const IAICanvasToolbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyImageToClipboard = () => {
|
const handleCopyImageToClipboard = () => {
|
||||||
|
if (!isClipboardAPIAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch(canvasCopiedToClipboard());
|
dispatch(canvasCopiedToClipboard());
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -256,6 +261,7 @@ const IAICanvasToolbar = () => {
|
|||||||
onClick={handleSaveToGallery}
|
onClick={handleSaveToGallery}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
|
{isClipboardAPIAvailable && (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
@ -263,6 +269,7 @@ const IAICanvasToolbar = () => {
|
|||||||
onClick={handleCopyImageToClipboard}
|
onClick={handleCopyImageToClipboard}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import { ButtonGroup, Flex, FlexProps, Link } from '@chakra-ui/react';
|
import {
|
||||||
|
ButtonGroup,
|
||||||
|
Flex,
|
||||||
|
FlexProps,
|
||||||
|
Link,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
@ -20,6 +29,7 @@ import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/U
|
|||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import {
|
import {
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
@ -48,6 +58,8 @@ import {
|
|||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
|
||||||
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
|
import SingleSelectionMenuItems from '../ImageContextMenu/SingleSelectionMenuItems';
|
||||||
|
|
||||||
const currentImageButtonsSelector = createSelector(
|
const currentImageButtonsSelector = createSelector(
|
||||||
[stateSelector, activeTabNameSelector],
|
[stateSelector, activeTabNameSelector],
|
||||||
@ -120,6 +132,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
||||||
|
useCopyImageToClipboard();
|
||||||
|
|
||||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||||
useRecallParameters();
|
useRecallParameters();
|
||||||
|
|
||||||
@ -128,7 +143,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
500
|
500
|
||||||
);
|
);
|
||||||
|
|
||||||
const { currentData: image, isFetching } = useGetImageDTOQuery(
|
const { currentData: imageDTO, isFetching } = useGetImageDTOQuery(
|
||||||
lastSelectedImage ?? skipToken
|
lastSelectedImage ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -142,15 +157,15 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
|
|
||||||
const handleCopyImageLink = useCallback(() => {
|
const handleCopyImageLink = useCallback(() => {
|
||||||
const getImageUrl = () => {
|
const getImageUrl = () => {
|
||||||
if (!image) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.image_url.startsWith('http')) {
|
if (imageDTO.image_url.startsWith('http')) {
|
||||||
return image.image_url;
|
return imageDTO.image_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.location.toString() + image.image_url;
|
return window.location.toString() + imageDTO.image_url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = getImageUrl();
|
const url = getImageUrl();
|
||||||
@ -174,7 +189,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [toaster, t, image]);
|
}, [toaster, t, imageDTO]);
|
||||||
|
|
||||||
const handleClickUseAllParameters = useCallback(() => {
|
const handleClickUseAllParameters = useCallback(() => {
|
||||||
recallAllParameters(metadata);
|
recallAllParameters(metadata);
|
||||||
@ -192,31 +207,31 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
recallSeed(metadata?.seed);
|
recallSeed(metadata?.seed);
|
||||||
}, [metadata?.seed, recallSeed]);
|
}, [metadata?.seed, recallSeed]);
|
||||||
|
|
||||||
useHotkeys('s', handleUseSeed, [image]);
|
useHotkeys('s', handleUseSeed, [imageDTO]);
|
||||||
|
|
||||||
const handleUsePrompt = useCallback(() => {
|
const handleUsePrompt = useCallback(() => {
|
||||||
recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt);
|
recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt);
|
||||||
}, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]);
|
}, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]);
|
||||||
|
|
||||||
useHotkeys('p', handleUsePrompt, [image]);
|
useHotkeys('p', handleUsePrompt, [imageDTO]);
|
||||||
|
|
||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(initialImageSelected(image));
|
dispatch(initialImageSelected(imageDTO));
|
||||||
}, [dispatch, image]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
useHotkeys('shift+i', handleSendToImageToImage, [image]);
|
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
||||||
|
|
||||||
const handleClickUpscale = useCallback(() => {
|
const handleClickUpscale = useCallback(() => {
|
||||||
// selectedImage && dispatch(runESRGAN(selectedImage));
|
// selectedImage && dispatch(runESRGAN(selectedImage));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
if (!image) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageToDeleteSelected(image));
|
dispatch(imageToDeleteSelected(imageDTO));
|
||||||
}, [dispatch, image]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'Shift+U',
|
'Shift+U',
|
||||||
@ -236,7 +251,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
isUpscalingEnabled,
|
isUpscalingEnabled,
|
||||||
image,
|
imageDTO,
|
||||||
isESRGANAvailable,
|
isESRGANAvailable,
|
||||||
shouldDisableToolbarButtons,
|
shouldDisableToolbarButtons,
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -268,7 +283,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
|
|
||||||
[
|
[
|
||||||
isFaceRestoreEnabled,
|
isFaceRestoreEnabled,
|
||||||
image,
|
imageDTO,
|
||||||
isGFPGANAvailable,
|
isGFPGANAvailable,
|
||||||
shouldDisableToolbarButtons,
|
shouldDisableToolbarButtons,
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -283,10 +298,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSendToCanvas = useCallback(() => {
|
const handleSendToCanvas = useCallback(() => {
|
||||||
if (!image) return;
|
if (!imageDTO) return;
|
||||||
dispatch(sentImageToCanvas());
|
dispatch(sentImageToCanvas());
|
||||||
|
|
||||||
dispatch(setInitialCanvasImage(image));
|
dispatch(setInitialCanvasImage(imageDTO));
|
||||||
dispatch(requestCanvasRescale());
|
dispatch(requestCanvasRescale());
|
||||||
|
|
||||||
if (activeTabName !== 'unifiedCanvas') {
|
if (activeTabName !== 'unifiedCanvas') {
|
||||||
@ -299,12 +314,12 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
duration: 2500,
|
duration: 2500,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
}, [image, dispatch, activeTabName, toaster, t]);
|
}, [imageDTO, dispatch, activeTabName, toaster, t]);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'i',
|
'i',
|
||||||
() => {
|
() => {
|
||||||
if (image) {
|
if (imageDTO) {
|
||||||
handleClickShowImageDetails();
|
handleClickShowImageDetails();
|
||||||
} else {
|
} else {
|
||||||
toaster({
|
toaster({
|
||||||
@ -315,13 +330,20 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[image, shouldShowImageDetails, toaster]
|
[imageDTO, shouldShowImageDetails, toaster]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickProgressImagesToggle = useCallback(() => {
|
const handleClickProgressImagesToggle = useCallback(() => {
|
||||||
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
||||||
}, [dispatch, shouldShowProgressInViewer]);
|
}, [dispatch, shouldShowProgressInViewer]);
|
||||||
|
|
||||||
|
const handleCopyImage = useCallback(() => {
|
||||||
|
if (!imageDTO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copyImageToClipboard(imageDTO.image_url);
|
||||||
|
}, [copyImageToClipboard, imageDTO]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex
|
<Flex
|
||||||
@ -334,63 +356,18 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
||||||
<IAIPopover
|
<Menu>
|
||||||
triggerComponent={
|
<MenuButton
|
||||||
<IAIIconButton
|
as={IAIIconButton}
|
||||||
aria-label={`${t('parameters.sendTo')}...`}
|
aria-label={`${t('parameters.sendTo')}...`}
|
||||||
tooltip={`${t('parameters.sendTo')}...`}
|
tooltip={`${t('parameters.sendTo')}...`}
|
||||||
isDisabled={!image}
|
isDisabled={!imageDTO}
|
||||||
icon={<FaShareAlt />}
|
icon={<FaShareAlt />}
|
||||||
/>
|
/>
|
||||||
}
|
<MenuList motionProps={menuListMotionProps}>
|
||||||
>
|
{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}
|
||||||
<Flex
|
</MenuList>
|
||||||
sx={{
|
</Menu>
|
||||||
flexDirection: 'column',
|
|
||||||
rowGap: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IAIButton
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSendToImageToImage}
|
|
||||||
leftIcon={<FaShare />}
|
|
||||||
id="send-to-img2img"
|
|
||||||
>
|
|
||||||
{t('parameters.sendToImg2Img')}
|
|
||||||
</IAIButton>
|
|
||||||
{isCanvasEnabled && (
|
|
||||||
<IAIButton
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSendToCanvas}
|
|
||||||
leftIcon={<FaShare />}
|
|
||||||
id="send-to-canvas"
|
|
||||||
>
|
|
||||||
{t('parameters.sendToUnifiedCanvas')}
|
|
||||||
</IAIButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* <IAIButton
|
|
||||||
size="sm"
|
|
||||||
onClick={handleCopyImage}
|
|
||||||
leftIcon={<FaCopy />}
|
|
||||||
>
|
|
||||||
{t('parameters.copyImage')}
|
|
||||||
</IAIButton> */}
|
|
||||||
<IAIButton
|
|
||||||
size="sm"
|
|
||||||
onClick={handleCopyImageLink}
|
|
||||||
leftIcon={<FaCopy />}
|
|
||||||
>
|
|
||||||
{t('parameters.copyImageToLink')}
|
|
||||||
</IAIButton>
|
|
||||||
|
|
||||||
<Link download={true} href={image?.image_url} target="_blank">
|
|
||||||
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
|
|
||||||
{t('parameters.downloadImage')}
|
|
||||||
</IAIButton>
|
|
||||||
</Link>
|
|
||||||
</Flex>
|
|
||||||
</IAIPopover>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
||||||
@ -443,7 +420,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
<IAIButton
|
<IAIButton
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!isGFPGANAvailable ||
|
!isGFPGANAvailable ||
|
||||||
!image ||
|
!imageDTO ||
|
||||||
!(isConnected && !isProcessing) ||
|
!(isConnected && !isProcessing) ||
|
||||||
!facetoolStrength
|
!facetoolStrength
|
||||||
}
|
}
|
||||||
@ -474,7 +451,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
<IAIButton
|
<IAIButton
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!isESRGANAvailable ||
|
!isESRGANAvailable ||
|
||||||
!image ||
|
!imageDTO ||
|
||||||
!(isConnected && !isProcessing) ||
|
!(isConnected && !isProcessing) ||
|
||||||
!upscalingLevel
|
!upscalingLevel
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,14 @@ import { stateSelector } from 'app/store/store';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||||
import { memo, useMemo } from 'react';
|
import { MouseEvent, memo, useCallback, useMemo } from 'react';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
|
import { menuListMotionProps } from 'theme/components/menu';
|
||||||
import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
|
import MultipleSelectionMenuItems from './MultipleSelectionMenuItems';
|
||||||
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
imageDTO: ImageDTO;
|
imageDTO: ImageDTO | undefined;
|
||||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,18 +32,32 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
|||||||
|
|
||||||
const { selectionCount } = useAppSelector(selector);
|
const { selectionCount } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const handleContextMenu = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu<HTMLDivElement>
|
<ContextMenu<HTMLDivElement>
|
||||||
menuProps={{ size: 'sm', isLazy: true }}
|
menuProps={{ size: 'sm', isLazy: true }}
|
||||||
renderMenu={() => (
|
menuButtonProps={{
|
||||||
<MenuList sx={{ visibility: 'visible !important' }}>
|
bg: 'transparent',
|
||||||
|
_hover: { bg: 'transparent' },
|
||||||
|
}}
|
||||||
|
renderMenu={() =>
|
||||||
|
imageDTO ? (
|
||||||
|
<MenuList
|
||||||
|
sx={{ visibility: 'visible !important' }}
|
||||||
|
motionProps={menuListMotionProps}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
>
|
||||||
{selectionCount === 1 ? (
|
{selectionCount === 1 ? (
|
||||||
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
||||||
) : (
|
) : (
|
||||||
<MultipleSelectionMenuItems />
|
<MultipleSelectionMenuItems />
|
||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
)}
|
) : null
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
import { Link, MenuItem } from '@chakra-ui/react';
|
||||||
import { MenuItem } from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppToaster } from 'app/components/Toaster';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { stateSelector } from 'app/store/store';
|
import { stateSelector } from 'app/store/store';
|
||||||
@ -14,11 +13,21 @@ import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletio
|
|||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { memo, useCallback, useContext, useMemo } from 'react';
|
import { memo, useCallback, useContext, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaFolder, FaShare, FaTrash } from 'react-icons/fa';
|
import {
|
||||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
FaAsterisk,
|
||||||
|
FaCopy,
|
||||||
|
FaDownload,
|
||||||
|
FaExternalLinkAlt,
|
||||||
|
FaFolder,
|
||||||
|
FaQuoteRight,
|
||||||
|
FaSeedling,
|
||||||
|
FaShare,
|
||||||
|
FaTrash,
|
||||||
|
} from 'react-icons/fa';
|
||||||
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
||||||
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
||||||
import { ImageDTO } from 'services/api/types';
|
import { ImageDTO } from 'services/api/types';
|
||||||
@ -61,6 +70,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
|
|
||||||
const { currentData } = useGetImageMetadataQuery(imageDTO.image_name);
|
const { currentData } = useGetImageMetadataQuery(imageDTO.image_name);
|
||||||
|
|
||||||
|
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
||||||
|
useCopyImageToClipboard();
|
||||||
|
|
||||||
const metadata = currentData?.metadata;
|
const metadata = currentData?.metadata;
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
@ -130,13 +142,27 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
||||||
}, [dispatch, imageDTO.image_name]);
|
}, [dispatch, imageDTO.image_name]);
|
||||||
|
|
||||||
|
const handleCopyImage = useCallback(() => {
|
||||||
|
copyImageToClipboard(imageDTO.image_url);
|
||||||
|
}, [copyImageToClipboard, imageDTO.image_url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem icon={<ExternalLinkIcon />} onClickCapture={handleOpenInNewTab}>
|
<Link href={imageDTO.image_url} target="_blank">
|
||||||
|
<MenuItem
|
||||||
|
icon={<FaExternalLinkAlt />}
|
||||||
|
onClickCapture={handleOpenInNewTab}
|
||||||
|
>
|
||||||
{t('common.openInNewTab')}
|
{t('common.openInNewTab')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
</Link>
|
||||||
|
{isClipboardAPIAvailable && (
|
||||||
|
<MenuItem icon={<FaCopy />} onClickCapture={handleCopyImage}>
|
||||||
|
{t('parameters.copyImage')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<IoArrowUndoCircleOutline />}
|
icon={<FaQuoteRight />}
|
||||||
onClickCapture={handleRecallPrompt}
|
onClickCapture={handleRecallPrompt}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
metadata?.positive_prompt === undefined &&
|
metadata?.positive_prompt === undefined &&
|
||||||
@ -147,14 +173,14 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<IoArrowUndoCircleOutline />}
|
icon={<FaSeedling />}
|
||||||
onClickCapture={handleRecallSeed}
|
onClickCapture={handleRecallSeed}
|
||||||
isDisabled={metadata?.seed === undefined}
|
isDisabled={metadata?.seed === undefined}
|
||||||
>
|
>
|
||||||
{t('parameters.useSeed')}
|
{t('parameters.useSeed')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<IoArrowUndoCircleOutline />}
|
icon={<FaAsterisk />}
|
||||||
onClickCapture={handleUseAllParameters}
|
onClickCapture={handleUseAllParameters}
|
||||||
isDisabled={!metadata}
|
isDisabled={!metadata}
|
||||||
>
|
>
|
||||||
@ -193,6 +219,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
Remove from Board
|
Remove from Board
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
<Link download={true} href={imageDTO.image_url} target="_blank">
|
||||||
|
<MenuItem icon={<FaDownload />} w="100%">
|
||||||
|
{t('parameters.downloadImage')}
|
||||||
|
</MenuItem>
|
||||||
|
</Link>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
|
@ -4,6 +4,8 @@ import IAIIconButton from 'common/components/IAIIconButton';
|
|||||||
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
|
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaCopy } from 'react-icons/fa';
|
import { FaCopy } from 'react-icons/fa';
|
||||||
@ -11,6 +13,7 @@ import { FaCopy } from 'react-icons/fa';
|
|||||||
export default function UnifiedCanvasCopyToClipboard() {
|
export default function UnifiedCanvasCopyToClipboard() {
|
||||||
const isStaging = useAppSelector(isStagingSelector);
|
const isStaging = useAppSelector(isStagingSelector);
|
||||||
const canvasBaseLayer = getCanvasBaseLayer();
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
|
||||||
|
|
||||||
const isProcessing = useAppSelector(
|
const isProcessing = useAppSelector(
|
||||||
(state: RootState) => state.system.isProcessing
|
(state: RootState) => state.system.isProcessing
|
||||||
@ -25,15 +28,22 @@ export default function UnifiedCanvasCopyToClipboard() {
|
|||||||
handleCopyImageToClipboard();
|
handleCopyImageToClipboard();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: () => !isStaging,
|
enabled: () => !isStaging && isClipboardAPIAvailable,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[canvasBaseLayer, isProcessing]
|
[canvasBaseLayer, isProcessing, isClipboardAPIAvailable]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCopyImageToClipboard = () => {
|
const handleCopyImageToClipboard = useCallback(() => {
|
||||||
|
if (!isClipboardAPIAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch(canvasCopiedToClipboard());
|
dispatch(canvasCopiedToClipboard());
|
||||||
};
|
}, [dispatch, isClipboardAPIAvailable]);
|
||||||
|
|
||||||
|
if (!isClipboardAPIAvailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const useCopyImageToClipboard = () => {
|
||||||
|
const toaster = useAppToaster();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isClipboardAPIAvailable = useMemo(() => {
|
||||||
|
return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copyImageToClipboard = useCallback(
|
||||||
|
async (image_url: string) => {
|
||||||
|
if (!isClipboardAPIAvailable) {
|
||||||
|
toaster({
|
||||||
|
title: t('toast.problemCopyingImage'),
|
||||||
|
description: "Your browser doesn't support the Clipboard API.",
|
||||||
|
status: 'error',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(image_url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
[blob.type]: blob,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
toaster({
|
||||||
|
title: t('toast.imageCopied'),
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
toaster({
|
||||||
|
title: t('toast.problemCopyingImage'),
|
||||||
|
description: String(err),
|
||||||
|
status: 'error',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isClipboardAPIAvailable, t, toaster]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isClipboardAPIAvailable, copyImageToClipboard };
|
||||||
|
};
|
@ -1,6 +1,7 @@
|
|||||||
import { menuAnatomy } from '@chakra-ui/anatomy';
|
import { menuAnatomy } from '@chakra-ui/anatomy';
|
||||||
import { createMultiStyleConfigHelpers } from '@chakra-ui/react';
|
import { createMultiStyleConfigHelpers } from '@chakra-ui/react';
|
||||||
import { mode } from '@chakra-ui/theme-tools';
|
import { mode } from '@chakra-ui/theme-tools';
|
||||||
|
import { MotionProps } from 'framer-motion';
|
||||||
|
|
||||||
const { definePartsStyle, defineMultiStyleConfig } =
|
const { definePartsStyle, defineMultiStyleConfig } =
|
||||||
createMultiStyleConfigHelpers(menuAnatomy.keys);
|
createMultiStyleConfigHelpers(menuAnatomy.keys);
|
||||||
@ -21,6 +22,7 @@ const invokeAI = definePartsStyle((props) => ({
|
|||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
|
color: mode('base.900', 'base.150')(props),
|
||||||
bg: mode('base.200', 'base.800')(props),
|
bg: mode('base.200', 'base.800')(props),
|
||||||
shadow: 'dark-lg',
|
shadow: 'dark-lg',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@ -35,6 +37,9 @@ const invokeAI = definePartsStyle((props) => ({
|
|||||||
_focus: {
|
_focus: {
|
||||||
bg: mode('base.400', 'base.600')(props),
|
bg: mode('base.400', 'base.600')(props),
|
||||||
},
|
},
|
||||||
|
svg: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -46,3 +51,28 @@ export const menuTheme = defineMultiStyleConfig({
|
|||||||
variant: 'invokeAI',
|
variant: 'invokeAI',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const menuListMotionProps: MotionProps = {
|
||||||
|
variants: {
|
||||||
|
enter: {
|
||||||
|
visibility: 'visible',
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.07,
|
||||||
|
ease: [0.4, 0, 0.2, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
transitionEnd: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0.8,
|
||||||
|
transition: {
|
||||||
|
duration: 0.07,
|
||||||
|
easings: 'easeOut',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
45
pull_request_template.md
Normal file
45
pull_request_template.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
## What type of PR is this? (check all applicable)
|
||||||
|
|
||||||
|
- [ ] Refactor
|
||||||
|
- [ ] Feature
|
||||||
|
- [ ] Bug Fix
|
||||||
|
- [ ] Optimization
|
||||||
|
- [ ] Documentation Update
|
||||||
|
|
||||||
|
|
||||||
|
## Have you discussed this change with the InvokeAI team?
|
||||||
|
- [ ] Yes
|
||||||
|
- [ ] No, because:
|
||||||
|
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
|
||||||
|
## Related Tickets & Documents
|
||||||
|
|
||||||
|
<!--
|
||||||
|
For pull requests that relate or close an issue, please include them
|
||||||
|
below.
|
||||||
|
|
||||||
|
For example having the text: "closes #1234" would connect the current pull
|
||||||
|
request to issue 1234. And when we merge the pull request, Github will
|
||||||
|
automatically close the issue.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- Related Issue #
|
||||||
|
- Closes #
|
||||||
|
|
||||||
|
## QA Instructions, Screenshots, Recordings
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please provide steps on how to test changes, any hardware or
|
||||||
|
software specifications as well as any other pertinent information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Added/updated tests?
|
||||||
|
|
||||||
|
- [ ] Yes
|
||||||
|
- [ ] No : _please replace this line with details on why tests
|
||||||
|
have not been included_
|
||||||
|
|
||||||
|
## [optional] Are there any post deployment tasks we need to perform?
|
Loading…
Reference in New Issue
Block a user