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",
|
"newWorkflow": "New Workflow",
|
||||||
"newWorkflowDesc": "Create a new workflow?",
|
"newWorkflowDesc": "Create a new workflow?",
|
||||||
"newWorkflowDesc2": "Your current workflow has unsaved changes.",
|
"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",
|
"scheduler": "Scheduler",
|
||||||
"schedulerDescription": "TODO",
|
"schedulerDescription": "TODO",
|
||||||
"sDXLMainModelField": "SDXL Model",
|
"sDXLMainModelField": "SDXL Model",
|
||||||
@ -1698,6 +1701,7 @@
|
|||||||
"downloadWorkflow": "Save to File",
|
"downloadWorkflow": "Save to File",
|
||||||
"saveWorkflow": "Save Workflow",
|
"saveWorkflow": "Save Workflow",
|
||||||
"saveWorkflowAs": "Save Workflow As",
|
"saveWorkflowAs": "Save Workflow As",
|
||||||
|
"saveWorkflowToProject": "Save Workflow to Project",
|
||||||
"savingWorkflow": "Saving Workflow...",
|
"savingWorkflow": "Saving Workflow...",
|
||||||
"problemSavingWorkflow": "Problem Saving Workflow",
|
"problemSavingWorkflow": "Problem Saving Workflow",
|
||||||
"workflowSaved": "Workflow Saved",
|
"workflowSaved": "Workflow Saved",
|
||||||
@ -1712,6 +1716,7 @@
|
|||||||
"clearWorkflowSearchFilter": "Clear Workflow Search Filter",
|
"clearWorkflowSearchFilter": "Clear Workflow Search Filter",
|
||||||
"workflowName": "Workflow Name",
|
"workflowName": "Workflow Name",
|
||||||
"newWorkflowCreated": "New Workflow Created",
|
"newWorkflowCreated": "New Workflow Created",
|
||||||
|
"workflowCleared": "Workflow Cleared",
|
||||||
"workflowEditorMenu": "Workflow Editor Menu",
|
"workflowEditorMenu": "Workflow Editor Menu",
|
||||||
"workflowIsOpen": "Workflow is Open"
|
"workflowIsOpen": "Workflow is Open"
|
||||||
},
|
},
|
||||||
|
@ -23,9 +23,7 @@ export type AppFeature =
|
|||||||
| 'resumeQueue'
|
| 'resumeQueue'
|
||||||
| 'prependQueue'
|
| 'prependQueue'
|
||||||
| 'invocationCache'
|
| 'invocationCache'
|
||||||
| 'bulkDownload'
|
| 'bulkDownload';
|
||||||
| 'workflowLibrary';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A disable-able Stable Diffusion feature
|
* A disable-able Stable Diffusion feature
|
||||||
*/
|
*/
|
||||||
|
@ -4,6 +4,7 @@ import { Flex } from '@invoke-ai/ui-library';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
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 type { AnimationProps } from 'framer-motion';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
@ -59,6 +60,7 @@ const NodeEditor = () => {
|
|||||||
<TopPanel />
|
<TopPanel />
|
||||||
<BottomLeftPanel />
|
<BottomLeftPanel />
|
||||||
<MinimapPanel />
|
<MinimapPanel />
|
||||||
|
<SaveWorkflowAsDialog />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
@ -179,6 +179,7 @@ const AddNodePopover = () => {
|
|||||||
closeOnBlur={true}
|
closeOnBlur={true}
|
||||||
returnFocusOnClose={true}
|
returnFocusOnClose={true}
|
||||||
initialFocusRef={inputRef}
|
initialFocusRef={inputRef}
|
||||||
|
isLazy
|
||||||
>
|
>
|
||||||
<PopoverAnchor>
|
<PopoverAnchor>
|
||||||
<Flex position="absolute" top="15%" insetInlineStart="50%" pointerEvents="none" />
|
<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 { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
|
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 UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton';
|
||||||
import WorkflowName from 'features/nodes/components/flow/panels/TopPanel/WorkflowName';
|
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 WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||||
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const TopCenterPanel = () => {
|
const TopCenterPanel = () => {
|
||||||
const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} top={2} left={2} right={2} position="absolute" alignItems="center" pointerEvents="none">
|
<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 />
|
<AddNodeButton />
|
||||||
|
<WorkflowLibraryButton />
|
||||||
|
</Flex>
|
||||||
<UpdateNodesButton />
|
<UpdateNodesButton />
|
||||||
|
</Flex>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<WorkflowName />
|
<WorkflowName />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{isWorkflowLibraryEnabled && <WorkflowLibraryButton />}
|
<ClearFlowButton />
|
||||||
|
<SaveWorkflowButton />
|
||||||
<WorkflowLibraryMenu />
|
<WorkflowLibraryMenu />
|
||||||
</Flex>
|
</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 { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
|
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
|
||||||
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
||||||
@ -19,9 +19,13 @@ const UpdateNodesButton = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button leftIcon={<PiWarningBold />} onClick={handleClickUpdateNodes} pointerEvents="auto">
|
<IconButton
|
||||||
{t('nodes.updateAllNodes')}
|
tooltip={t('nodes.updateAllNodes')}
|
||||||
</Button>
|
aria-label={t('nodes.updateAllNodes')}
|
||||||
|
icon={<PiWarningBold />}
|
||||||
|
onClick={handleClickUpdateNodes}
|
||||||
|
pointerEvents="auto"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,26 +1,13 @@
|
|||||||
import { Text } from '@invoke-ai/ui-library';
|
import { Text } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { memo } from 'react';
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const TopCenterPanel = () => {
|
const TopCenterPanel = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const name = useAppSelector((s) => s.workflow.name);
|
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 (
|
return (
|
||||||
<Text m={2} fontSize="lg" userSelect="none" noOfLines={1} wordBreak="break-all" fontWeight="semibold" opacity={0.8}>
|
<Text m={2} fontSize="lg" userSelect="none" noOfLines={1} wordBreak="break-all" fontWeight="semibold" opacity={0.8}>
|
||||||
{displayName}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
|
||||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||||
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const TopRightPanel = () => {
|
const TopRightPanel = () => {
|
||||||
const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} position="absolute" top={2} insetInlineEnd={2}>
|
<Flex gap={2} position="absolute" top={2} insetInlineEnd={2}>
|
||||||
{isWorkflowLibraryEnabled && <WorkflowLibraryButton />}
|
<WorkflowLibraryButton />
|
||||||
<WorkflowLibraryMenu />
|
<WorkflowLibraryMenu />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,7 @@ import { workflowLoaded } from 'features/nodes/store/actions';
|
|||||||
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
||||||
import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
|
import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
|
||||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
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';
|
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
|
||||||
|
|
||||||
export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
|
export const blankWorkflow: Omit<WorkflowV2, 'nodes' | 'edges'> = {
|
||||||
@ -46,6 +46,11 @@ const workflowSlice = createSlice({
|
|||||||
state.name = action.payload;
|
state.name = action.payload;
|
||||||
state.isTouched = true;
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
|
workflowCategoryChanged: (state, action: PayloadAction<WorkflowCategory | undefined>) => {
|
||||||
|
if (action.payload) {
|
||||||
|
state.meta.category = action.payload;
|
||||||
|
}
|
||||||
|
},
|
||||||
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
|
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.description = action.payload;
|
state.description = action.payload;
|
||||||
state.isTouched = true;
|
state.isTouched = true;
|
||||||
@ -102,6 +107,7 @@ export const {
|
|||||||
workflowExposedFieldAdded,
|
workflowExposedFieldAdded,
|
||||||
workflowExposedFieldRemoved,
|
workflowExposedFieldRemoved,
|
||||||
workflowNameChanged,
|
workflowNameChanged,
|
||||||
|
workflowCategoryChanged,
|
||||||
workflowDescriptionChanged,
|
workflowDescriptionChanged,
|
||||||
workflowTagsChanged,
|
workflowTagsChanged,
|
||||||
workflowAuthorChanged,
|
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 { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -12,9 +12,13 @@ const WorkflowLibraryButton = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowLibraryModalContext.Provider value={disclosure}>
|
<WorkflowLibraryModalContext.Provider value={disclosure}>
|
||||||
<Button leftIcon={<PiBooksBold />} onClick={disclosure.onOpen} pointerEvents="auto">
|
<IconButton
|
||||||
{t('workflows.workflowLibrary')}
|
aria-label={t('workflows.workflowLibrary')}
|
||||||
</Button>
|
tooltip={t('workflows.workflowLibrary')}
|
||||||
|
icon={<PiBooksBold />}
|
||||||
|
onClick={disclosure.onOpen}
|
||||||
|
pointerEvents="auto"
|
||||||
|
/>
|
||||||
<WorkflowLibraryModal />
|
<WorkflowLibraryModal />
|
||||||
</WorkflowLibraryModalContext.Provider>
|
</WorkflowLibraryModalContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,53 +1,18 @@
|
|||||||
import { ConfirmationAlertDialog, FormControl, FormLabel, Input, MenuItem, useDisclosure } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog';
|
||||||
import { useSaveWorkflowAs } from 'features/workflowLibrary/hooks/useSaveWorkflowAs';
|
import { memo } from 'react';
|
||||||
import { getWorkflowCopyName } from 'features/workflowLibrary/util/getWorkflowCopyName';
|
|
||||||
import type { ChangeEvent } from 'react';
|
|
||||||
import { memo, useCallback, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCopyBold } from 'react-icons/pi';
|
import { PiCopyBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const SaveWorkflowAsButton = () => {
|
const SaveWorkflowAsMenuItem = () => {
|
||||||
const currentName = useAppSelector((s) => s.workflow.name);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { saveWorkflowAs } = useSaveWorkflowAs();
|
const { onOpen } = useSaveWorkflowAsDialog();
|
||||||
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);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MenuItem as="button" icon={<PiCopyBold />} onClick={onOpen}>
|
||||||
<MenuItem as="button" icon={<PiCopyBold />} onClick={onOpenCallback}>
|
|
||||||
{t('workflows.saveWorkflowAs')}
|
{t('workflows.saveWorkflowAs')}
|
||||||
</MenuItem>
|
</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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(SaveWorkflowAsButton);
|
export default memo(SaveWorkflowAsMenuItem);
|
||||||
|
@ -1,17 +1,37 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow';
|
import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog';
|
||||||
import { memo } from 'react';
|
import { isWorkflowWithID, useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiFloppyDiskBold } from 'react-icons/pi';
|
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 { t } = useTranslation();
|
||||||
const { saveWorkflow } = useSaveLibraryWorkflow();
|
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 (
|
return (
|
||||||
<MenuItem as="button" icon={<PiFloppyDiskBold />} onClick={saveWorkflow}>
|
<MenuItem as="button" isDisabled={!isTouched} icon={<PiFloppyDiskBold />} onClick={handleClickSave}>
|
||||||
{t('workflows.saveWorkflow')}
|
{t('workflows.saveWorkflow')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(SaveLibraryWorkflowMenuItem);
|
export default memo(SaveWorkflowMenuItem);
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
useGlobalMenuClose,
|
useGlobalMenuClose,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
|
||||||
import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
|
import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
|
||||||
import NewWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem';
|
import NewWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem';
|
||||||
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
|
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
|
||||||
@ -22,9 +21,6 @@ const WorkflowLibraryMenu = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
useGlobalMenuClose(onClose);
|
useGlobalMenuClose(onClose);
|
||||||
|
|
||||||
const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
@ -34,11 +30,12 @@ const WorkflowLibraryMenu = () => {
|
|||||||
pointerEvents="auto"
|
pointerEvents="auto"
|
||||||
/>
|
/>
|
||||||
<MenuList pointerEvents="auto">
|
<MenuList pointerEvents="auto">
|
||||||
{isWorkflowLibraryEnabled && <SaveWorkflowMenuItem />}
|
|
||||||
{isWorkflowLibraryEnabled && <SaveWorkflowAsMenuItem />}
|
|
||||||
<DownloadWorkflowMenuItem />
|
|
||||||
<UploadWorkflowMenuItem />
|
|
||||||
<NewWorkflowMenuItem />
|
<NewWorkflowMenuItem />
|
||||||
|
<UploadWorkflowMenuItem />
|
||||||
|
<MenuDivider />
|
||||||
|
<SaveWorkflowMenuItem />
|
||||||
|
<SaveWorkflowAsMenuItem />
|
||||||
|
<DownloadWorkflowMenuItem />
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<SettingsMenuItem />
|
<SettingsMenuItem />
|
||||||
</MenuList>
|
</MenuList>
|
||||||
|
@ -17,7 +17,8 @@ type UseSaveLibraryWorkflowReturn = {
|
|||||||
|
|
||||||
type UseSaveLibraryWorkflow = () => 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 = () => {
|
export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -2,13 +2,21 @@ import type { ToastId } from '@invoke-ai/ui-library';
|
|||||||
import { useToast } from '@invoke-ai/ui-library';
|
import { useToast } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher';
|
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 { useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCreateWorkflowMutation, workflowsApi } from 'services/api/endpoints/workflows';
|
import { useCreateWorkflowMutation, workflowsApi } from 'services/api/endpoints/workflows';
|
||||||
|
|
||||||
|
import type { WorkflowCategory } from '../../nodes/types/workflow';
|
||||||
|
|
||||||
type SaveWorkflowAsArg = {
|
type SaveWorkflowAsArg = {
|
||||||
name: string;
|
name: string;
|
||||||
|
category: WorkflowCategory;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
};
|
};
|
||||||
@ -28,7 +36,7 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const toastRef = useRef<ToastId | undefined>();
|
const toastRef = useRef<ToastId | undefined>();
|
||||||
const saveWorkflowAs = useCallback(
|
const saveWorkflowAs = useCallback(
|
||||||
async ({ name: newName, onSuccess, onError }: SaveWorkflowAsArg) => {
|
async ({ name: newName, category, onSuccess, onError }: SaveWorkflowAsArg) => {
|
||||||
const workflow = $builtWorkflow.get();
|
const workflow = $builtWorkflow.get();
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
return;
|
return;
|
||||||
@ -42,10 +50,14 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
|
|||||||
try {
|
try {
|
||||||
workflow.id = undefined;
|
workflow.id = undefined;
|
||||||
workflow.name = newName;
|
workflow.name = newName;
|
||||||
|
workflow.meta.category = category;
|
||||||
|
|
||||||
const data = await createWorkflow(workflow).unwrap();
|
const data = await createWorkflow(workflow).unwrap();
|
||||||
dispatch(workflowIDChanged(data.workflow.id));
|
dispatch(workflowIDChanged(data.workflow.id));
|
||||||
dispatch(workflowNameChanged(data.workflow.name));
|
dispatch(workflowNameChanged(data.workflow.name));
|
||||||
|
dispatch(workflowCategoryChanged(data.workflow.meta.category));
|
||||||
dispatch(workflowSaved());
|
dispatch(workflowSaved());
|
||||||
|
|
||||||
onSuccess && onSuccess();
|
onSuccess && onSuccess();
|
||||||
toast.update(toastRef.current, {
|
toast.update(toastRef.current, {
|
||||||
title: t('workflows.workflowSaved'),
|
title: t('workflows.workflowSaved'),
|
||||||
|
Loading…
Reference in New Issue
Block a user