mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 05:35:41 +00:00
Feat: Collapsable Sidebar (#5302)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
const IconSidebarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, className = '', ...rest }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={`icon icon-tabler icons-tabler-outline icon-tabler-layout-sidebar ${className}`}
|
||||
{...rest}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M9 4l0 16" />
|
||||
{!collapsed && (
|
||||
<rect x="4.6" y="4.6" width="4.8" height="14.8" rx="0.8" fill="currentColor" />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSidebarToggle;
|
||||
@@ -18,6 +18,7 @@ const RequestTabs = () => {
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const screenWidth = useSelector((state) => state.app.screenWidth);
|
||||
|
||||
const getTabClassname = (tab, index) => {
|
||||
@@ -49,7 +50,8 @@ const RequestTabs = () => {
|
||||
const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid);
|
||||
const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid);
|
||||
|
||||
const maxTablistWidth = screenWidth - leftSidebarWidth - 150;
|
||||
const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth;
|
||||
const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150;
|
||||
const tabsWidth = collectionRequestTabs.length * 150 + 34; // 34: (+)icon
|
||||
const showChevrons = maxTablistWidth < tabsWidth;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IconArrowBackUp, IconEdit } from '@tabler/icons';
|
||||
import Help from 'components/Help';
|
||||
import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
@@ -45,6 +46,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||
.then(() => {
|
||||
toast.success('Collection created!');
|
||||
dispatch(toggleSidebarCollapse());
|
||||
onClose();
|
||||
})
|
||||
.catch((e) => toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e))));
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
|
||||
aside {
|
||||
background-color: ${(props) => props.theme.sidebar.bg};
|
||||
overflow: hidden;
|
||||
|
||||
.collection-title {
|
||||
line-height: 1.5;
|
||||
@@ -50,6 +51,7 @@ const Wrapper = styled.div`
|
||||
background-color: transparent;
|
||||
width: 6px;
|
||||
right: -3px;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover div.drag-request-border {
|
||||
width: 2px;
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
import TitleBar from './TitleBar';
|
||||
import Collections from './Collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useApp } from 'providers/App';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 221;
|
||||
const MAX_LEFT_SIDEBAR_WIDTH = 600;
|
||||
|
||||
const Sidebar = () => {
|
||||
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const { version } = useApp();
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
const lastWidthRef = useRef(leftSidebarWidth);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const currentWidth = sidebarCollapsed ? 0 : asideWidth;
|
||||
|
||||
// Clamp helper keeps width in allowed range
|
||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragging) {
|
||||
e.preventDefault();
|
||||
let width = e.clientX + 2;
|
||||
if (width < MIN_LEFT_SIDEBAR_WIDTH || width > MAX_LEFT_SIDEBAR_WIDTH) {
|
||||
return;
|
||||
}
|
||||
setAsideWidth(width);
|
||||
}
|
||||
if (!dragging || sidebarCollapsed) return;
|
||||
e.preventDefault();
|
||||
const nextWidth = clamp(e.clientX + 2, MIN_LEFT_SIDEBAR_WIDTH, MAX_LEFT_SIDEBAR_WIDTH);
|
||||
if (Math.abs(nextWidth - lastWidthRef.current) < 3) return;
|
||||
lastWidthRef.current = nextWidth;
|
||||
setAsideWidth(nextWidth);
|
||||
};
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
if (dragging) {
|
||||
e.preventDefault();
|
||||
@@ -49,6 +50,9 @@ const Sidebar = () => {
|
||||
};
|
||||
const handleDragbarMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
if (sidebarCollapsed) {
|
||||
return;
|
||||
}
|
||||
setDragging(true);
|
||||
dispatch(
|
||||
updateIsDragging({
|
||||
@@ -73,7 +77,7 @@ const Sidebar = () => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex relative h-full">
|
||||
<aside>
|
||||
<aside style={{ width: currentWidth, transition: dragging ? 'none' : 'width 0.2s ease-in-out' }}>
|
||||
<div className="flex flex-row h-full w-full">
|
||||
<div className="flex flex-col w-full" style={{ width: asideWidth }}>
|
||||
<div className="flex flex-col flex-grow">
|
||||
@@ -84,9 +88,11 @@ const Sidebar = () => {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="absolute drag-sidebar h-full" onMouseDown={handleDragbarMouseDown}>
|
||||
<div className="drag-request-border" />
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<div className="absolute drag-sidebar h-full" onMouseDown={handleDragbarMouseDown}>
|
||||
<div className="drag-request-border" />
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconSettings, IconCookie, IconTool } from '@tabler/icons';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import Preferences from 'components/Preferences';
|
||||
import Cookies from 'components/Cookies';
|
||||
import Notifications from 'components/Notifications';
|
||||
import Portal from 'components/Portal';
|
||||
import { showPreferences } from 'providers/ReduxStore/slices/app';
|
||||
import { showPreferences, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { useApp } from 'providers/App';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -15,6 +16,7 @@ const StatusBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const preferencesOpen = useSelector((state) => state.app.showPreferences);
|
||||
const logs = useSelector((state) => state.logs.logs);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const [cookiesOpen, setCookiesOpen] = useState(false);
|
||||
const { version } = useApp();
|
||||
|
||||
@@ -59,6 +61,16 @@ const StatusBar = () => {
|
||||
<div className="status-bar">
|
||||
<div className="status-bar-section">
|
||||
<div className="status-bar-group">
|
||||
<ToolHint text="Toggle Sidebar" toolhintId="Toggle Sidebar" place="top-start" offset={10}>
|
||||
<button
|
||||
className="status-bar-button"
|
||||
aria-label="Toggle Sidebar"
|
||||
onClick={() => dispatch(toggleSidebarCollapse())}
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Preferences" toolhintId="Preferences" place="top-start" offset={10}>
|
||||
<button
|
||||
className="status-bar-button"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import { IconBrandGithub, IconPlus, IconDownload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
@@ -14,13 +15,19 @@ import StyledWrapper from './StyledWrapper';
|
||||
const Welcome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')));
|
||||
dispatch(openCollection())
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
toast.error(t('WELCOME.COLLECTION_OPEN_ERROR'));
|
||||
});
|
||||
};
|
||||
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { getKeyBindingsForActionAllOS } from './keyMappings';
|
||||
|
||||
export const HotkeysContext = React.createContext();
|
||||
@@ -224,6 +225,18 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, dispatch]);
|
||||
|
||||
// Collapse sidebar (ctrl/cmd + \)
|
||||
useEffect(() => {
|
||||
Mousetrap.bind([...getKeyBindingsForActionAllOS('collapseSidebar')], (e) => {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
return false;
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind([...getKeyBindingsForActionAllOS('collapseSidebar')]);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const currentCollection = getCurrentCollection();
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,7 +20,8 @@ const KeyMapping = {
|
||||
windows: 'ctrl+pagedown',
|
||||
name: 'Switch to Next Tab'
|
||||
},
|
||||
closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' }
|
||||
closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' },
|
||||
collapseSidebar: { mac: 'command+\\', windows: 'ctrl+\\', name: 'Collapse Sidebar' }
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ const initialState = {
|
||||
isDragging: false,
|
||||
idbConnectionReady: false,
|
||||
leftSidebarWidth: 222,
|
||||
sidebarCollapsed: false,
|
||||
screenWidth: 500,
|
||||
showHomePage: false,
|
||||
showPreferences: false,
|
||||
@@ -89,6 +90,9 @@ export const appSlice = createSlice({
|
||||
...state.generateCode,
|
||||
...action.payload
|
||||
};
|
||||
},
|
||||
toggleSidebarCollapse: (state) => {
|
||||
state.sidebarCollapsed = !state.sidebarCollapsed;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -108,7 +112,8 @@ export const {
|
||||
removeTaskFromQueue,
|
||||
removeAllTasksFromQueue,
|
||||
updateSystemProxyEnvVariables,
|
||||
updateGenerateCode
|
||||
updateGenerateCode,
|
||||
toggleSidebarCollapse
|
||||
} = appSlice.actions;
|
||||
|
||||
export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'utils/common/path';
|
||||
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
||||
import { insertTaskIntoQueue, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
findCollectionByUid,
|
||||
@@ -1462,7 +1462,14 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
collectionSchema
|
||||
.validate(collection)
|
||||
.then(() => dispatch(_createCollection({ ...collection, securityConfig })))
|
||||
.then(resolve)
|
||||
.then(() => {
|
||||
// Expand sidebar if it's collapsed after collection is successfully opened
|
||||
const state = getState();
|
||||
if (state.app.sidebarCollapsed) {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user