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:
psychedelicious 2023-12-06 20:15:26 +11:00
parent 283bb73418
commit e4f67628c0
20 changed files with 511 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,6 +45,9 @@ const invokeAI = definePartsStyle((props) => ({
fontSize: 14,
},
},
divider: {
borderColor: mode('base.400', 'base.700')(props),
},
}));
export const menuTheme = defineMultiStyleConfig({