From 83bfbdcad4bbf1ba7b41dc861c8c9b92018ffd26 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 12 Mar 2025 19:35:11 +1000
Subject: [PATCH] feat(ui): more workflow loading standardization
There is now a single entrypoint for loading a workflow - `useLoadWorkflowWithDialog`.
The hook:
Handles loading workflows from various sources. If there are unsaved changes, the user will be prompted to confirm before loading the workflow.
It returns a function that:
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.
WHEW
---
.../ImageMenuItemLoadWorkflow.tsx | 15 +-
.../features/gallery/hooks/useImageActions.ts | 13 +-
.../EditWorkflow.tsx | 17 ++-
.../ViewWorkflow.tsx | 17 ++-
.../WorkflowLibrarySideNav.tsx | 16 ++-
.../WorkflowLibrary/WorkflowListItem.tsx | 20 ++-
.../LoadWorkflowConfirmationAlertDialog.tsx | 133 +++++++++++-------
.../LoadWorkflowFromGraphModal.tsx | 8 +-
.../components/UploadWorkflowButton.tsx | 11 +-
.../UploadWorkflowMenuItem.tsx | 11 +-
.../hooks/useLoadWorkflowFromFile.tsx | 49 ++++---
.../hooks/useLoadWorkflowFromImage.ts | 18 ++-
.../hooks/useLoadWorkflowFromLibrary.ts | 11 +-
.../hooks/useLoadWorkflowFromObject.ts | 39 +++++
.../hooks/useValidateAndLoadWorkflow.ts | 15 ++
15 files changed, 270 insertions(+), 123 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromObject.ts
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();