Merge branch 'main' into fix-civit-model-imports

This commit is contained in:
Brandon 2024-01-31 10:12:44 -05:00 committed by GitHub
commit ed466a99ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 362 additions and 98 deletions

View File

@ -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"
},

View File

@ -23,9 +23,7 @@ export type AppFeature =
| 'resumeQueue'
| 'prependQueue'
| 'invocationCache'
| 'bulkDownload'
| 'workflowLibrary';
| 'bulkDownload';
/**
* A disable-able Stable Diffusion feature
*/

View File

@ -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 = () => {
<TopPanel />
<BottomLeftPanel />
<MinimapPanel />
<SaveWorkflowAsDialog />
</motion.div>
)}
</AnimatePresence>

View File

@ -179,6 +179,7 @@ const AddNodePopover = () => {
closeOnBlur={true}
returnFocusOnClose={true}
initialFocusRef={inputRef}
isLazy
>
<PopoverAnchor>
<Flex position="absolute" top="15%" insetInlineStart="50%" pointerEvents="none" />

View File

@ -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 (
<>
<IconButton
tooltip={t('nodes.clearWorkflow')}
aria-label={t('nodes.clearWorkflow')}
icon={<PiTrashSimpleFill />}
onClick={onClick}
pointerEvents="auto"
/>
<ConfirmationAlertDialog
isOpen={isOpen}
onClose={onClose}
title={t('nodes.clearWorkflow')}
acceptCallback={handleNewWorkflow}
>
<Flex flexDir="column" gap={2}>
<Text>{t('nodes.clearWorkflowDesc')}</Text>
<Text variant="subtext">{t('nodes.clearWorkflowDesc2')}</Text>
</Flex>
</ConfirmationAlertDialog>
</>
);
};
export default memo(ClearFlowButton);

View File

@ -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 (
<IconButton
tooltip={t('workflows.saveWorkflow')}
aria-label={t('workflows.saveWorkflow')}
icon={<PiFloppyDiskBold />}
isDisabled={!isTouched}
onClick={handleClickSave}
pointerEvents="auto"
/>
);
};
export default memo(SaveWorkflowButton);

View File

@ -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 (
<Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="center" pointerEvents="none">
<AddNodeButton />
<UpdateNodesButton />
<Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="flex-start" pointerEvents="none">
<Flex flexDir="column" gap="2">
<Flex gap="2">
<AddNodeButton />
<WorkflowLibraryButton />
</Flex>
<UpdateNodesButton />
</Flex>
<Spacer />
<WorkflowName />
<Spacer />
{isWorkflowLibraryEnabled && <WorkflowLibraryButton />}
<ClearFlowButton />
<SaveWorkflowButton />
<WorkflowLibraryMenu />
</Flex>
);

View File

@ -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 (
<Button leftIcon={<PiWarningBold />} onClick={handleClickUpdateNodes} pointerEvents="auto">
{t('nodes.updateAllNodes')}
</Button>
<IconButton
tooltip={t('nodes.updateAllNodes')}
aria-label={t('nodes.updateAllNodes')}
icon={<PiWarningBold />}
onClick={handleClickUpdateNodes}
pointerEvents="auto"
/>
);
};

View File

@ -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 (
<Text m={2} fontSize="lg" userSelect="none" noOfLines={1} wordBreak="break-all" fontWeight="semibold" opacity={0.8}>
{displayName}
{name}
</Text>
);
};

View File

@ -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 (
<Flex gap={2} position="absolute" top={2} insetInlineEnd={2}>
{isWorkflowLibraryEnabled && <WorkflowLibraryButton />}
<WorkflowLibraryButton />
<WorkflowLibraryMenu />
</Flex>
);

View File

