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" + /> ); }; 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" + /> ); 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 ( { pointerEvents="auto" /> - {isWorkflowLibraryEnabled && } - {isWorkflowLibraryEnabled && } - - + + + + + diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts index 15d0fc650e..78d7071c20 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts @@ -17,7 +17,8 @@ type UseSaveLibraryWorkflowReturn = { type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn; -const isWorkflowWithID = (workflow: WorkflowV2): workflow is O.Required => Boolean(workflow.id); +export const isWorkflowWithID = (workflow: WorkflowV2): workflow is O.Required => + Boolean(workflow.id); export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts index c24961b918..2bd7aabbe4 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts @@ -2,13 +2,21 @@ import type { ToastId } from '@invoke-ai/ui-library'; import { useToast } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; -import { workflowIDChanged, workflowNameChanged, workflowSaved } from 'features/nodes/store/workflowSlice'; +import { + workflowCategoryChanged, + workflowIDChanged, + workflowNameChanged, + workflowSaved, +} from 'features/nodes/store/workflowSlice'; import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useCreateWorkflowMutation, workflowsApi } from 'services/api/endpoints/workflows'; +import type { WorkflowCategory } from '../../nodes/types/workflow'; + type SaveWorkflowAsArg = { name: string; + category: WorkflowCategory; onSuccess?: () => void; onError?: () => void; }; @@ -28,7 +36,7 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { const toast = useToast(); const toastRef = useRef(); const saveWorkflowAs = useCallback( - async ({ name: newName, onSuccess, onError }: SaveWorkflowAsArg) => { + async ({ name: newName, category, onSuccess, onError }: SaveWorkflowAsArg) => { const workflow = $builtWorkflow.get(); if (!workflow) { return; @@ -42,10 +50,14 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { try { workflow.id = undefined; workflow.name = newName; + workflow.meta.category = category; + const data = await createWorkflow(workflow).unwrap(); dispatch(workflowIDChanged(data.workflow.id)); dispatch(workflowNameChanged(data.workflow.name)); + dispatch(workflowCategoryChanged(data.workflow.meta.category)); dispatch(workflowSaved()); + onSuccess && onSuccess(); toast.update(toastRef.current, { title: t('workflows.workflowSaved'),