mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): revise workflow editor buttons
- Add menu to top-right of editor, save/saveas/download/upload/reset/settings moved in here - Add workflow name to top-center
This commit is contained in:
parent
283bb73418
commit
e4f67628c0
@ -1627,7 +1627,7 @@
|
||||
},
|
||||
"workflows": {
|
||||
"workflows": "Workflows",
|
||||
"workflowLibrary": "Workflow Library",
|
||||
"workflowLibrary": "Library",
|
||||
"userWorkflows": "My Workflows",
|
||||
"defaultWorkflows": "Default Workflows",
|
||||
"openWorkflow": "Open Workflow",
|
||||
@ -1637,6 +1637,7 @@
|
||||
"downloadWorkflow": "Download Workflow",
|
||||
"saveWorkflow": "Save Workflow",
|
||||
"saveWorkflowAs": "Save Workflow As",
|
||||
"savingWorkflow": "Saving Workflow...",
|
||||
"problemSavingWorkflow": "Problem Saving Workflow",
|
||||
"workflowSaved": "Workflow Saved",
|
||||
"noRecentWorkflows": "No Recent Workflows",
|
||||
@ -1648,7 +1649,8 @@
|
||||
"searchWorkflows": "Search Workflows",
|
||||
"clearWorkflowSearchFilter": "Clear Workflow Search Filter",
|
||||
"workflowName": "Workflow Name",
|
||||
"workflowEditorReset": "Workflow Editor Reset"
|
||||
"workflowEditorReset": "Workflow Editor Reset",
|
||||
"workflowEditorMenu": "Workflow Editor Menu"
|
||||
},
|
||||
"app": {
|
||||
"storeNotInitialized": "Store is not initialized"
|
||||
|
@ -7,12 +7,10 @@ import { MdDeviceHub } from 'react-icons/md';
|
||||
import 'reactflow/dist/style.css';
|
||||
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
||||
import { Flow } from './flow/Flow';
|
||||
import TopLeftPanel from './flow/panels/TopLeftPanel/TopLeftPanel';
|
||||
import TopCenterPanel from './flow/panels/TopCenterPanel/TopCenterPanel';
|
||||
import TopRightPanel from './flow/panels/TopRightPanel/TopRightPanel';
|
||||
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
|
||||
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
||||
|
||||
const NodeEditor = () => {
|
||||
const isReady = useAppSelector((state) => state.nodes.isReady);
|
||||
@ -47,9 +45,7 @@ const NodeEditor = () => {
|
||||
>
|
||||
<Flow />
|
||||
<AddNodePopover />
|
||||
<TopLeftPanel />
|
||||
<TopCenterPanel />
|
||||
<TopRightPanel />
|
||||
<TopPanel />
|
||||
<BottomLeftPanel />
|
||||
<MinimapPanel />
|
||||
</motion.div>
|
||||
|
@ -3,6 +3,25 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
||||
import {
|
||||
connectionEnded,
|
||||
connectionMade,
|
||||
connectionStarted,
|
||||
edgeAdded,
|
||||
edgeChangeStarted,
|
||||
edgeDeleted,
|
||||
edgesChanged,
|
||||
edgesDeleted,
|
||||
nodesChanged,
|
||||
nodesDeleted,
|
||||
selectedAll,
|
||||
selectedEdgesChanged,
|
||||
selectedNodesChanged,
|
||||
selectionCopied,
|
||||
selectionPasted,
|
||||
viewportChanged,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
||||
import { bumpGlobalMenuCloseTrigger } from 'features/ui/store/uiSlice';
|
||||
import { MouseEvent, useCallback, useRef } from 'react';
|
||||
@ -25,25 +44,6 @@ import {
|
||||
ReactFlowProps,
|
||||
XYPosition,
|
||||
} from 'reactflow';
|
||||
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
||||
import {
|
||||
connectionEnded,
|
||||
connectionMade,
|
||||
connectionStarted,
|
||||
edgeAdded,
|
||||
edgeChangeStarted,
|
||||
edgeDeleted,
|
||||
edgesChanged,
|
||||
edgesDeleted,
|
||||
nodesChanged,
|
||||
nodesDeleted,
|
||||
selectedAll,
|
||||
selectedEdgesChanged,
|
||||
selectedNodesChanged,
|
||||
selectionCopied,
|
||||
selectionPasted,
|
||||
viewportChanged,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
|
||||
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
|
||||
import InvocationDefaultEdge from './edges/InvocationDefaultEdge';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import { Flex, Heading } from '@chakra-ui/layout';
|
||||
import { memo } from 'react';
|
||||
import DownloadWorkflowButton from 'features/workflowLibrary/components/DownloadWorkflowButton';
|
||||
import UploadWorkflowButton from 'features/workflowLibrary/components/LoadWorkflowFromFileButton';
|
||||
@ -6,10 +6,14 @@ import ResetWorkflowEditorButton from 'features/workflowLibrary/components/Reset
|
||||
import SaveWorkflowButton from 'features/workflowLibrary/components/SaveWorkflowButton';
|
||||
import SaveWorkflowAsButton from 'features/workflowLibrary/components/SaveWorkflowAsButton';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
const isWorkflowLibraryEnabled =
|
||||
useFeatureStatus('workflowLibrary').isFeatureEnabled;
|
||||
const name = useAppSelector(
|
||||
(state) => state.workflow.name || 'Untitled Workflow'
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -19,9 +23,21 @@ const TopCenterPanel = () => {
|
||||
top: 2,
|
||||
insetInlineStart: '50%',
|
||||
transform: 'translate(-50%)',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<DownloadWorkflowButton />
|
||||
<Heading
|
||||
m={2}
|
||||
size="md"
|
||||
userSelect="none"
|
||||
pointerEvents="none"
|
||||
noOfLines={1}
|
||||
wordBreak="break-all"
|
||||
maxW={80}
|
||||
>
|
||||
{name}
|
||||
</Heading>
|
||||
{/* <DownloadWorkflowButton />
|
||||
<UploadWorkflowButton />
|
||||
{isWorkflowLibraryEnabled && (
|
||||
<>
|
||||
@ -29,7 +45,7 @@ const TopCenterPanel = () => {
|
||||
<SaveWorkflowAsButton />
|
||||
</>
|
||||
)}
|
||||
<ResetWorkflowEditorButton />
|
||||
<ResetWorkflowEditorButton /> */}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
|
||||
const AddNodeButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const handleOpenAddNodePopover = useCallback(() => {
|
||||
dispatch(addNodePopoverOpened());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip={t('nodes.addNodeToolTip')}
|
||||
aria-label={t('nodes.addNode')}
|
||||
icon={<FaPlus />}
|
||||
onClick={handleOpenAddNodePopover}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AddNodeButton);
|
@ -0,0 +1,37 @@
|
||||
import { Flex, Spacer } from '@chakra-ui/layout';
|
||||
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
|
||||
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
|
||||
sx={{
|
||||
gap: 2,
|
||||
top: 2,
|
||||
left: 2,
|
||||
right: 2,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<AddNodeButton />
|
||||
<UpdateNodesButton />
|
||||
<Spacer />
|
||||
<WorkflowName />
|
||||
<Spacer />
|
||||
{isWorkflowLibraryEnabled && <WorkflowLibraryButton />}
|
||||
<WorkflowLibraryMenu />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TopCenterPanel);
|
@ -0,0 +1,32 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
|
||||
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExclamationTriangle } from 'react-icons/fa';
|
||||
|
||||
const UpdateNodesButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const nodesNeedUpdate = useGetNodesNeedUpdate();
|
||||
const handleClickUpdateNodes = useCallback(() => {
|
||||
dispatch(updateAllNodesRequested());
|
||||
}, [dispatch]);
|
||||
|
||||
if (!nodesNeedUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IAIButton
|
||||
leftIcon={<FaExclamationTriangle />}
|
||||
onClick={handleClickUpdateNodes}
|
||||
pointerEvents="auto"
|
||||
>
|
||||
{t('nodes.updateAllNodes')}
|
||||
</IAIButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UpdateNodesButton);
|
@ -0,0 +1,25 @@
|
||||
import { Text } from '@chakra-ui/layout';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { memo } from 'react';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
const name = useAppSelector(
|
||||
(state) => state.workflow.name || 'Untitled Workflow'
|
||||
);
|
||||
|
||||
return (
|
||||
<Text
|
||||
m={2}
|
||||
fontSize="lg"
|
||||
userSelect="none"
|
||||
noOfLines={1}
|
||||
wordBreak="break-all"
|
||||
fontWeight={600}
|
||||
opacity={0.8}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TopCenterPanel);
|
@ -1,8 +1,8 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
import { memo } from 'react';
|
||||
import WorkflowEditorSettings from './WorkflowEditorSettings';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu';
|
||||
|
||||
const TopRightPanel = () => {
|
||||
const isWorkflowLibraryEnabled =
|
||||
@ -11,7 +11,7 @@ const TopRightPanel = () => {
|
||||
return (
|
||||
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineEnd: 2 }}>
|
||||
{isWorkflowLibraryEnabled && <WorkflowLibraryButton />}
|
||||
<WorkflowEditorSettings />
|
||||
<WorkflowLibraryMenu />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -9,15 +9,14 @@ import {
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
forwardRef,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton';
|
||||
import {
|
||||
selectionModeChanged,
|
||||
shouldAnimateEdgesChanged,
|
||||
@ -25,11 +24,9 @@ import {
|
||||
shouldSnapToGridChanged,
|
||||
shouldValidateGraphChanged,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { FaCog } from 'react-icons/fa';
|
||||
import { SelectionMode } from 'reactflow';
|
||||
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopCenterPanel/ReloadSchemaButton';
|
||||
import { ChangeEvent, ReactNode, memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectionMode } from 'reactflow';
|
||||
|
||||
const formLabelProps: FormLabelProps = {
|
||||
fontWeight: 600,
|
||||
@ -56,7 +53,11 @@ const selector = createSelector(
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const WorkflowEditorSettings = forwardRef((_, ref) => {
|
||||
type Props = {
|
||||
children: (props: { onOpen: () => void }) => ReactNode;
|
||||
};
|
||||
|
||||
const WorkflowEditorSettings = ({ children }: Props) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
@ -106,13 +107,7 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<IAIIconButton
|
||||
ref={ref}
|
||||
aria-label={t('nodes.workflowSettings')}
|
||||
tooltip={t('nodes.workflowSettings')}
|
||||
icon={<FaCog />}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
{children({ onOpen })}
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered>
|
||||
<ModalOverlay />
|
||||
@ -151,6 +146,7 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
|
||||
label={t('nodes.colorCodeEdges')}
|
||||
helperText={t('nodes.colorCodeEdgesHelp')}
|
||||
/>
|
||||
<Divider />
|
||||
<IAISwitch
|
||||
formLabelProps={formLabelProps}
|
||||
isChecked={selectionModeIsChecked}
|
||||
@ -175,6 +171,6 @@ const WorkflowEditorSettings = forwardRef((_, ref) => {
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default memo(WorkflowEditorSettings);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaFolderOpen } from 'react-icons/fa';
|
||||
import WorkflowLibraryModal from './WorkflowLibraryModal';
|
||||
import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
|
||||
|
||||
const WorkflowLibraryButton = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -12,12 +12,13 @@ const WorkflowLibraryButton = () => {
|
||||
|
||||
return (
|
||||
<WorkflowLibraryModalContext.Provider value={disclosure}>
|
||||
<IAIIconButton
|
||||
icon={<FaFolderOpen />}
|
||||
<IAIButton
|
||||
leftIcon={<FaFolderOpen />}
|
||||
onClick={disclosure.onOpen}
|
||||
tooltip={t('workflows.workflowLibrary')}
|
||||
aria-label={t('workflows.workflowLibrary')}
|
||||
/>
|
||||
pointerEvents="auto"
|
||||
>
|
||||
{t('workflows.workflowLibrary')}
|
||||
</IAIButton>
|
||||
<WorkflowLibraryModal />
|
||||
</WorkflowLibraryModalContext.Provider>
|
||||
);
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { MenuItem } from '@chakra-ui/react';
|
||||
import { useDownloadWorkflow } from 'features/nodes/hooks/useDownloadWorkflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload } from 'react-icons/fa';
|
||||
|
||||
const DownloadWorkflowMenuItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const downloadWorkflow = useDownloadWorkflow();
|
||||
|
||||
return (
|
||||
<MenuItem as="button" icon={<FaDownload />} onClick={downloadWorkflow}>
|
||||
{t('workflows.downloadWorkflow')}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DownloadWorkflowMenuItem);
|
@ -0,0 +1,88 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
Button,
|
||||
Flex,
|
||||
MenuItem,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { makeToast } from 'features/system/util/makeToast';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
|
||||
const ResetWorkflowEditorMenuItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleConfirmClear = useCallback(() => {
|
||||
dispatch(nodeEditorReset());
|
||||
|
||||
dispatch(
|
||||
addToast(
|
||||
makeToast({
|
||||
title: t('workflows.workflowEditorReset'),
|
||||
status: 'success',
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [dispatch, t, onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
as="button"
|
||||
icon={<FaTrash />}
|
||||
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{t('nodes.resetWorkflow')}
|
||||
</MenuItem>
|
||||
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
leastDestructiveRef={cancelRef}
|
||||
isCentered
|
||||
>
|
||||
<AlertDialogOverlay />
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
{t('nodes.resetWorkflow')}
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody py={4}>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Text>{t('nodes.resetWorkflowDesc')}</Text>
|
||||
<Text variant="subtext">{t('nodes.resetWorkflowDesc2')}</Text>
|
||||
</Flex>
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="error" ml={3} onClick={handleConfirmClear}>
|
||||
{t('common.accept')}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ResetWorkflowEditorMenuItem);
|
@ -0,0 +1,85 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
MenuItem,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { useSaveWorkflowAs } from 'features/workflowLibrary/hooks/useSaveWorkflowAs';
|
||||
import { getWorkflowCopyName } from 'features/workflowLibrary/util/getWorkflowCopyName';
|
||||
import { ChangeEvent, memo, useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaClone } from 'react-icons/fa';
|
||||
|
||||
const SaveWorkflowAsButton = () => {
|
||||
const currentName = useAppSelector((state) => state.workflow.name);
|
||||
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();
|
||||
}, [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 (
|
||||
<>
|
||||
<MenuItem as="button" icon={<FaClone />} onClick={onOpenCallback}>
|
||||
{t('workflows.saveWorkflowAs')}
|
||||
</MenuItem>
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
leastDestructiveRef={inputRef}
|
||||
isCentered
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
{t('workflows.saveWorkflowAs')}
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
<FormControl>
|
||||
<FormLabel>{t('workflows.workflowName')}</FormLabel>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={name}
|
||||
onChange={onChange}
|
||||
placeholder={t('workflows.workflowName')}
|
||||
/>
|
||||
</FormControl>
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<IAIButton onClick={onClose}>{t('common.cancel')}</IAIButton>
|
||||
<IAIButton colorScheme="accent" onClick={onSave} ml={3}>
|
||||
{t('common.saveAs')}
|
||||
</IAIButton>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SaveWorkflowAsButton);
|
@ -0,0 +1,17 @@
|
||||
import { MenuItem } from '@chakra-ui/react';
|
||||
import { useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaSave } from 'react-icons/fa';
|
||||
|
||||
const SaveLibraryWorkflowMenuItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const { saveWorkflow } = useSaveLibraryWorkflow();
|
||||
return (
|
||||
<MenuItem as="button" icon={<FaSave />} onClick={saveWorkflow}>
|
||||
{t('workflows.saveWorkflow')}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SaveLibraryWorkflowMenuItem);
|
@ -0,0 +1,21 @@
|
||||
import { MenuItem } from '@chakra-ui/react';
|
||||
import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCog } from 'react-icons/fa';
|
||||
|
||||
const DownloadWorkflowMenuItem = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<WorkflowEditorSettings>
|
||||
{({ onOpen }) => (
|
||||
<MenuItem as="button" icon={<FaCog />} onClick={onOpen}>
|
||||
{t('nodes.workflowSettings')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</WorkflowEditorSettings>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DownloadWorkflowMenuItem);
|
@ -0,0 +1,27 @@
|
||||
import { MenuItem } from '@chakra-ui/react';
|
||||
import { FileButton } from '@mantine/core';
|
||||
import { useLoadWorkflowFromFile } from 'features/workflowLibrary/hooks/useLoadWorkflowFromFile';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
|
||||
const UploadWorkflowMenuItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const resetRef = useRef<() => void>(null);
|
||||
const loadWorkflowFromFile = useLoadWorkflowFromFile({ resetRef });
|
||||
return (
|
||||
<FileButton
|
||||
resetRef={resetRef}
|
||||
accept="application/json"
|
||||
onChange={loadWorkflowFromFile}
|
||||
>
|
||||
{(props) => (
|
||||
<MenuItem as="button" icon={<FaUpload />} {...props}>
|
||||
{t('workflows.uploadWorkflow')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</FileButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UploadWorkflowMenuItem);
|
@ -0,0 +1,50 @@
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuList,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem';
|
||||
import ResetWorkflowEditorMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem';
|
||||
import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem';
|
||||
import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem';
|
||||
import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem';
|
||||
import UploadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaEllipsis } from 'react-icons/fa6';
|
||||
import { menuListMotionProps } from 'theme/components/menu';
|
||||
|
||||
const WorkflowLibraryMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
useGlobalMenuCloseTrigger(onClose);
|
||||
const isWorkflowLibraryEnabled =
|
||||
useFeatureStatus('workflowLibrary').isFeatureEnabled;
|
||||
|
||||
return (
|
||||
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||
<MenuButton
|
||||
as={IAIIconButton}
|
||||
aria-label={t('workflows.workflowEditorMenu')}
|
||||
icon={<FaEllipsis />}
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
<MenuList motionProps={menuListMotionProps} pointerEvents="auto">
|
||||
{isWorkflowLibraryEnabled && <SaveWorkflowMenuItem />}
|
||||
{isWorkflowLibraryEnabled && <SaveWorkflowAsMenuItem />}
|
||||
<DownloadWorkflowMenuItem />
|
||||
<UploadWorkflowMenuItem />
|
||||
<ResetWorkflowEditorMenuItem />
|
||||
<MenuDivider />
|
||||
<SettingsMenuItem />
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryMenu);
|
@ -0,0 +1,17 @@
|
||||
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useDownloadWorkflow = () => {
|
||||
const workflow = useWorkflow();
|
||||
const downloadWorkflow = useCallback(() => {
|
||||
const blob = new Blob([JSON.stringify(workflow, null, 2)]);
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${workflow.name || 'My Workflow'}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}, [workflow]);
|
||||
|
||||
return downloadWorkflow;
|
||||
};
|
@ -45,6 +45,9 @@ const invokeAI = definePartsStyle((props) => ({
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
divider: {
|
||||
borderColor: mode('base.400', 'base.700')(props),
|
||||
},
|
||||
}));
|
||||
|
||||
export const menuTheme = defineMultiStyleConfig({
|
||||
|
Loading…
Reference in New Issue
Block a user