mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into fix-civit-model-imports
This commit is contained in:
commit
ed466a99ec
@ -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"
|
||||
},
|
||||
|
@ -23,9 +23,7 @@ export type AppFeature =
|
||||
| 'resumeQueue'
|
||||
| 'prependQueue'
|
||||
| 'invocationCache'
|
||||
| 'bulkDownload'
|
||||
| 'workflowLibrary';
|
||||
|
||||
| 'bulkDownload';
|
||||
/**
|
||||
* A disable-able Stable Diffusion feature
|
||||
*/
|
||||
|
@ -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>
|
||||
|
@ -179,6 +179,7 @@ const AddNodePopover = () => {
|
||||
closeOnBlur={true}
|
||||
returnFocusOnClose={true}
|
||||
initialFocusRef={inputRef}
|
||||
isLazy
|
||||
>
|
||||
<PopoverAnchor>
|
||||
<Flex position="absolute" top="15%" insetInlineStart="50%" pointerEvents="none" />
|
||||
|
@ -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);
|
@ -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);
|
@ -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>
|
||||
);
|
||||
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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 };
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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'),
|
||||
|
Loading…
Reference in New Issue
Block a user