From 73124fd7152e8e2f24326740906eaf8850357b0b Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Wed, 17 Dec 2025 20:35:18 +0530 Subject: [PATCH] add: manage workspace (#6424) * add: manage workspace * fixes * replace dropdown to MenuDropdown * rm: refs --- .../src/components/AppTitleBar/index.js | 14 +- .../ManageWorkspace/DeleteWorkspace/index.js | 55 ++++++ .../ManageWorkspace/RenameWorkspace/index.js | 95 ++++++++++ .../ManageWorkspace/StyledWrapper.js | 175 ++++++++++++++++++ .../src/components/ManageWorkspace/index.js | 162 ++++++++++++++++ .../CreateEnvironment/index.js | 2 +- .../RenameEnvironment/index.js | 2 +- packages/bruno-app/src/pages/Bruno/index.js | 4 + .../src/providers/ReduxStore/slices/app.js | 12 ++ 9 files changed, 517 insertions(+), 4 deletions(-) create mode 100644 packages/bruno-app/src/components/ManageWorkspace/DeleteWorkspace/index.js create mode 100644 packages/bruno-app/src/components/ManageWorkspace/RenameWorkspace/index.js create mode 100644 packages/bruno-app/src/components/ManageWorkspace/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ManageWorkspace/index.js diff --git a/packages/bruno-app/src/components/AppTitleBar/index.js b/packages/bruno-app/src/components/AppTitleBar/index.js index a54f942df..0c9ec1f8d 100644 --- a/packages/bruno-app/src/components/AppTitleBar/index.js +++ b/packages/bruno-app/src/components/AppTitleBar/index.js @@ -1,10 +1,10 @@ import React from 'react'; -import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconUpload } from '@tabler/icons'; +import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconUpload, IconSettings } from '@tabler/icons'; import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useDispatch, useSelector } from 'react-redux'; -import { savePreferences, showHomePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; +import { savePreferences, showHomePage, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs'; import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces'; @@ -90,6 +90,10 @@ const AppTitleBar = () => { setCreateWorkspaceModalOpen(true); }; + const handleManageWorkspaces = () => { + dispatch(showManageWorkspacePage()); + }; + const handleImportWorkspace = () => { setImportWorkspaceModalOpen(true); }; @@ -166,6 +170,12 @@ const AppTitleBar = () => { leftSection: IconUpload, label: 'Import workspace', onClick: handleImportWorkspace + }, + { + id: 'manage-workspaces', + leftSection: IconSettings, + label: 'Manage workspaces', + onClick: handleManageWorkspaces } ); diff --git a/packages/bruno-app/src/components/ManageWorkspace/DeleteWorkspace/index.js b/packages/bruno-app/src/components/ManageWorkspace/DeleteWorkspace/index.js new file mode 100644 index 000000000..1eea605ab --- /dev/null +++ b/packages/bruno-app/src/components/ManageWorkspace/DeleteWorkspace/index.js @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import Portal from 'components/Portal/index'; +import Modal from 'components/Modal/index'; +import toast from 'react-hot-toast'; +import { useDispatch } from 'react-redux'; +import { IconFolder } from '@tabler/icons'; +import { closeWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions'; + +const DeleteWorkspace = ({ onClose, workspace }) => { + const dispatch = useDispatch(); + const [isDeleting, setIsDeleting] = useState(false); + + const onConfirm = async () => { + if (isDeleting) return; + + try { + setIsDeleting(true); + await dispatch(closeWorkspaceAction(workspace.uid)); + onClose(); + } catch (error) { + toast.error(error?.message || 'An error occurred while removing the workspace'); + setIsDeleting(false); + } + }; + + return ( + + +
+ + {workspace?.name} +
+ {workspace?.pathname && ( +
{workspace.pathname}
+ )} +
+ Are you sure you want to remove workspace {workspace?.name}? +
+
+ The workspace will still be available in the file system and can be re-opened later. +
+
+
+ ); +}; + +export default DeleteWorkspace; diff --git a/packages/bruno-app/src/components/ManageWorkspace/RenameWorkspace/index.js b/packages/bruno-app/src/components/ManageWorkspace/RenameWorkspace/index.js new file mode 100644 index 000000000..d4c4c8045 --- /dev/null +++ b/packages/bruno-app/src/components/ManageWorkspace/RenameWorkspace/index.js @@ -0,0 +1,95 @@ +import React, { useEffect, useRef } from 'react'; +import Portal from 'components/Portal/index'; +import Modal from 'components/Modal/index'; +import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { useDispatch, useSelector } from 'react-redux'; +import { renameWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions'; + +const RenameWorkspace = ({ onClose, workspace }) => { + const dispatch = useDispatch(); + const { workspaces } = useSelector((state) => state.workspaces); + const inputRef = useRef(); + + const formik = useFormik({ + enableReinitialize: true, + initialValues: { + name: workspace.name + }, + validationSchema: Yup.object({ + name: Yup.string() + .min(1, 'must be at least 1 character') + .max(255, 'must be 255 characters or less') + .required('name is required') + .test('unique-name', 'A workspace with this name already exists', function (value) { + if (!value) return true; + return !workspaces.some((w) => + w.uid !== workspace.uid && w.name.toLowerCase() === value.toLowerCase() + ); + }) + }), + onSubmit: (values) => { + if (values.name === workspace.name) { + onClose(); + return; + } + dispatch(renameWorkspaceAction(workspace.uid, values.name)) + .then(() => { + onClose(); + }) + .catch((error) => { + toast.error(error?.message || 'An error occurred while renaming the workspace'); + }); + } + }); + + useEffect(() => { + if (inputRef && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [inputRef]); + + const onSubmit = () => { + formik.handleSubmit(); + }; + + return ( + + +
e.preventDefault()}> +
+ + + {formik.touched.name && formik.errors.name ? ( +
{formik.errors.name}
+ ) : null} +
+
+
+
+ ); +}; + +export default RenameWorkspace; diff --git a/packages/bruno-app/src/components/ManageWorkspace/StyledWrapper.js b/packages/bruno-app/src/components/ManageWorkspace/StyledWrapper.js new file mode 100644 index 000000000..f049cc1f4 --- /dev/null +++ b/packages/bruno-app/src/components/ManageWorkspace/StyledWrapper.js @@ -0,0 +1,175 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + height: 100%; + display: flex; + flex-direction: column; + + .manage-workspace-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid ${(props) => props.theme.workspace.border}; + } + + .header-left { + display: flex; + align-items: center; + gap: 8px; + } + + .back-button { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + cursor: pointer; + color: ${(props) => props.theme.text}; + } + + .header-title { + font-size: 15px; + font-weight: 600; + color: ${(props) => props.theme.text}; + } + + .create-workspace-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: ${(props) => props.theme.border.radius.base}; + background: ${(props) => props.theme.workspace.accent}; + color: white; + font-size: ${(props) => props.theme.font.size.sm}; + font-weight: 500; + cursor: pointer; + border: none; + } + + .workspace-list { + flex: 1; + overflow-y: auto; + padding: 0 16px; + } + + .workspace-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid ${(props) => props.theme.workspace.border}; + } + + .workspace-info { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + min-width: 0; + } + + .workspace-name-row { + display: flex; + align-items: center; + gap: 6px; + } + + .workspace-icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &.default { + color: ${(props) => props.theme.colors.text.muted}; + } + + &.regular { + color: ${(props) => props.theme.workspace.accent}; + } + } + + .workspace-name { + font-size: ${(props) => props.theme.font.size.md}; + font-weight: 500; + color: ${(props) => props.theme.text}; + } + + .default-badge { + padding: 1px 6px; + border-radius: ${(props) => props.theme.border.radius.sm}; + background: ${(props) => props.theme.sidebar.badge.bg}; + color: ${(props) => props.theme.colors.text.muted}; + font-size: ${(props) => props.theme.font.size.xs}; + font-weight: 500; + } + + .workspace-path { + font-size: ${(props) => props.theme.font.size.xs}; + color: ${(props) => props.theme.colors.text.muted}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .workspace-actions { + display: flex; + align-items: center; + gap: 8px; + } + + .action-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + background: transparent; + border: none; + color: ${(props) => props.theme.text}; + font-size: ${(props) => props.theme.font.size.xs}; + cursor: pointer; + } + + .more-actions-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + background: transparent; + border: none; + color: ${(props) => props.theme.text}; + cursor: pointer; + } + + .dropdown-menu { + min-width: 120px; + } + + .dropdown-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + color: ${(props) => props.theme.text}; + font-size: ${(props) => props.theme.font.size.sm}; + + &.danger { + color: ${(props) => props.theme.colors.text.danger}; + } + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + color: ${(props) => props.theme.colors.text.muted}; + font-size: ${(props) => props.theme.font.size.sm}; + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ManageWorkspace/index.js b/packages/bruno-app/src/components/ManageWorkspace/index.js new file mode 100644 index 000000000..f0e6622b7 --- /dev/null +++ b/packages/bruno-app/src/components/ManageWorkspace/index.js @@ -0,0 +1,162 @@ +import React, { useState, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { IconArrowLeft, IconPlus, IconFolder, IconLock, IconDots, IconCategory, IconLogin } from '@tabler/icons'; +import toast from 'react-hot-toast'; + +import { showHomePage } from 'providers/ReduxStore/slices/app'; +import { switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; +import { showInFolder } from 'providers/ReduxStore/slices/collections/actions'; +import { sortWorkspaces } from 'utils/workspaces'; + +import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace'; +import RenameWorkspace from './RenameWorkspace'; +import DeleteWorkspace from './DeleteWorkspace'; +import StyledWrapper from './StyledWrapper'; +import MenuDropdown from 'ui/MenuDropdown/index'; + +const ManageWorkspace = () => { + const dispatch = useDispatch(); + const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); + const preferences = useSelector((state) => state.app.preferences); + + const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false); + const [renameWorkspaceModal, setRenameWorkspaceModal] = useState({ open: false, workspace: null }); + const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState({ open: false, workspace: null }); + + const sortedWorkspaces = useMemo(() => { + return sortWorkspaces(workspaces, preferences); + }, [workspaces, preferences]); + + const handleBack = () => { + dispatch(showHomePage()); + }; + + const handleOpenWorkspace = (workspace) => { + dispatch(switchWorkspace(workspace.uid)); + dispatch(showHomePage()); + toast.success(`Switched to ${workspace.name}`); + }; + + const handleShowInFolder = (workspace) => { + if (workspace.pathname) { + dispatch(showInFolder(workspace.pathname)).catch(() => { + toast.error('Error opening the folder'); + }); + } + }; + + const handleRenameClick = (workspace) => { + setRenameWorkspaceModal({ open: true, workspace }); + }; + + const handleCloseClick = (workspace) => { + if (workspace.type === 'default') { + toast.error('Cannot remove the default workspace'); + return; + } + setDeleteWorkspaceModal({ open: true, workspace }); + }; + + return ( + + {createWorkspaceModalOpen && ( + setCreateWorkspaceModalOpen(false)} /> + )} + + {renameWorkspaceModal.open && renameWorkspaceModal.workspace && ( + setRenameWorkspaceModal({ open: false, workspace: null })} + /> + )} + + {deleteWorkspaceModal.open && deleteWorkspaceModal.workspace && ( + setDeleteWorkspaceModal({ open: false, workspace: null })} + /> + )} + +
+
+
+ +
+ Manage Workspace +
+ +
+ +
+ {sortedWorkspaces.length === 0 ? ( +
+ No workspaces found +
+ ) : ( + sortedWorkspaces.map((workspace) => { + const isDefault = workspace.type === 'default'; + const isActive = workspace.uid === activeWorkspaceUid; + + return ( +
+
+
+ + {isDefault ? ( + + ) : ( + + )} + + {workspace.name} + {isDefault && Default} +
+ {workspace.pathname && ( +
{workspace.pathname}
+ )} +
+ +
+ + {workspace.pathname && workspace.type !== 'default' && ( + + )} + {!isDefault && ( + handleRenameClick(workspace) }, + { id: 'remove', label: 'Remove', onClick: () => handleCloseClick(workspace) } + ]} + > + + + )} +
+
+ ); + }) + )} +
+
+ ); +}; + +export default ManageWorkspace; diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment/index.js index 7899c807a..a286108ad 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment/index.js @@ -69,7 +69,7 @@ const CreateEnvironment = ({ onClose, onEnvironmentCreated }) => { >
e.preventDefault()}>
-