diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow.tsx
index f39206811e..86bf6426ba 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow.tsx
@@ -1,9 +1,8 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
-import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
-import { useLoadWorkflowFromImage } from 'features/workflowLibrary/hooks/useLoadWorkflowFromImage';
+import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFlowArrowBold } from 'react-icons/pi';
@@ -11,19 +10,15 @@ import { PiFlowArrowBold } from 'react-icons/pi';
export const ImageMenuItemLoadWorkflow = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
- const [getAndLoadEmbeddedWorkflow, { isLoading }] = useLoadWorkflowFromImage();
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const hasTemplates = useStore($hasTemplates);
const onClick = useCallback(() => {
- getAndLoadEmbeddedWorkflow(imageDTO.image_name);
- }, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]);
+ loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
+ }, [loadWorkflowWithDialog, imageDTO.image_name]);
return (
- : }
- onClickCapture={onClick}
- isDisabled={!imageDTO.has_workflow || !hasTemplates}
- >
+ } onClickCapture={onClick} isDisabled={!imageDTO.has_workflow || !hasTemplates}>
{t('nodes.loadWorkflow')}
);
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
index 2f90766512..1ba22baf4d 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
@@ -17,7 +17,7 @@ import {
} from 'features/stylePresets/store/stylePresetSlice';
import { toast } from 'features/toast/toast';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
-import { useLoadWorkflowFromImage } from 'features/workflowLibrary/hooks/useLoadWorkflowFromImage';
+import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
@@ -147,14 +147,15 @@ export const useImageActions = (imageDTO: ImageDTO) => {
});
}, [metadata, imageDTO]);
- const [getAndLoadEmbeddedWorkflow] = useLoadWorkflowFromImage();
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
- const loadWorkflow = useCallback(() => {
+ const loadWorkflowFromImage = useCallback(() => {
if (!imageDTO.has_workflow || !hasTemplates) {
return;
}
- getAndLoadEmbeddedWorkflow(imageDTO.image_name);
- }, [getAndLoadEmbeddedWorkflow, hasTemplates, imageDTO.has_workflow, imageDTO.image_name]);
+
+ loadWorkflowWithDialog({ type: 'image', data: imageDTO.image_name });
+ }, [hasTemplates, imageDTO.has_workflow, imageDTO.image_name, loadWorkflowWithDialog]);
const recallSize = useCallback(() => {
if (isStaging) {
@@ -180,7 +181,7 @@ export const useImageActions = (imageDTO: ImageDTO) => {
recallSeed,
recallPrompts,
createAsPreset,
- loadWorkflow,
+ loadWorkflow: loadWorkflowFromImage,
hasWorkflow: imageDTO.has_workflow,
recallSize,
upscale,
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/EditWorkflow.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/EditWorkflow.tsx
index 06d21921b2..707a3f834c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/EditWorkflow.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/EditWorkflow.tsx
@@ -1,20 +1,29 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
-import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
+import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import type { MouseEvent } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPencilBold } from 'react-icons/pi';
export const EditWorkflow = ({ workflowId }: { workflowId: string }) => {
- const loadWorkflow = useLoadWorkflow();
+ const dispatch = useAppDispatch();
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const { t } = useTranslation();
const handleClickEdit = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
- loadWorkflow.loadWithDialog({ type: 'library', workflowId, mode: 'view' });
+ loadWorkflowWithDialog({
+ type: 'library',
+ data: workflowId,
+ onSuccess: () => {
+ dispatch(workflowModeChanged('edit'));
+ },
+ });
},
- [loadWorkflow, workflowId]
+ [dispatch, loadWorkflowWithDialog, workflowId]
);
return (
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/ViewWorkflow.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/ViewWorkflow.tsx
index 3e062ae41e..584ee535e5 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/ViewWorkflow.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/ViewWorkflow.tsx
@@ -1,20 +1,29 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
-import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
+import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import type { MouseEvent } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold } from 'react-icons/pi';
export const ViewWorkflow = ({ workflowId }: { workflowId: string }) => {
- const loadWorkflow = useLoadWorkflow();
+ const dispatch = useAppDispatch();
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const { t } = useTranslation();
const handleClickLoad = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
- loadWorkflow.loadWithDialog({ type: 'library', workflowId, mode: 'view' });
+ loadWorkflowWithDialog({
+ type: 'library',
+ data: workflowId,
+ onSuccess: () => {
+ dispatch(workflowModeChanged('view'));
+ },
+ });
},
- [loadWorkflow, workflowId]
+ [dispatch, loadWorkflowWithDialog, workflowId]
);
return (
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx
index deaa74e3b4..b83dc9f108 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx
@@ -13,7 +13,8 @@ import {
workflowLibraryTagToggled,
workflowLibraryViewChanged,
} from 'features/nodes/store/workflowLibrarySlice';
-import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
+import { workflowModeChanged } from 'features/nodes/store/workflowSlice';
+import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton';
import { UploadWorkflowButton } from 'features/workflowLibrary/components/UploadWorkflowButton';
import { memo, useCallback, useMemo } from 'react';
@@ -155,10 +156,17 @@ const useCountForTagCategory = (tagCategory: WorkflowTagCategory) => {
};
const RecentWorkflowButton = memo(({ workflow }: { workflow: S['WorkflowRecordListItemWithThumbnailDTO'] }) => {
- const loadWorkflow = useLoadWorkflow();
+ const dispatch = useAppDispatch();
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const load = useCallback(() => {
- loadWorkflow.loadWithDialog({ type: 'library', workflowId: workflow.workflow_id, mode: 'view' });
- }, [loadWorkflow, workflow.workflow_id]);
+ loadWorkflowWithDialog({
+ type: 'library',
+ data: workflow.workflow_id,
+ onSuccess: () => {
+ dispatch(workflowModeChanged('view'));
+ },
+ });
+ }, [dispatch, loadWorkflowWithDialog, workflow.workflow_id]);
return (
{
const { t } = useTranslation();
-
+ const dispatch = useAppDispatch();
const workflowId = useAppSelector(selectWorkflowId);
- const loadWorkflow = useLoadWorkflow();
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const isActive = useMemo(() => {
return workflowId === workflow.workflow_id;
}, [workflowId, workflow.workflow_id]);
const handleClickLoad = useCallback(() => {
- loadWorkflow.loadWithDialog({ type: 'library', workflowId: workflow.workflow_id, mode: 'view' });
- }, [loadWorkflow, workflow.workflow_id]);
+ loadWorkflowWithDialog({
+ type: 'library',
+ data: workflow.workflow_id,
+ onSuccess: () => {
+ dispatch(workflowModeChanged('view'));
+ },
+ });
+ }, [dispatch, loadWorkflowWithDialog, workflow.workflow_id]);
return (
void;
+ onError?: () => void;
+ onCompleted?: () => void;
+};
+
+type LoadLibraryWorkflowData = Callbacks & {
type: 'library';
- workflowId: string;
- mode: 'view' | 'edit';
+ data: string;
};
-type LoadDirectWorkflowData = {
+type LoadWorkflowFromObjectData = Callbacks & {
type: 'direct';
- workflow: WorkflowV3;
- mode: 'view' | 'edit';
+ data: WorkflowV3;
};
-type LoadFileWorkflowData = {
+type LoadWorkflowFromFileData = Callbacks & {
type: 'file';
- file: File;
- mode: 'view' | 'edit';
+ data: File;
+};
+
+type LoadWorkflowFromImageData = Callbacks & {
+ type: 'image';
+ data: string;
+};
+
+type DialogStateExtra = {
+ isOpen: boolean;
};
const $dialogState = atom<
- | (LoadLibraryWorkflowData & { isOpen: boolean })
- | (LoadDirectWorkflowData & { isOpen: boolean })
- | (LoadFileWorkflowData & { isOpen: boolean })
+ | (LoadLibraryWorkflowData & DialogStateExtra)
+ | (LoadWorkflowFromObjectData & DialogStateExtra)
+ | (LoadWorkflowFromFileData & DialogStateExtra)
+ | (LoadWorkflowFromImageData & DialogStateExtra)
| null
>(null);
const cleanup = () => $dialogState.set(null);
-export const useLoadWorkflow = () => {
- const dispatch = useAppDispatch();
+const useLoadImmediate = () => {
const workflowLibraryModal = useWorkflowLibraryModal();
const loadWorkflowFromLibrary = useLoadWorkflowFromLibrary();
const loadWorkflowFromFile = useLoadWorkflowFromFile();
- const validatedAndLoadWorkflow = useValidateAndLoadWorkflow();
-
- const isTouched = useAppSelector(selectWorkflowIsTouched);
+ const loadWorkflowFromImage = useLoadWorkflowFromImage();
+ const loadWorkflowFromObject = useLoadWorkflowFromObject();
const loadImmediate = useCallback(async () => {
- const data = $dialogState.get();
- if (!data) {
+ const dialogState = $dialogState.get();
+ if (!dialogState) {
return;
}
- if (data.type === 'direct') {
- const validatedWorkflow = await validatedAndLoadWorkflow(data.workflow);
- if (validatedWorkflow) {
- dispatch(workflowModeChanged(data.mode));
- }
- } else if (data.type === 'file') {
- await loadWorkflowFromFile(data.file, {
- onSuccess: () => {
- dispatch(workflowModeChanged(data.mode));
- },
- });
- } else {
- await loadWorkflowFromLibrary(data.workflowId, {
- onSuccess: () => {
- dispatch(workflowModeChanged(data.mode));
- },
- });
+ const { type, data, onSuccess, onError, onCompleted } = dialogState;
+ const options = {
+ onSuccess,
+ onError,
+ onCompleted,
+ };
+ if (type === 'direct') {
+ await loadWorkflowFromObject(data, options);
+ } else if (type === 'file') {
+ await loadWorkflowFromFile(data, options);
+ } else if (type === 'library') {
+ await loadWorkflowFromLibrary(data, options);
+ } else if (type === 'image') {
+ await loadWorkflowFromImage(data, options);
}
cleanup();
workflowLibraryModal.close();
- }, [dispatch, loadWorkflowFromFile, loadWorkflowFromLibrary, validatedAndLoadWorkflow, workflowLibraryModal]);
+ }, [
+ loadWorkflowFromFile,
+ loadWorkflowFromImage,
+ loadWorkflowFromLibrary,
+ loadWorkflowFromObject,
+ workflowLibraryModal,
+ ]);
- const loadWithDialog = useCallback(
- (data: LoadLibraryWorkflowData | LoadDirectWorkflowData | LoadFileWorkflowData) => {
+ return loadImmediate;
+};
+
+/**
+ * Handles loading workflows from various sources. If there are unsaved changes, the user will be prompted to confirm
+ * before loading the workflow.
+ */
+export const useLoadWorkflowWithDialog = () => {
+ const isTouched = useAppSelector(selectWorkflowIsTouched);
+ const loadImmediate = useLoadImmediate();
+
+ const loadWorkflowWithDialog = useCallback(
+ /**
+ * Loads a workflow from various sources. If there are unsaved changes, the user will be prompted to confirm before
+ * loading the workflow. The workflow will be loaded immediately if there are no unsaved changes. On success, error
+ * or completion, the corresponding callback will be called.
+ *
+ * @param data - The data to load the workflow from.
+ * @param data.type - The type of data to load the workflow from.
+ * @param data.data - The data to load the workflow from. The type of this data depends on the `type` field.
+ * @param data.onSuccess - A callback to call when the workflow is successfully loaded.
+ * @param data.onError - A callback to call when an error occurs while loading the workflow.
+ * @param data.onCompleted - A callback to call when the loading process is completed (both success and error).
+ */
+ (
+ data: LoadLibraryWorkflowData | LoadWorkflowFromObjectData | LoadWorkflowFromFileData | LoadWorkflowFromImageData
+ ) => {
if (!isTouched) {
$dialogState.set({ ...data, isOpen: false });
loadImmediate();
@@ -86,24 +126,21 @@ export const useLoadWorkflow = () => {
[loadImmediate, isTouched]
);
- return {
- loadImmediate,
- loadWithDialog,
- } as const;
+ return loadWorkflowWithDialog;
};
export const LoadWorkflowConfirmationAlertDialog = memo(() => {
useAssertSingleton('LoadWorkflowConfirmationAlertDialog');
const { t } = useTranslation();
const workflow = useStore($dialogState);
- const loadWorkflow = useLoadWorkflow();
+ const loadImmediate = useLoadImmediate();
return (
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx
index f12d56ae61..3374b78d45 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx
@@ -15,7 +15,7 @@ import {
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow';
-import { useValidateAndLoadWorkflow } from 'features/workflowLibrary/hooks/useValidateAndLoadWorkflow';
+import { useLoadWorkflowFromObject } from 'features/workflowLibrary/hooks/useLoadWorkflowFromObject';
import { atom } from 'nanostores';
import type { ChangeEvent } from 'react';
import { useCallback, useState } from 'react';
@@ -37,8 +37,8 @@ export const useLoadWorkflowFromGraphModal = () => {
export const LoadWorkflowFromGraphModal = () => {
const { t } = useTranslation();
- const validateAndLoadWorkflow = useValidateAndLoadWorkflow();
const { isOpen, onClose } = useLoadWorkflowFromGraphModal();
+ const loadWorkflowFromObject = useLoadWorkflowFromObject();
const [graphRaw, setGraphRaw] = useState('');
const [unvalidatedWorkflow, setUnvalidatedWorkflow] = useState('');
const [shouldAutoLayout, setShouldAutoLayout] = useState(true);
@@ -57,9 +57,9 @@ export const LoadWorkflowFromGraphModal = () => {
setUnvalidatedWorkflow(JSON.stringify(workflow, null, 2));
}, [graphRaw, shouldAutoLayout]);
const loadWorkflow = useCallback(async () => {
- await validateAndLoadWorkflow(unvalidatedWorkflow);
+ await loadWorkflowFromObject(unvalidatedWorkflow);
onClose();
- }, [validateAndLoadWorkflow, onClose, unvalidatedWorkflow]);
+ }, [loadWorkflowFromObject, unvalidatedWorkflow, onClose]);
return (
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/UploadWorkflowButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/UploadWorkflowButton.tsx
index 3fe90f84e8..8173da91d1 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/UploadWorkflowButton.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/UploadWorkflowButton.tsx
@@ -1,5 +1,5 @@
import { Button } from '@invoke-ai/ui-library';
-import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
+import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { memo, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
@@ -7,20 +7,19 @@ import { PiUploadSimpleBold } from 'react-icons/pi';
export const UploadWorkflowButton = memo(() => {
const { t } = useTranslation();
- const loadWorkflow = useLoadWorkflow();
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const onDropAccepted = useCallback(
([file]: File[]) => {
if (!file) {
return;
}
- loadWorkflow.loadWithDialog({
+ loadWorkflowWithDialog({
type: 'file',
- file,
- mode: 'edit',
+ data: file,
});
},
- [loadWorkflow]
+ [loadWorkflowWithDialog]
);
const { getInputProps, getRootProps } = useDropzone({
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx
index cd0d8e7552..339d79bf4f 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx
@@ -1,5 +1,5 @@
import { MenuItem } from '@invoke-ai/ui-library';
-import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
+import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { memo, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
@@ -7,20 +7,19 @@ import { PiUploadSimpleBold } from 'react-icons/pi';
const UploadWorkflowMenuItem = () => {
const { t } = useTranslation();
- const loadWorkflow = useLoadWorkflow();
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
const onDropAccepted = useCallback(
([file]: File[]) => {
if (!file) {
return;
}
- loadWorkflow.loadWithDialog({
+ loadWorkflowWithDialog({
type: 'file',
- file,
- mode: 'edit',
+ data: file,
});
},
- [loadWorkflow]
+ [loadWorkflowWithDialog]
);
const { getRootProps, getInputProps } = useDropzone({
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx
index cf0c031a86..1fb59009a5 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx
@@ -4,6 +4,12 @@ import { useValidateAndLoadWorkflow } from 'features/workflowLibrary/hooks/useVa
import { workflowLoadedFromFile } from 'features/workflowLibrary/store/actions';
import { useCallback } from 'react';
+/**
+ * Loads a workflow from a file.
+ *
+ * You probably should instead use `useLoadWorkflowWithDialog`, which opens a dialog to prevent loss of unsaved changes
+ * and handles the loading process.
+ */
export const useLoadWorkflowFromFile = () => {
const dispatch = useAppDispatch();
const validatedAndLoadWorkflow = useValidateAndLoadWorkflow();
@@ -13,30 +19,37 @@ export const useLoadWorkflowFromFile = () => {
options: {
onSuccess?: (workflow: WorkflowV3) => void;
onError?: () => void;
+ onCompleted?: () => void;
} = {}
) => {
- const reader = new FileReader();
- reader.onload = async () => {
- const rawJSON = reader.result;
- const { onSuccess, onError } = options;
- try {
- const unvalidatedWorkflow = JSON.parse(rawJSON as string);
- const validatedWorkflow = await validatedAndLoadWorkflow(unvalidatedWorkflow);
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = async () => {
+ const rawJSON = reader.result;
+ const { onSuccess, onError, onCompleted } = options;
+ try {
+ const unvalidatedWorkflow = JSON.parse(rawJSON as string);
+ const validatedWorkflow = await validatedAndLoadWorkflow(unvalidatedWorkflow);
- if (!validatedWorkflow) {
- reader.abort();
+ if (!validatedWorkflow) {
+ reader.abort();
+ onError?.();
+ return;
+ }
+ dispatch(workflowLoadedFromFile());
+ onSuccess?.(validatedWorkflow);
+ resolve(validatedWorkflow);
+ } catch {
+ // This is catching the error from the parsing the JSON file
onError?.();
- return;
+ reject();
+ } finally {
+ onCompleted?.();
}
- dispatch(workflowLoadedFromFile());
- onSuccess?.(validatedWorkflow);
- } catch {
- // This is catching the error from the parsing the JSON file
- onError?.();
- }
- };
+ };
- reader.readAsText(file);
+ reader.readAsText(file);
+ });
},
[validatedAndLoadWorkflow, dispatch]
);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromImage.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromImage.ts
index 800622d495..448275c785 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromImage.ts
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromImage.ts
@@ -8,19 +8,26 @@ import { useLazyGetImageWorkflowQuery } from 'services/api/endpoints/images';
import type { NonNullableGraph } from 'services/api/types';
import { assert } from 'tsafe';
+/**
+ * Loads a workflow from an image.
+ *
+ * You probably should instead use `useLoadWorkflowWithDialog`, which opens a dialog to prevent loss of unsaved changes
+ * and handles the loading process.
+ */
export const useLoadWorkflowFromImage = () => {
const { t } = useTranslation();
- const [getWorkflowAndGraphFromImage, result] = useLazyGetImageWorkflowQuery();
+ const [getWorkflowAndGraphFromImage] = useLazyGetImageWorkflowQuery();
const validateAndLoadWorkflow = useValidateAndLoadWorkflow();
- const getAndLoadEmbeddedWorkflow = useCallback(
+ const loadWorkflowFromImage = useCallback(
async (
imageName: string,
options: {
onSuccess?: (workflow: WorkflowV3) => void;
onError?: () => void;
+ onCompleted?: () => void;
} = {}
) => {
- const { onSuccess, onError } = options;
+ const { onSuccess, onError, onCompleted } = options;
try {
const { workflow, graph } = await getWorkflowAndGraphFromImage(imageName).unwrap();
@@ -52,11 +59,12 @@ export const useLoadWorkflowFromImage = () => {
status: 'error',
});
onError?.();
- return;
+ } finally {
+ onCompleted?.();
}
},
[getWorkflowAndGraphFromImage, validateAndLoadWorkflow, t]
);
- return [getAndLoadEmbeddedWorkflow, result] as const;
+ return loadWorkflowFromImage;
};
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromLibrary.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromLibrary.ts
index 8ce0479486..81fad78872 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromLibrary.ts
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromLibrary.ts
@@ -5,6 +5,12 @@ import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useLazyGetWorkflowQuery, useUpdateOpenedAtMutation, workflowsApi } from 'services/api/endpoints/workflows';
+/**
+ * Loads a workflow from the library.
+ *
+ * You probably should instead use `useLoadWorkflowWithDialog`, which opens a dialog to prevent loss of unsaved changes
+ * and handles the loading process.
+ */
export const useLoadWorkflowFromLibrary = () => {
const toast = useToast();
const { t } = useTranslation();
@@ -17,9 +23,10 @@ export const useLoadWorkflowFromLibrary = () => {
options: {
onSuccess?: (workflow: WorkflowV3) => void;
onError?: () => void;
+ onCompleted?: () => void;
} = {}
) => {
- const { onSuccess, onError } = options;
+ const { onSuccess, onError, onCompleted } = options;
try {
const res = await getWorkflow(workflowId).unwrap();
@@ -39,6 +46,8 @@ export const useLoadWorkflowFromLibrary = () => {
status: 'error',
});
onError?.();
+ } finally {
+ onCompleted?.();
}
},
[getWorkflow, validateAndLoadWorkflow, updateOpenedAt, toast, t]
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromObject.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromObject.ts
new file mode 100644
index 0000000000..b185182d30
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromObject.ts
@@ -0,0 +1,39 @@
+import type { WorkflowV3 } from 'features/nodes/types/workflow';
+import { useValidateAndLoadWorkflow } from 'features/workflowLibrary/hooks/useValidateAndLoadWorkflow';
+import { useCallback } from 'react';
+
+/**
+ * Loads a workflow from an object.
+ *
+ * You probably should instead use `useLoadWorkflowWithDialog`, which opens a dialog to prevent loss of unsaved changes
+ * and handles the loading process.
+ */
+export const useLoadWorkflowFromObject = () => {
+ const validateAndLoadWorkflow = useValidateAndLoadWorkflow();
+ const loadWorkflowFromObject = useCallback(
+ async (
+ unvalidatedWorkflow: unknown,
+ options: {
+ onSuccess?: (workflow: WorkflowV3) => void;
+ onError?: () => void;
+ onCompleted?: () => void;
+ } = {}
+ ) => {
+ const { onSuccess, onError, onCompleted } = options;
+ try {
+ const validatedWorkflow = await validateAndLoadWorkflow(unvalidatedWorkflow);
+
+ if (!validatedWorkflow) {
+ onError?.();
+ return;
+ }
+ onSuccess?.(validatedWorkflow);
+ } finally {
+ onCompleted?.();
+ }
+ },
+ [validateAndLoadWorkflow]
+ );
+
+ return loadWorkflowFromObject;
+};
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts
index f16952c1d1..de0cd927d8 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts
@@ -17,6 +17,21 @@ import { fromZodError } from 'zod-validation-error';
const log = logger('workflows');
+/**
+ * This hook manages the lower-level workflow validation and loading process.
+ *
+ * You probably should instead use `useLoadWorkflowWithDialog`, which opens a dialog to prevent loss of unsaved changes
+ * and handles the loading process.
+ *
+ * Internally, `useLoadWorkflowWithDialog` uses these hooks...
+ *
+ * - `useLoadWorkflowFromFile`
+ * - `useLoadWorkflowFromImage`
+ * - `useLoadWorkflowFromLibrary`
+ * - `useLoadWorkflowFromObject`
+ *
+ * ...each of which internally uses hook.
+ */
export const useValidateAndLoadWorkflow = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();