mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Workflow navigation & save-as (#5607)
* redo top panel of workflow editor * add checkbox option to save to project, integrate save-as flow into first time saving workflow * remove log * remove workflowLibrary as a feature that can be disabled * lint * feat(ui): make SaveWorkflowAsDialog a singleton Fixes an issue where the workflow name would erroneously be an empty string (which it should show the current workflow name). Also makes it easier to interact with this component. - Extract the dialog state to a hook - Render the dialog once in `<NodeEditor />` - Use the hook in the various buttons that should open the dialog - Fix a few wonkily named components (pre-existing issue) * fix(ui): when saving a never-before-saved workflow, do not append " (copy)" to the name * fix(ui): do not obscure workflow library button with add node popover This component is kinda janky :/ the popover content somehow renders invisibly over the button. I think it's related to the `<PopoverAnchor />. Need to redo this in the future, but for now, making the popover render lazily fixes this. --------- Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local> Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
This commit is contained in:
parent
f48a2c5fd2
commit
f68f8898c0
@ -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