diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 21489ffe1f..9df2e15e7f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -14,11 +14,11 @@ import { sentImageToCanvas, sentImageToImg2Img, } from 'features/gallery/store/actions'; -import { workflowLoadRequested } from 'features/nodes/store/actions'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; +import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; import { memo, useCallback } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; @@ -36,7 +36,6 @@ import { import { FaCircleNodes } from 'react-icons/fa6'; import { MdStar, MdStarBorder } from 'react-icons/md'; import { - useLazyGetImageWorkflowQuery, useStarImagesMutation, useUnstarImagesMutation, } from 'services/api/endpoints/images'; @@ -62,12 +61,12 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { imageDTO?.image_name ); - const [getWorkflow, getWorkflowResult] = useLazyGetImageWorkflowQuery(); + const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = + useGetAndLoadEmbeddedWorkflow({}); + const handleLoadWorkflow = useCallback(() => { - getWorkflow(imageDTO.image_name).then((workflow) => { - dispatch(workflowLoadRequested(workflow.data)); - }); - }, [dispatch, getWorkflow, imageDTO]); + getAndLoadEmbeddedWorkflow(imageDTO.image_name); + }, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]); const [starImages] = useStarImagesMutation(); const [unstarImages] = useUnstarImagesMutation(); @@ -176,7 +175,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { {t('parameters.downloadImage')} : } + icon={ + getAndLoadEmbeddedWorkflowResult.isLoading ? ( + + ) : ( + + ) + } onClickCapture={handleLoadWorkflow} isDisabled={!imageDTO.has_workflow} > diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromFileButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromFileButton.tsx index ba43efb6d1..1d8abac774 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromFileButton.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromFileButton.tsx @@ -8,7 +8,7 @@ import { FaUpload } from 'react-icons/fa'; const UploadWorkflowButton = () => { const { t } = useTranslation(); const resetRef = useRef<() => void>(null); - const loadWorkflowFromFile = useLoadWorkflowFromFile(resetRef); + const loadWorkflowFromFile = useLoadWorkflowFromFile({ resetRef }); return ( void; onError?: () => void; }; -export const useDeleteLibraryWorkflow = ({ +type UseDeleteLibraryWorkflowReturn = { + deleteWorkflow: (workflow_id: string) => Promise; + deleteWorkflowResult: ReturnType[1]; +}; + +type UseDeleteLibraryWorkflow = ( + arg: UseDeleteLibraryWorkflowOptions +) => UseDeleteLibraryWorkflowReturn; + +export const useDeleteLibraryWorkflow: UseDeleteLibraryWorkflow = ({ onSuccess, onError, -}: UseDeleteLibraryWorkflowArg) => { +}) => { const toaster = useAppToaster(); const { t } = useTranslation(); const [_deleteWorkflow, deleteWorkflowResult] = useDeleteWorkflowMutation(); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts index 86bded7b94..8c20801c53 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow.ts @@ -1,21 +1,52 @@ +import { useAppToaster } from 'app/components/Toaster'; import { useAppDispatch } from 'app/store/storeHooks'; import { workflowLoadRequested } from 'features/nodes/store/actions'; import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useLazyGetImageWorkflowQuery } from 'services/api/endpoints/images'; -export const useGetAndLoadEmbeddedWorkflow = ( - image_name: string | undefined -) => { - const dispatch = useAppDispatch(); - const [_trigger, result] = useLazyGetImageWorkflowQuery(); - const trigger = useCallback(() => { - if (!image_name) { - return; - } - _trigger(image_name).then((workflow) => { - dispatch(workflowLoadRequested(workflow.data)); - }); - }, [dispatch, _trigger, image_name]); - - return [trigger, result]; +type UseGetAndLoadEmbeddedWorkflowOptions = { + onSuccess?: () => void; + onError?: () => void; +}; + +type UseGetAndLoadEmbeddedWorkflowReturn = { + getAndLoadEmbeddedWorkflow: (imageName: string) => Promise; + getAndLoadEmbeddedWorkflowResult: ReturnType< + typeof useLazyGetImageWorkflowQuery + >[1]; +}; + +type UseGetAndLoadEmbeddedWorkflow = ( + options: UseGetAndLoadEmbeddedWorkflowOptions +) => UseGetAndLoadEmbeddedWorkflowReturn; + +export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ + onSuccess, + onError, +}) => { + const dispatch = useAppDispatch(); + const toaster = useAppToaster(); + const { t } = useTranslation(); + const [_getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult] = + useLazyGetImageWorkflowQuery(); + const getAndLoadEmbeddedWorkflow = useCallback( + async (imageName: string) => { + try { + const workflow = await _getAndLoadEmbeddedWorkflow(imageName); + dispatch(workflowLoadRequested(workflow.data)); + // No toast - the listener for this action does that after the workflow is loaded + onSuccess && onSuccess(); + } catch { + toaster({ + title: t('toast.problemRetrievingWorkflow'), + status: 'error', + }); + onError && onError(); + } + }, + [_getAndLoadEmbeddedWorkflow, dispatch, onSuccess, toaster, t, onError] + ); + + return { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult }; }; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts index 97ce78b7ab..b44e854176 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts @@ -5,15 +5,24 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useLazyGetWorkflowQuery } from 'services/api/endpoints/workflows'; -type UseGetAndLoadLibraryWorkflowArg = { +type UseGetAndLoadLibraryWorkflowOptions = { onSuccess?: () => void; onError?: () => void; }; -export const useGetAndLoadLibraryWorkflow = ({ +type UseGetAndLoadLibraryWorkflowReturn = { + getAndLoadWorkflow: (workflow_id: string) => Promise; + getAndLoadWorkflowResult: ReturnType[1]; +}; + +type UseGetAndLoadLibraryWorkflow = ( + arg: UseGetAndLoadLibraryWorkflowOptions +) => UseGetAndLoadLibraryWorkflowReturn; + +export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = ({ onSuccess, onError, -}: UseGetAndLoadLibraryWorkflowArg) => { +}) => { const dispatch = useAppDispatch(); const toaster = useAppToaster(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx index 03a7f5e824..bafab51f38 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx @@ -1,15 +1,22 @@ -import { ListItem, Text, UnorderedList } from '@chakra-ui/react'; import { useLogger } from 'app/logging/useLogger'; import { useAppDispatch } from 'app/store/storeHooks'; +import { workflowLoadRequested } from 'features/nodes/store/actions'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; -import { RefObject, memo, useCallback } from 'react'; +import { RefObject, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { ZodError } from 'zod'; -import { fromZodIssue } from 'zod-validation-error'; -import { workflowLoadRequested } from 'features/nodes/store/actions'; -export const useLoadWorkflowFromFile = (resetRef: RefObject<() => void>) => { +type useLoadWorkflowFromFileOptions = { + resetRef: RefObject<() => void>; +}; + +type UseLoadWorkflowFromFile = ( + options: useLoadWorkflowFromFileOptions +) => (file: File | null) => void; + +export const useLoadWorkflowFromFile: UseLoadWorkflowFromFile = ({ + resetRef, +}) => { const dispatch = useAppDispatch(); const logger = useLogger('nodes'); const { t } = useTranslation(); @@ -41,6 +48,7 @@ export const useLoadWorkflowFromFile = (resetRef: RefObject<() => void>) => { }; reader.readAsText(file); + // Reset the file picker internal state so that the same file can be loaded again resetRef.current?.(); }, @@ -49,24 +57,3 @@ export const useLoadWorkflowFromFile = (resetRef: RefObject<() => void>) => { return loadWorkflowFromFile; }; - -const WorkflowValidationErrorContent = memo((props: { error: ZodError }) => { - if (props.error.issues[0]) { - return ( - - {fromZodIssue(props.error.issues[0], { prefix: null }).toString()} - - ); - } - return ( - - {props.error.issues.map((issue, i) => ( - - {fromZodIssue(issue, { prefix: null }).toString()} - - ))} - - ); -}); - -WorkflowValidationErrorContent.displayName = 'WorkflowValidationErrorContent'; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts index 9e164bf4e8..6255fde8b4 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts @@ -10,7 +10,15 @@ import { useUpdateWorkflowMutation, } from 'services/api/endpoints/workflows'; -export const useSaveLibraryWorkflow = () => { +type UseSaveLibraryWorkflowReturn = { + saveWorkflow: () => Promise; + isLoading: boolean; + isError: boolean; +}; + +type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn; + +export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const workflow = useWorkflow(); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDuplicateWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts similarity index 81% rename from invokeai/frontend/web/src/features/workflowLibrary/hooks/useDuplicateWorkflow.ts rename to invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts index 20b3fc980a..8d5aab18eb 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDuplicateWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts @@ -7,20 +7,28 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useCreateWorkflowMutation } from 'services/api/endpoints/workflows'; -type Arg = { +type SaveWorkflowAsArg = { name: string; onSuccess?: () => void; onError?: () => void; }; -export const useSaveWorkflowAs = () => { +type UseSaveWorkflowAsReturn = { + saveWorkflowAs: (arg: SaveWorkflowAsArg) => Promise; + isLoading: boolean; + isError: boolean; +}; + +type UseSaveWorkflowAs = () => UseSaveWorkflowAsReturn; + +export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const workflow = useWorkflow(); const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); const toaster = useAppToaster(); const saveWorkflowAs = useCallback( - async ({ name: newName, onSuccess, onError }: Arg) => { + async ({ name: newName, onSuccess, onError }: SaveWorkflowAsArg) => { try { workflow.id = undefined; workflow.name = newName;