diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 57b69199df..3df56c10ac 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1014,6 +1014,9 @@
"newWorkflow": "New Workflow",
"newWorkflowDesc": "Create a new workflow?",
"newWorkflowDesc2": "Your current workflow has unsaved changes.",
+ "clearWorkflow": "Clear Workflow",
+ "clearWorkflowDesc": "Clear this workflow and start a new one?",
+ "clearWorkflowDesc2": "Your current workflow has unsaved changes.",
"scheduler": "Scheduler",
"schedulerDescription": "TODO",
"sDXLMainModelField": "SDXL Model",
@@ -1698,6 +1701,7 @@
"downloadWorkflow": "Save to File",
"saveWorkflow": "Save Workflow",
"saveWorkflowAs": "Save Workflow As",
+ "saveWorkflowToProject": "Save Workflow to Project",
"savingWorkflow": "Saving Workflow...",
"problemSavingWorkflow": "Problem Saving Workflow",
"workflowSaved": "Workflow Saved",
@@ -1712,6 +1716,7 @@
"clearWorkflowSearchFilter": "Clear Workflow Search Filter",
"workflowName": "Workflow Name",
"newWorkflowCreated": "New Workflow Created",
+ "workflowCleared": "Workflow Cleared",
"workflowEditorMenu": "Workflow Editor Menu",
"workflowIsOpen": "Workflow is Open"
},
diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts
index faee66a9a9..d511812cb4 100644
--- a/invokeai/frontend/web/src/app/types/invokeai.ts
+++ b/invokeai/frontend/web/src/app/types/invokeai.ts
@@ -23,9 +23,7 @@ export type AppFeature =
| 'resumeQueue'
| 'prependQueue'
| 'invocationCache'
- | 'bulkDownload'
- | 'workflowLibrary';
-
+ | 'bulkDownload';
/**
* A disable-able Stable Diffusion feature
*/
diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
index 501d263c38..bb801b6f39 100644
--- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
@@ -4,6 +4,7 @@ import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
+import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import type { CSSProperties } from 'react';
@@ -59,6 +60,7 @@ const NodeEditor = () => {
+
)}
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx
index faa4c0b054..b24b52c6ab 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx
@@ -179,6 +179,7 @@ const AddNodePopover = () => {
closeOnBlur={true}
returnFocusOnClose={true}
initialFocusRef={inputRef}
+ isLazy
>
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx
new file mode 100644
index 0000000000..3ea6c54c2c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx
@@ -0,0 +1,64 @@
+import { ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiTrashSimpleFill } from 'react-icons/pi';
+
+import { addToast } from '../../../../../system/store/systemSlice';
+import { makeToast } from '../../../../../system/util/makeToast';
+import { nodeEditorReset } from '../../../../store/nodesSlice';
+
+const ClearFlowButton = () => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const isTouched = useAppSelector((s) => s.workflow.isTouched);
+
+ const handleNewWorkflow = useCallback(() => {
+ dispatch(nodeEditorReset());
+
+ dispatch(
+ addToast(
+ makeToast({
+ title: t('workflows.workflowCleared'),
+ status: 'success',
+ })
+ )
+ );
+
+ onClose();
+ }, [dispatch, onClose, t]);
+
+ const onClick = useCallback(() => {
+ if (!isTouched) {
+ handleNewWorkflow();
+ return;
+ }
+ onOpen();
+ }, [handleNewWorkflow, isTouched, onOpen]);
+
+ return (
+ <>
+ }
+ onClick={onClick}
+ pointerEvents="auto"
+ />
+
+
+ {t('nodes.clearWorkflowDesc')}
+ {t('nodes.clearWorkflowDesc2')}
+
+
+ >
+ );
+};
+
+export default memo(ClearFlowButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx
new file mode 100644
index 0000000000..2d0abf3af5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx
@@ -0,0 +1,42 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiFloppyDiskBold } from 'react-icons/pi';
+
+import { isWorkflowWithID, useSaveLibraryWorkflow } from '../../../../../workflowLibrary/hooks/useSaveWorkflow';
+import { $builtWorkflow } from '../../../../hooks/useWorkflowWatcher';
+
+const SaveWorkflowButton = () => {
+ const { t } = useTranslation();
+ const isTouched = useAppSelector((s) => s.workflow.isTouched);
+ const { onOpen } = useSaveWorkflowAsDialog();
+ const { saveWorkflow } = useSaveLibraryWorkflow();
+
+ const handleClickSave = useCallback(async () => {
+ const builtWorkflow = $builtWorkflow.get();
+ if (!builtWorkflow) {
+ return;
+ }
+
+ if (isWorkflowWithID(builtWorkflow)) {
+ saveWorkflow();
+ } else {
+ onOpen();
+ }
+ }, [onOpen, saveWorkflow]);
+
+ return (
+ }
+ isDisabled={!isTouched}
+ onClick={handleClickSave}
+ pointerEvents="auto"
+ />
+ );
+};
+
+export default memo(SaveWorkflowButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx
index 442b0d33b8..c87af124bf 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx
@@ -1,23 +1,28 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
+import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
+import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton';
import WorkflowName from 'features/nodes/components/flow/panels/TopPanel/WorkflowName';
-import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
import { memo } from 'react';
const TopCenterPanel = () => {
- const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled;
-
return (
-
-
-
+
+
+
+
+
+
+
+
- {isWorkflowLibraryEnabled && }
+
+
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx
index 300eb19396..d356eaa4e1 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx
@@ -1,4 +1,4 @@
-import { Button } from '@invoke-ai/ui-library';
+import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
@@ -19,9 +19,13 @@ const UpdateNodesButton = () => {
}
return (
- } onClick={handleClickUpdateNodes} pointerEvents="auto">
- {t('nodes.updateAllNodes')}
-
+ }
+ onClick={handleClickUpdateNodes}
+ pointerEvents="auto"
+ />
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx
index 5573e89270..527147c67d 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx
@@ -1,26 +1,13 @@
import { Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
-import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
-import { memo, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
+import { memo } from 'react';
const TopCenterPanel = () => {
- const { t } = useTranslation();
const name = useAppSelector((s) => s.workflow.name);
- const isTouched = useAppSelector((s) => s.workflow.isTouched);
- const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled;
-
- const displayName = useMemo(() => {
- let _displayName = name || t('workflows.unnamedWorkflow');
- if (isTouched && isWorkflowLibraryEnabled) {
- _displayName += ` (${t('common.unsaved')})`;
- }
- return _displayName;
- }, [t, name, isTouched, isWorkflowLibraryEnabled]);
return (
- {displayName}
+ {name}
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx
index f3c903bf4a..be939f35bd 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx
@@ -1,15 +1,12 @@
import { Flex } from '@invoke-ai/ui-library';
-import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
import { memo } from 'react';
const TopRightPanel = () => {
- const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled;
-
return (
- {isWorkflowLibraryEnabled && }
+
);
diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
index 5f6859c623..f6ffa20f13 100644
--- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
@@ -5,7 +5,7 @@ import { workflowLoaded } from 'features/nodes/store/actions';
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
import type { FieldIdentifier } from 'features/nodes/types/field';
-import type { WorkflowV2 } from 'features/nodes/types/workflow';
+import type { WorkflowCategory, WorkflowV2 } from 'features/nodes/types/workflow';
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
export const blankWorkflow: Omit = {
@@ -46,6 +46,11 @@ const workflowSlice = createSlice({
state.name = action.payload;
state.isTouched = true;
},
+ workflowCategoryChanged: (state, action: PayloadAction) => {
+ if (action.payload) {
+ state.meta.category = action.payload;
+ }
+ },
workflowDescriptionChanged: (state, action: PayloadAction) => {
state.description = action.payload;
state.isTouched = true;
@@ -102,6 +107,7 @@ export const {
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
workflowNameChanged,
+ workflowCategoryChanged,
workflowDescriptionChanged,
workflowTagsChanged,
workflowAuthorChanged,
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog.tsx
new file mode 100644
index 0000000000..74a8916475
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog.tsx
@@ -0,0 +1,102 @@
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ Button,
+ Checkbox,
+ Flex,
+ FormControl,
+ FormLabel,
+ Input,
+} from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog';
+import { t } from 'i18next';
+import type { ChangeEvent } from 'react';
+import { useCallback, useRef } from 'react';
+
+import { $workflowCategories } from '../../../../app/store/nanostores/workflowCategories';
+import { useSaveWorkflowAs } from '../../hooks/useSaveWorkflowAs';
+
+export const SaveWorkflowAsDialog = () => {
+ const { isOpen, onClose, workflowName, setWorkflowName, shouldSaveToProject, setShouldSaveToProject } =
+ useSaveWorkflowAsDialog();
+
+ const workflowCategories = useStore($workflowCategories);
+
+ const { saveWorkflowAs } = useSaveWorkflowAs();
+
+ const cancelRef = useRef(null);
+ const inputRef = useRef(null);
+
+ const onChange = useCallback(
+ (e: ChangeEvent) => {
+ setWorkflowName(e.target.value);
+ },
+ [setWorkflowName]
+ );
+
+ const onChangeCheckbox = useCallback(
+ (e: ChangeEvent) => {
+ setShouldSaveToProject(e.target.checked);
+ },
+ [setShouldSaveToProject]
+ );
+
+ const clearAndClose = useCallback(() => {
+ onClose();
+ }, [onClose]);
+
+ const onSave = useCallback(async () => {
+ const category = shouldSaveToProject ? 'project' : 'user';
+ await saveWorkflowAs({
+ name: workflowName,
+ category,
+ onSuccess: clearAndClose,
+ onError: clearAndClose,
+ });
+ }, [workflowName, saveWorkflowAs, shouldSaveToProject, clearAndClose]);
+
+ return (
+
+
+
+
+ {t('workflows.saveWorkflowAs')}
+
+
+
+
+ {t('workflows.workflowName')}
+
+
+ {workflowCategories.includes('project') && (
+
+ {t('workflows.saveWorkflowToProject')}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog.ts b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog.ts
new file mode 100644
index 0000000000..78f3e3f537
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog.ts
@@ -0,0 +1,52 @@
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
+import { getWorkflowCopyName } from 'features/workflowLibrary/util/getWorkflowCopyName';
+import { atom } from 'nanostores';
+import { useCallback } from 'react';
+
+const $isOpen = atom(false);
+const $workflowName = atom('');
+const $shouldSaveToProject = atom(false);
+
+const selectNewWorkflowName = createSelector(selectWorkflowSlice, ({ name, id }): string => {
+ // If the workflow has no ID, it's a new workflow that has never been saved to the server. The dialog should use
+ // whatever the user has entered in the workflow name field.
+ if (!id) {
+ return name;
+ }
+ // Else, the workflow is already saved to the server. The dialog should use the workflow's name with " (copy)"
+ // appended to it.
+ if (name.length) {
+ return getWorkflowCopyName(name);
+ }
+ // Else, we have a workflow that has been saved to the server, but has no name. This should never happen, but if
+ // it does, we just return an empty string and let the dialog use the default name.
+ return '';
+});
+
+export const useSaveWorkflowAsDialog = () => {
+ const newWorkflowName = useAppSelector(selectNewWorkflowName);
+
+ const isOpen = useStore($isOpen);
+ const onOpen = useCallback(() => {
+ $workflowName.set(newWorkflowName);
+ $isOpen.set(true);
+ }, [newWorkflowName]);
+ const onClose = useCallback(() => {
+ $isOpen.set(false);
+ $workflowName.set('');
+ $shouldSaveToProject.set(false);
+ }, []);
+
+ const workflowName = useStore($workflowName);
+ const setWorkflowName = useCallback((workflowName: string) => $workflowName.set(workflowName), []);
+
+ const shouldSaveToProject = useStore($shouldSaveToProject);
+ const setShouldSaveToProject = useCallback((shouldSaveToProject: boolean) => {
+ $shouldSaveToProject.set(shouldSaveToProject);
+ }, []);
+
+ return { workflowName, setWorkflowName, shouldSaveToProject, setShouldSaveToProject, isOpen, onOpen, onClose };
+};
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx
index f128505dbe..33c3cee2bb 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx
@@ -1,4 +1,4 @@
-import { Button, useDisclosure } from '@invoke-ai/ui-library';
+import { IconButton, useDisclosure } from '@invoke-ai/ui-library';
import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -12,9 +12,13 @@ const WorkflowLibraryButton = () => {
return (
- } onClick={disclosure.onOpen} pointerEvents="auto">
- {t('workflows.workflowLibrary')}
-
+ }
+ onClick={disclosure.onOpen}
+ pointerEvents="auto"
+ />
);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx
index 55add16267..bd8a909ace 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx
@@ -1,53 +1,18 @@
-import { ConfirmationAlertDialog, FormControl, FormLabel, Input, MenuItem, useDisclosure } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
-import { useSaveWorkflowAs } from 'features/workflowLibrary/hooks/useSaveWorkflowAs';
-import { getWorkflowCopyName } from 'features/workflowLibrary/util/getWorkflowCopyName';
-import type { ChangeEvent } from 'react';
-import { memo, useCallback, useRef, useState } from 'react';
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog';
+import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
-const SaveWorkflowAsButton = () => {
- const currentName = useAppSelector((s) => s.workflow.name);
+const SaveWorkflowAsMenuItem = () => {
const { t } = useTranslation();
- const { saveWorkflowAs } = useSaveWorkflowAs();
- const [name, setName] = useState(getWorkflowCopyName(currentName));
- const { isOpen, onOpen, onClose } = useDisclosure();
- const inputRef = useRef(null);
-
- const onOpenCallback = useCallback(() => {
- setName(getWorkflowCopyName(currentName));
- onOpen();
- inputRef.current?.focus();
- }, [currentName, onOpen]);
-
- const onSave = useCallback(async () => {
- saveWorkflowAs({ name, onSuccess: onClose, onError: onClose });
- }, [name, onClose, saveWorkflowAs]);
-
- const onChange = useCallback((e: ChangeEvent) => {
- setName(e.target.value);
- }, []);
+ const { onOpen } = useSaveWorkflowAsDialog();
return (
- <>
- } onClick={onOpenCallback}>
- {t('workflows.saveWorkflowAs')}
-
-
-
-
- {t('workflows.workflowName')}
-
-
-
- >
+ } onClick={onOpen}>
+ {t('workflows.saveWorkflowAs')}
+
);
};
-export default memo(SaveWorkflowAsButton);
+export default memo(SaveWorkflowAsMenuItem);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx
index 1e2866cb77..3fc7cee257 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx
@@ -1,17 +1,37 @@
import { MenuItem } from '@invoke-ai/ui-library';
-import { useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow';
-import { memo } from 'react';
+import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog';
+import { isWorkflowWithID, useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow';
+import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFloppyDiskBold } from 'react-icons/pi';
-const SaveLibraryWorkflowMenuItem = () => {
+import { useAppSelector } from '../../../../app/store/storeHooks';
+import { $builtWorkflow } from '../../../nodes/hooks/useWorkflowWatcher';
+
+const SaveWorkflowMenuItem = () => {
const { t } = useTranslation();
const { saveWorkflow } = useSaveLibraryWorkflow();
+ const { onOpen } = useSaveWorkflowAsDialog();
+ const isTouched = useAppSelector((s) => s.workflow.isTouched);
+
+ const handleClickSave = useCallback(async () => {
+ const builtWorkflow = $builtWorkflow.get();
+ if (!builtWorkflow) {
+ return;
+ }
+
+ if (isWorkflowWithID(builtWorkflow)) {
+ saveWorkflow();
+ } else {
+ onOpen();
+ }
+ }, [onOpen, saveWorkflow]);
+
return (
- } onClick={saveWorkflow}>
+ } onClick={handleClickSave}>
{t('workflows.saveWorkflow')}
);
};
-export default memo(SaveLibraryWorkflowMenuItem);
+export default memo(SaveWorkflowMenuItem);
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx
index 158eb73edc..73d0249d3d 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx
@@ -7,7 +7,6 @@ import {
useDisclosure,
useGlobalMenuClose,
} from '@invoke-ai/ui-library';
-import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
import NewWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem';
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
@@ -22,9 +21,6 @@ const WorkflowLibraryMenu = () => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
useGlobalMenuClose(onClose);
-
- const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled;
-
return (