diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index f45b9973ce..a7ad34bfc7 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -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" diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 3675dd9af9..8d3ad1a16f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -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 = () => { > - - - + diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 2f0695f03a..5ba0015c60 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx index 4b515bcd0a..e6d366b0f3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx @@ -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 ( { top: 2, insetInlineStart: '50%', transform: 'translate(-50%)', + alignItems: 'center', }} > - + + {name} + + {/* {isWorkflowLibraryEnabled && ( <> @@ -29,7 +45,7 @@ const TopCenterPanel = () => { )} - + */} ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx new file mode 100644 index 0000000000..266d711337 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx @@ -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 ( + } + onClick={handleOpenAddNodePopover} + pointerEvents="auto" + /> + ); +}; + +export default memo(AddNodeButton); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx new file mode 100644 index 0000000000..2e79ea9763 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx @@ -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 ( + + + + + + + {isWorkflowLibraryEnabled && } + + + ); +}; + +export default memo(TopCenterPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx new file mode 100644 index 0000000000..6d82cc5b9d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx @@ -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 ( + } + onClick={handleClickUpdateNodes} + pointerEvents="auto" + > + {t('nodes.updateAllNodes')} + + ); +}; + +export default memo(UpdateNodesButton); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx new file mode 100644 index 0000000000..58fc3bb5b8 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx @@ -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 ( + + {name} + + ); +}; + +export default memo(TopCenterPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx index a06b4a7656..944ec15c42 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx @@ -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 ( {isWorkflowLibraryEnabled && } - + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx index 1ca671a222..66a55b20c7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx @@ -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 ( <> - } - onClick={onOpen} - /> + {children({ onOpen })} @@ -151,6 +146,7 @@ const WorkflowEditorSettings = forwardRef((_, ref) => { label={t('nodes.colorCodeEdges')} helperText={t('nodes.colorCodeEdgesHelp')} /> + { ); -}); +}; export default memo(WorkflowEditorSettings); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx index ee0a8314a0..2848aea49d 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx @@ -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 ( - } + } onClick={disclosure.onOpen} - tooltip={t('workflows.workflowLibrary')} - aria-label={t('workflows.workflowLibrary')} - /> + pointerEvents="auto" + > + {t('workflows.workflowLibrary')} + ); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem.tsx new file mode 100644 index 0000000000..1a04b11594 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem.tsx @@ -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 ( + } onClick={downloadWorkflow}> + {t('workflows.downloadWorkflow')} + + ); +}; + +export default memo(DownloadWorkflowMenuItem); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem.tsx new file mode 100644 index 0000000000..5d7e58e198 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/ResetWorkflowEditorMenuItem.tsx @@ -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(null); + + const handleConfirmClear = useCallback(() => { + dispatch(nodeEditorReset()); + + dispatch( + addToast( + makeToast({ + title: t('workflows.workflowEditorReset'), + status: 'success', + }) + ) + ); + + onClose(); + }, [dispatch, t, onClose]); + + return ( + <> + } + sx={{ color: 'error.600', _dark: { color: 'error.300' } }} + onClick={onOpen} + > + {t('nodes.resetWorkflow')} + + + + + + + + {t('nodes.resetWorkflow')} + + + + + {t('nodes.resetWorkflowDesc')} + {t('nodes.resetWorkflowDesc2')} + + + + + + + + + + + ); +}; + +export default memo(ResetWorkflowEditorMenuItem); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx new file mode 100644 index 0000000000..dbcf7edd9d --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx @@ -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(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) => { + setName(e.target.value); + }, []); + + return ( + <> + } onClick={onOpenCallback}> + {t('workflows.saveWorkflowAs')} + + + + + + {t('workflows.saveWorkflowAs')} + + + + + {t('workflows.workflowName')} + + + + + + {t('common.cancel')} + + {t('common.saveAs')} + + + + + + + ); +}; + +export default memo(SaveWorkflowAsButton); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx new file mode 100644 index 0000000000..15ca98b69e --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx @@ -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 ( + } onClick={saveWorkflow}> + {t('workflows.saveWorkflow')} + + ); +}; + +export default memo(SaveLibraryWorkflowMenuItem); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx new file mode 100644 index 0000000000..0a9bbdeb2c --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx @@ -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 ( + + {({ onOpen }) => ( + } onClick={onOpen}> + {t('nodes.workflowSettings')} + + )} + + ); +}; + +export default memo(DownloadWorkflowMenuItem); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx new file mode 100644 index 0000000000..d217a652bc --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/UploadWorkflowMenuItem.tsx @@ -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 ( + + {(props) => ( + } {...props}> + {t('workflows.uploadWorkflow')} + + )} + + ); +}; + +export default memo(UploadWorkflowMenuItem); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx new file mode 100644 index 0000000000..a19b9cd6b5 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx @@ -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 ( + + } + pointerEvents="auto" + /> + + {isWorkflowLibraryEnabled && } + {isWorkflowLibraryEnabled && } + + + + + + + + ); +}; + +export default memo(WorkflowLibraryMenu); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts new file mode 100644 index 0000000000..8b0a640ef9 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts @@ -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; +}; diff --git a/invokeai/frontend/web/src/theme/components/menu.ts b/invokeai/frontend/web/src/theme/components/menu.ts index 4ab323bdb5..04a456daa1 100644 --- a/invokeai/frontend/web/src/theme/components/menu.ts +++ b/invokeai/frontend/web/src/theme/components/menu.ts @@ -45,6 +45,9 @@ const invokeAI = definePartsStyle((props) => ({ fontSize: 14, }, }, + divider: { + borderColor: mode('base.400', 'base.700')(props), + }, })); export const menuTheme = defineMultiStyleConfig({