Feat: Collapsable Sidebar (#5302)

This commit is contained in:
Pragadesh-45
2025-08-29 21:03:46 +05:30
committed by GitHub
parent cb7f61ee4b
commit ba56e87375
11 changed files with 111 additions and 26 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 }) => {

View File

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

View File

@@ -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' }
};
/**

View File

@@ -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) => {

View File

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