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();