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;