@ -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<WorkflowV2, 'nodes' | 'edges'> = {
@ -46,6 +46,11 @@ const workflowSlice = createSlice({
state.name = action.payload;
state.isTouched = true;
},
workflowCategoryChanged: (state, action: PayloadAction<WorkflowCategory | undefined>) => {
if (action.payload) {
state.meta.category = action.payload;
}
},
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
state.description = action.payload;
state.isTouched = true;
@ -102,6 +107,7 @@ export const {
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
workflowNameChanged,
workflowCategoryChanged,
workflowDescriptionChanged,
workflowTagsChanged,
workflowAuthorChanged,

View File

@ -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<HTMLButtonElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setWorkflowName(e.target.value);
},
[setWorkflowName]
);
const onChangeCheckbox = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
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 (
<AlertDialog isOpen={isOpen} onClose={onClose} leastDestructiveRef={cancelRef} isCentered={true}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{t('workflows.saveWorkflowAs')}
</AlertDialogHeader>
<AlertDialogBody>
<FormControl alignItems="flex-start">
<FormLabel mt="2">{t('workflows.workflowName')}</FormLabel>
<Flex flexDir="column" width="full" gap="2">
<Input
ref={inputRef}
value={workflowName}
onChange={onChange}
placeholder={t('workflows.workflowName')}
/>
{workflowCategories.includes('project') && (
<Checkbox isChecked={shouldSaveToProject} onChange={onChangeCheckbox}>
<FormLabel>{t('workflows.saveWorkflowToProject')}</FormLabel>
</Checkbox>
)}
</Flex>
</FormControl>
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={clearAndClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="invokeBlue" onClick={onSave} ml={3} isDisabled={!workflowName || !workflowName.length}>
{t('common.saveAs')}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
};

View File

@ -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 };
};

View File

@ -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 (
<WorkflowLibraryModalContext.Provider value={disclosure}>
<Button leftIcon={<PiBooksBold />} onClick={disclosure.onOpen} pointerEvents="auto">
{t('workflows.workflowLibrary')}
</Button>
<IconButton
aria-label={t('workflows.workflowLibrary')}
tooltip={t('workflows.workflowLibrary')}
icon={<PiBooksBold />}
onClick={disclosure.onOpen}
pointerEvents="auto"
/>
<WorkflowLibraryModal />
</WorkflowLibraryModalContext.Provider>
);

View File

@ -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<HTMLInputElement>(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<HTMLInputElement>) => {
setName(e.target.value);
}, []);
const { onOpen } = useSaveWorkflowAsDialog();
return (
<>
<MenuItem as="button" icon={<PiCopyBold />} onClick={onOpenCallback}>
{t('workflows.saveWorkflowAs')}
</MenuItem>
<ConfirmationAlertDialog
isOpen={isOpen}
onClose={onClose}
title={t('workflows.saveWorkflowAs')}
acceptCallback={onSave}
>
<FormControl>
<FormLabel>{t('workflows.workflowName')}</FormLabel>
<Input ref={inputRef} value={name} onChange={onChange} placeholder={t('workflows.workflowName')} />
</FormControl>
</ConfirmationAlertDialog>
</>
<MenuItem as="button" icon={<PiCopyBold />} onClick={onOpen}>
{t('workflows.saveWorkflowAs')}
</MenuItem>
);
};
export default memo(SaveWorkflowAsButton);
export default memo(SaveWorkflowAsMenuItem);

View File

@ -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 (
<MenuItem as="button" icon={<PiFloppyDiskBold />} onClick={saveWorkflow}>
<MenuItem as="button" isDisabled={!isTouched} icon={<PiFloppyDiskBold />} onClick={handleClickSave}>
{t('workflows.saveWorkflow')}
</MenuItem>
);
};
export default memo(SaveLibraryWorkflowMenuItem);
export default memo(SaveWorkflowMenuItem);

View File

@ -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 (
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
<MenuButton
@ -34,11 +30,12 @@ const WorkflowLibraryMenu = () => {
pointerEvents="auto"
/>
<MenuList pointerEvents="auto">
{isWorkflowLibraryEnabled && <SaveWorkflowMenuItem />}
{isWorkflowLibraryEnabled && <SaveWorkflowAsMenuItem />}
<DownloadWorkflowMenuItem />
<UploadWorkflowMenuItem />
<NewWorkflowMenuItem />
<UploadWorkflowMenuItem />
<MenuDivider />
<SaveWorkflowMenuItem />
<SaveWorkflowAsMenuItem />
<DownloadWorkflowMenuItem />
<MenuDivider />
<SettingsMenuItem />
</MenuList>

View File

@ -17,7 +17,8 @@ type UseSaveLibraryWorkflowReturn = {
type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn;
const isWorkflowWithID = (workflow: WorkflowV2): workflow is O.Required<WorkflowV2, 'id'> => Boolean(workflow.id);
export const isWorkflowWithID = (workflow: WorkflowV2): workflow is O.Required<WorkflowV2, 'id'> =>
Boolean(workflow.id);
export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
const { t } = useTranslation();

View File

@ -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<ToastId | undefined>();
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'),