mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: Moved Workspace Selector to the Titlebar of the window. (#6319)
* refactor: update sidebar components and styles, replace TitleBar with SidebarHeader, and enhance collections search functionality * refactor: improve event listener management in AppTitleBar and clean up SidebarHeader styles * fix: ensure safe access to layout preferences in AppTitleBar and set default order in SidebarHeader * refactor: centralize toTitleCase utility and remove redundant implementations in AppTitleBar and WorkspaceSelector * feat: enhance accessibility and testing for sidebar and devtools toggle buttons in AppTitleBar * chore: quick fix on a flaky test --------- Co-authored-by: Bijin A B <bijin@usebruno.com>
This commit is contained in:
248
packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js
Normal file
248
packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js
Normal file
@@ -0,0 +1,248 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
|
||||
.titlebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
padding-left: 70px; /* Space for macOS window controls */
|
||||
transition: padding-left 0.15s ease;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so reduce padding */
|
||||
&.fullscreen .titlebar-content {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
/* Remove drag region from interactive elements */
|
||||
.workspace-name-container,
|
||||
.dropdown-item,
|
||||
.home-button,
|
||||
.env-selector-trigger,
|
||||
.dropdown,
|
||||
button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Left section */
|
||||
.titlebar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so remove margin-left */
|
||||
&.fullscreen .titlebar-left {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* Home button */
|
||||
.home-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
/* Workspace Name Dropdown Trigger */
|
||||
.workspace-name-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
flex-shrink: 0;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Center section - Bruno branding */
|
||||
.titlebar-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
|
||||
.bruno-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right section */
|
||||
.titlebar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Action buttons in right section */
|
||||
.titlebar-action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
}
|
||||
}
|
||||
|
||||
/* Draggable region */
|
||||
.drag-region {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
/* Workspace Dropdown Styles */
|
||||
.workspace-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 10px !important;
|
||||
margin: 0 !important;
|
||||
|
||||
&.active {
|
||||
.check-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin-btn:not(.pinned) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.workspace-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
opacity: 0;
|
||||
|
||||
&.pinned {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjust for non-macOS platforms */
|
||||
body:not(.os-mac) & {
|
||||
.titlebar-content {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Leave room for Windows caption buttons when the overlay is enabled */
|
||||
body.os-windows & {
|
||||
.titlebar-content {
|
||||
padding-right: 120px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
244
packages/bruno-app/src/components/AppTitleBar/index.js
Normal file
244
packages/bruno-app/src/components/AppTitleBar/index.js
Normal file
@@ -0,0 +1,244 @@
|
||||
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconLayoutColumns, IconLayoutRows, IconPin, IconPinned, IconPlus } from '@tabler/icons';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useRef, 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 { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
|
||||
|
||||
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { toTitleCase } from 'utils/common/index';
|
||||
|
||||
const AppTitleBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
|
||||
// Listen for fullscreen changes
|
||||
useEffect(() => {
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => {
|
||||
setIsFullScreen(true);
|
||||
});
|
||||
|
||||
const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => {
|
||||
setIsFullScreen(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeEnterFullScreenListener();
|
||||
removeLeaveFullScreenListener();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Get workspace info
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
// Sort workspaces according to preferences
|
||||
const sortedWorkspaces = useMemo(() => {
|
||||
return sortWorkspaces(workspaces, preferences);
|
||||
}, [workspaces, preferences]);
|
||||
|
||||
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
|
||||
const [showWorkspaceDropdown, setShowWorkspaceDropdown] = useState(false);
|
||||
const workspaceDropdownTippyRef = useRef();
|
||||
const onWorkspaceDropdownCreate = (ref) => (workspaceDropdownTippyRef.current = ref);
|
||||
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" onClick={() => setShowWorkspaceDropdown(!showWorkspaceDropdown)}>
|
||||
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
|
||||
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleHomeClick = () => {
|
||||
dispatch(showHomePage());
|
||||
};
|
||||
|
||||
const handleWorkspaceSwitch = (workspaceUid) => {
|
||||
dispatch(switchWorkspace(workspaceUid));
|
||||
setShowWorkspaceDropdown(false);
|
||||
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
|
||||
};
|
||||
|
||||
const handleOpenWorkspace = async () => {
|
||||
setShowWorkspaceDropdown(false);
|
||||
try {
|
||||
await dispatch(openWorkspaceDialog());
|
||||
toast.success('Workspace opened successfully');
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Failed to open workspace');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateWorkspace = () => {
|
||||
setShowWorkspaceDropdown(false);
|
||||
setCreateWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePinWorkspace = useCallback((workspaceUid, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const newPreferences = toggleWorkspacePin(workspaceUid, preferences);
|
||||
dispatch(savePreferences(newPreferences));
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal';
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
};
|
||||
|
||||
const handleToggleDevtools = () => {
|
||||
if (isConsoleOpen) {
|
||||
dispatch(closeConsole());
|
||||
} else {
|
||||
dispatch(openConsole());
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleVerticalLayout = () => {
|
||||
const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
layout: {
|
||||
...preferences?.layout || {},
|
||||
responsePaneOrientation: newOrientation
|
||||
}
|
||||
};
|
||||
dispatch(savePreferences(updatedPreferences));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`app-titlebar ${isFullScreen ? 'fullscreen' : ''}`}>
|
||||
{createWorkspaceModalOpen && (
|
||||
<CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
|
||||
<div className="titlebar-content">
|
||||
{/* Left section: Home + Workspace */}
|
||||
<div className="titlebar-left">
|
||||
<button className="home-button" onClick={handleHomeClick} title="Home">
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</button>
|
||||
|
||||
{/* Workspace Dropdown */}
|
||||
<Dropdown
|
||||
onCreate={onWorkspaceDropdownCreate}
|
||||
icon={<WorkspaceName />}
|
||||
placement="bottom-start"
|
||||
style="new"
|
||||
visible={showWorkspaceDropdown}
|
||||
onClickOutside={() => setShowWorkspaceDropdown(false)}
|
||||
>
|
||||
{sortedWorkspaces.map((workspace) => {
|
||||
const isActive = workspace.uid === activeWorkspaceUid;
|
||||
const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={workspace.uid}
|
||||
className={`dropdown-item workspace-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => handleWorkspaceSwitch(workspace.uid)}
|
||||
>
|
||||
<span className="workspace-name">{toTitleCase(workspace.name)}</span>
|
||||
<div className="workspace-actions">
|
||||
{workspace.type !== 'default' && (
|
||||
<button
|
||||
className={`pin-btn ${isPinned ? 'pinned' : ''}`}
|
||||
onClick={(e) => handlePinWorkspace(workspace.uid, e)}
|
||||
title={isPinned ? 'Unpin workspace' : 'Pin workspace'}
|
||||
>
|
||||
{isPinned ? (
|
||||
<IconPinned size={14} stroke={1.5} />
|
||||
) : (
|
||||
<IconPin size={14} stroke={1.5} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="label-item border-top">Workspaces</div>
|
||||
|
||||
<div className="dropdown-item" onClick={handleCreateWorkspace}>
|
||||
<IconPlus size={16} stroke={1.5} className="icon" />
|
||||
Create workspace
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={handleOpenWorkspace}>
|
||||
<IconFolder size={16} stroke={1.5} className="icon" />
|
||||
Open workspace
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{/* Center section: Bruno logo + text */}
|
||||
<div className="titlebar-center">
|
||||
<Bruno width={18} />
|
||||
<span className="bruno-text">Bruno</span>
|
||||
</div>
|
||||
|
||||
{/* Right section: Action buttons */}
|
||||
<div className="titlebar-right">
|
||||
{/* Toggle sidebar */}
|
||||
<button
|
||||
className="titlebar-action-button"
|
||||
onClick={handleToggleSidebar}
|
||||
title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
|
||||
aria-label="Toggle Sidebar"
|
||||
data-testid="toggle-sidebar-button"
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{/* Toggle devtools */}
|
||||
<button
|
||||
className="titlebar-action-button"
|
||||
onClick={handleToggleDevtools}
|
||||
title={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
|
||||
aria-label="Toggle Devtools"
|
||||
data-testid="toggle-devtools-button"
|
||||
>
|
||||
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{/* Toggle vertical layout */}
|
||||
<button
|
||||
className="titlebar-action-button"
|
||||
onClick={handleToggleVerticalLayout}
|
||||
title={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
|
||||
aria-label="Toggle Vertical Layout"
|
||||
data-testid="toggle-vertical-layout-button"
|
||||
>
|
||||
{orientation === 'horizontal' ? (
|
||||
<IconLayoutColumns size={16} stroke={1.5} />
|
||||
) : (
|
||||
<IconLayoutRows size={16} stroke={1.5} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTitleBar;
|
||||
@@ -373,7 +373,7 @@ const ClientCertSettings = ({ collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex flex-row gap-2 items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" data-testid="add-client-cert">
|
||||
Add
|
||||
</button>
|
||||
<div className="h-4 border-l border-gray-600"></div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
const IconBottombarToggle = ({ 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" fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" className={`icon icon-tabler icons-tabler-outline icon-tabler-layout-bottombar ${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="M4 15l16 0" />
|
||||
{!collapsed && (
|
||||
<rect x="4.6" y="15.6" width="14.8" height="2.8" rx="0.8" fill="currentColor" />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconBottombarToggle;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { uuid } from 'utils/common';
|
||||
import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
|
||||
import { IconBox, IconRun, IconEye, IconSettings } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -45,7 +45,7 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center py-2 px-4">
|
||||
<div className="flex flex-1 items-center cursor-pointer hover:underline" onClick={viewCollectionSettings}>
|
||||
<IconFiles size={18} strokeWidth={1.5} />
|
||||
<IconBox size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-medium">{collection?.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-3 items-center justify-end">
|
||||
|
||||
@@ -5,7 +5,7 @@ const Wrapper = styled.div`
|
||||
height: 1.75rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding-left: 8px;
|
||||
padding-left: 4px;
|
||||
border: ${(props) => props.theme.dragAndDrop.borderStyle} transparent;
|
||||
|
||||
.rotate-90 {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
margin: 4px 10px 8px 10px;
|
||||
position: relative;
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 32px 0 32px;
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
border-color: ${(props) => props.theme.sidebar.muted}40;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
border-color: ${(props) => props.theme.sidebar.muted}80;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,39 +1,28 @@
|
||||
import { IconSearch, IconX } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionSearch = ({ searchText, setSearchText }) => {
|
||||
return (
|
||||
<div className="relative collection-filter px-2">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<span className="text-gray-500 sm:text-sm">
|
||||
<IconSearch size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
</div>
|
||||
<StyledWrapper>
|
||||
<IconSearch size={14} strokeWidth={1.5} className="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Search requests …"
|
||||
placeholder="Search requests..."
|
||||
id="search"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="block w-full pl-7 pr-8 py-1 sm:text-sm"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
|
||||
/>
|
||||
{searchText !== '' && (
|
||||
<div className="absolute inset-y-0 right-0 pr-4 flex items-center">
|
||||
<span
|
||||
className="close-icon"
|
||||
onClick={() => {
|
||||
setSearchText('');
|
||||
}}
|
||||
>
|
||||
<IconX size={16} strokeWidth={1.5} className="cursor-pointer" />
|
||||
</span>
|
||||
<div className="clear-icon" onClick={() => setSearchText('')}>
|
||||
<IconX size={14} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.collections-badge {
|
||||
margin-inline: 0.5rem;
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 5px;
|
||||
|
||||
.caret {
|
||||
margin-left: 0.25rem;
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
|
||||
.collections-header-actions {
|
||||
.collection-action-button {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,86 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IconArrowsSort, IconFolders, IconSortAscendingLetters, IconSortDescendingLetters } from '@tabler/icons';
|
||||
import CloseAllIcon from 'components/Icons/CloseAll';
|
||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/index';
|
||||
import RemoveCollectionsModal from '../RemoveCollectionsModal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionsHeader = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const { collectionSortOrder } = useSelector((state) => state.collections);
|
||||
const [collectionsToClose, setCollectionsToClose] = useState([]);
|
||||
|
||||
const sortCollectionOrder = () => {
|
||||
let order;
|
||||
switch (collectionSortOrder) {
|
||||
case 'default':
|
||||
order = 'alphabetical';
|
||||
break;
|
||||
case 'alphabetical':
|
||||
order = 'reverseAlphabetical';
|
||||
break;
|
||||
case 'reverseAlphabetical':
|
||||
order = 'default';
|
||||
break;
|
||||
}
|
||||
dispatch(sortCollections({ order }));
|
||||
};
|
||||
|
||||
let sortIcon;
|
||||
if (collectionSortOrder === 'default') {
|
||||
sortIcon = <IconArrowsSort size={18} strokeWidth={1.5} />;
|
||||
} else if (collectionSortOrder === 'alphabetical') {
|
||||
sortIcon = <IconSortAscendingLetters size={18} strokeWidth={1.5} />;
|
||||
} else {
|
||||
sortIcon = <IconSortDescendingLetters size={18} strokeWidth={1.5} />;
|
||||
}
|
||||
|
||||
const selectAllCollectionsToClose = () => {
|
||||
setCollectionsToClose(collections.map((c) => c.uid));
|
||||
};
|
||||
|
||||
const clearCollectionsToClose = () => {
|
||||
setCollectionsToClose([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="collections-badge flex items-center justify-between px-2 mt-2 relative">
|
||||
<div className="flex items-center py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
</div>
|
||||
{collections.length >= 1 && (
|
||||
<div className="flex items-center collections-header-actions">
|
||||
<button
|
||||
className="mr-1 collection-action-button"
|
||||
onClick={selectAllCollectionsToClose}
|
||||
aria-label="Close all collections"
|
||||
title="Close all collections"
|
||||
data-testid="close-all-collections-button"
|
||||
>
|
||||
<CloseAllIcon size={18} strokeWidth={1.5} className="cursor-pointer" />
|
||||
</button>
|
||||
<button
|
||||
className="collection-action-button"
|
||||
onClick={() => sortCollectionOrder()}
|
||||
aria-label="Sort collections"
|
||||
title="Sort collections"
|
||||
>
|
||||
{sortIcon}
|
||||
</button>
|
||||
{collectionsToClose.length > 0 && (
|
||||
<RemoveCollectionsModal collectionUids={collectionsToClose} onClose={clearCollectionsToClose} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionsHeader;
|
||||
@@ -1,12 +1,35 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
span.close-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding-top: 4px;
|
||||
|
||||
&:hover .collections-badge .collections-header-actions .collection-action-button {
|
||||
opacity: 1;
|
||||
.collections-list {
|
||||
min-height: 0;
|
||||
padding: 0 4px;
|
||||
padding-top: 4px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: ${(props) => props.theme.scrollbar.color};
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: ${(props) => props.theme.scrollbar.color};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||
import CollectionSearch from './CollectionSearch/index';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const Collections = ({ showSearch }) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
@@ -14,13 +15,12 @@ const Collections = ({ showSearch }) => {
|
||||
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid) || workspaces.find((w) => w.type === 'default');
|
||||
|
||||
let workspaceCollections = [];
|
||||
|
||||
if (activeWorkspace?.collections?.length) {
|
||||
workspaceCollections = activeWorkspace.collections.map((wc) => {
|
||||
return collections.find((c) => c.pathname === wc.path);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
const workspaceCollections = useMemo(() => {
|
||||
if (!activeWorkspace) return [];
|
||||
return collections.filter((c) =>
|
||||
activeWorkspace.collections?.some((wc) => wc.path === c.pathname)
|
||||
);
|
||||
}, [activeWorkspace, collections]);
|
||||
|
||||
if (!workspaceCollections || !workspaceCollections.length) {
|
||||
return (
|
||||
@@ -42,7 +42,7 @@ const Collections = ({ showSearch }) => {
|
||||
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
|
||||
)}
|
||||
|
||||
<div className={`mt-4 flex flex-col overflow-hidden hover:overflow-y-auto absolute ${showSearch ? 'top-16' : 'top-8'} bottom-0 left-0 right-0`}>
|
||||
<div className="collections-list flex flex-col flex-1 overflow-hidden hover:overflow-y-auto">
|
||||
{workspaceCollections && workspaceCollections.length
|
||||
? workspaceCollections.map((c) => {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
padding: 8px 4px 6px 10px;
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Section Title (single view mode) - with separator */
|
||||
&.single-view {
|
||||
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
/* Section Title (single view mode) */
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 2px 0;
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
}
|
||||
}
|
||||
|
||||
/* View Tabs (multi-view mode) */
|
||||
.view-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.view-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 280px) {
|
||||
span {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Header Actions */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown?.hoverBg || props.theme.sidebar?.collection?.item?.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown?.mutedText || props.theme.text?.muted || '#888'};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -9,6 +9,7 @@ import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
|
||||
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
|
||||
import { toTitleCase } from 'utils/common/index';
|
||||
|
||||
const WorkspaceSelector = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,15 +28,6 @@ const WorkspaceSelector = () => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const toTitleCase = (str) => {
|
||||
if (!str) return '';
|
||||
if (str === 'default') return 'Default';
|
||||
return str
|
||||
.split(/[\s-_]+/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" onClick={() => setShowDropdown(!showDropdown)}>
|
||||
337
packages/bruno-app/src/components/Sidebar/SidebarHeader/index.js
Normal file
337
packages/bruno-app/src/components/Sidebar/SidebarHeader/index.js
Normal file
@@ -0,0 +1,337 @@
|
||||
import {
|
||||
IconArrowsSort,
|
||||
IconBox,
|
||||
IconDeviceDesktop,
|
||||
IconDotsVertical,
|
||||
IconDownload,
|
||||
IconFolder,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
IconSortAscendingLetters,
|
||||
IconSortDescendingLetters,
|
||||
IconSquareX,
|
||||
IconTrash
|
||||
} from '@tabler/icons';
|
||||
import { useRef, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { importCollection, openCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { importCollectionInWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
|
||||
import RemoveCollectionsModal from '../Collections/RemoveCollectionsModal/index';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const VIEW_TABS = [
|
||||
{ id: 'collections', label: 'Collections', icon: IconBox }
|
||||
];
|
||||
|
||||
const SidebarHeader = ({ setShowSearch, activeView = 'collections', onViewChange }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
// Get collection sort order
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const { collectionSortOrder } = useSelector((state) => state.collections);
|
||||
const [collectionsToClose, setCollectionsToClose] = useState([]);
|
||||
|
||||
const [importData, setImportData] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
|
||||
const handleImportCollection = ({ rawData, type }) => {
|
||||
setImportCollectionModalOpen(false);
|
||||
|
||||
if (activeWorkspace && activeWorkspace.type !== 'default') {
|
||||
dispatch(importCollectionInWorkspace(rawData, activeWorkspace.uid, undefined, type))
|
||||
.catch((err) => {
|
||||
toast.error('An error occurred while importing the collection');
|
||||
});
|
||||
} else {
|
||||
setImportData({ rawData, type });
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (convertedCollection, collectionLocation) => {
|
||||
dispatch(importCollection(convertedCollection, collectionLocation))
|
||||
.then(() => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportData(null);
|
||||
toast.success('Collection imported successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
toast.error('An error occurred while importing the collection');
|
||||
});
|
||||
};
|
||||
|
||||
const addDropdownTippyRef = useRef();
|
||||
const onAddDropdownCreate = (ref) => (addDropdownTippyRef.current = ref);
|
||||
|
||||
const actionsDropdownTippyRef = useRef();
|
||||
const onActionsDropdownCreate = (ref) => (actionsDropdownTippyRef.current = ref);
|
||||
|
||||
const handleToggleSearch = () => {
|
||||
if (setShowSearch) {
|
||||
setShowSearch((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSortCollections = () => {
|
||||
let order;
|
||||
switch (collectionSortOrder) {
|
||||
case 'default':
|
||||
order = 'alphabetical';
|
||||
break;
|
||||
case 'alphabetical':
|
||||
order = 'reverseAlphabetical';
|
||||
break;
|
||||
case 'reverseAlphabetical':
|
||||
order = 'default';
|
||||
break;
|
||||
default:
|
||||
order = 'default';
|
||||
break;
|
||||
}
|
||||
dispatch(sortCollections({ order }));
|
||||
};
|
||||
|
||||
const getSortIcon = () => {
|
||||
switch (collectionSortOrder) {
|
||||
case 'alphabetical':
|
||||
return IconSortDescendingLetters;
|
||||
case 'reverseAlphabetical':
|
||||
return IconArrowsSort;
|
||||
default:
|
||||
return IconSortAscendingLetters;
|
||||
}
|
||||
};
|
||||
|
||||
const getSortLabel = () => {
|
||||
switch (collectionSortOrder) {
|
||||
case 'alphabetical':
|
||||
return 'Sort Z-A';
|
||||
case 'reverseAlphabetical':
|
||||
return 'Clear sort';
|
||||
default:
|
||||
return 'Sort A-Z';
|
||||
}
|
||||
};
|
||||
|
||||
const selectAllCollectionsToClose = () => {
|
||||
setCollectionsToClose(collections.map((c) => c.uid));
|
||||
};
|
||||
|
||||
const clearCollectionsToClose = () => {
|
||||
setCollectionsToClose([]);
|
||||
};
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
const options = {};
|
||||
if (activeWorkspace?.pathname) {
|
||||
options.workspaceId = activeWorkspace.pathname;
|
||||
}
|
||||
|
||||
dispatch(openCollection(options)).catch((err) => {
|
||||
toast.error('An error occurred while opening the collection');
|
||||
});
|
||||
};
|
||||
|
||||
const renderModals = () => (
|
||||
<>
|
||||
{createCollectionModalOpen && (
|
||||
<CreateCollection
|
||||
onClose={() => setCreateCollectionModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{importCollectionModalOpen && (
|
||||
<ImportCollection
|
||||
onClose={() => setImportCollectionModalOpen(false)}
|
||||
handleSubmit={handleImportCollection}
|
||||
/>
|
||||
)}
|
||||
{importCollectionLocationModalOpen && importData && (
|
||||
<ImportCollectionLocation
|
||||
rawData={importData.rawData}
|
||||
format={importData.type}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const isSingleView = VIEW_TABS.length === 1;
|
||||
|
||||
// Render Collections-specific actions
|
||||
const renderCollectionsActions = () => (
|
||||
<>
|
||||
<button
|
||||
className="action-button"
|
||||
onClick={handleToggleSearch}
|
||||
title="Search requests"
|
||||
>
|
||||
<IconSearch size={14} stroke={1.5} />
|
||||
</button>
|
||||
{/* Add/Create dropdown */}
|
||||
<Dropdown
|
||||
onCreate={onAddDropdownCreate}
|
||||
icon={(
|
||||
<button className="action-button plus-icon-button" title="Add new">
|
||||
<IconPlus size={14} stroke={1.5} />
|
||||
</button>
|
||||
)}
|
||||
placement="bottom-end"
|
||||
style="new"
|
||||
>
|
||||
<div className="label-item">Collections</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
setCreateCollectionModalOpen(true);
|
||||
addDropdownTippyRef.current?.hide();
|
||||
}}
|
||||
>
|
||||
<IconPlus size={16} stroke={1.5} className="icon" />
|
||||
Create collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
addDropdownTippyRef.current?.hide();
|
||||
setImportCollectionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<IconDownload size={16} stroke={1.5} className="icon" />
|
||||
Import collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
handleOpenCollection();
|
||||
addDropdownTippyRef.current?.hide();
|
||||
}}
|
||||
>
|
||||
<IconFolder size={16} stroke={1.5} className="icon" />
|
||||
Open collection
|
||||
</div>
|
||||
|
||||
</Dropdown>
|
||||
|
||||
{/* Actions dropdown (sort, close all, etc.) */}
|
||||
<Dropdown
|
||||
onCreate={onActionsDropdownCreate}
|
||||
icon={(
|
||||
<button className="action-button" title="More actions">
|
||||
<IconDotsVertical size={14} stroke={1.5} />
|
||||
</button>
|
||||
)}
|
||||
placement="bottom-end"
|
||||
style="new"
|
||||
>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
handleSortCollections();
|
||||
actionsDropdownTippyRef.current?.hide();
|
||||
}}
|
||||
aria-label="Sort collections"
|
||||
title="Sort collections"
|
||||
data-testid="sort-collections-button"
|
||||
>
|
||||
{(() => {
|
||||
const SortIcon = getSortIcon();
|
||||
return <SortIcon size={16} stroke={1.5} className="icon" />;
|
||||
})()}
|
||||
{getSortLabel()}
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
selectAllCollectionsToClose();
|
||||
actionsDropdownTippyRef.current?.hide();
|
||||
}}
|
||||
aria-label="Close all collections"
|
||||
title="Close all collections"
|
||||
data-testid="close-all-collections-button"
|
||||
>
|
||||
<IconSquareX size={16} stroke={1.5} className="icon" />
|
||||
Close all
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
{collectionsToClose.length > 0 && (
|
||||
<RemoveCollectionsModal collectionUids={collectionsToClose} onClose={clearCollectionsToClose} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// Render Second Tab-specific actions
|
||||
const renderSecondTabActions = () => (
|
||||
<>
|
||||
{/* Add second tab actions here */}
|
||||
</>
|
||||
);
|
||||
|
||||
// Render the view switcher - either tabs or single title
|
||||
const renderViewSwitcher = () => {
|
||||
if (isSingleView) {
|
||||
// Single view - just show the title
|
||||
const tab = VIEW_TABS[0];
|
||||
const TabIcon = tab.icon;
|
||||
return (
|
||||
<div className="section-title">
|
||||
<TabIcon size={14} stroke={1.5} />
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Multiple views - show segmented tabs
|
||||
return (
|
||||
<div className="view-tabs">
|
||||
{VIEW_TABS.map((tab) => {
|
||||
const TabIcon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`view-tab ${activeView === tab.id ? 'active' : ''}`}
|
||||
onClick={() => onViewChange?.(tab.id)}
|
||||
title={tab.label}
|
||||
>
|
||||
<TabIcon size={14} stroke={1.5} />
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={isSingleView ? 'single-view' : ''}>
|
||||
{renderModals()}
|
||||
<div className="sidebar-header">
|
||||
{renderViewSwitcher()}
|
||||
|
||||
{/* Action Buttons - Context Sensitive */}
|
||||
<div className="header-actions">
|
||||
{activeView === 'collections' ? renderCollectionsActions() : renderSecondTabActions()}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarHeader;
|
||||
@@ -28,18 +28,6 @@ const Wrapper = styled.div`
|
||||
top: -0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.collection-filter {
|
||||
input {
|
||||
border: ${(props) => props.theme.sidebar.search.border};
|
||||
border-radius: 2px;
|
||||
background-color: ${(props) => props.theme.sidebar.search.bg};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.sidebar-drag-handle {
|
||||
@@ -65,6 +53,15 @@ const Wrapper = styled.div`
|
||||
border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.activeBorder};
|
||||
}
|
||||
}
|
||||
|
||||
.second-tab-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.titlebar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workspace-name-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 10px;
|
||||
margin-left: 0px;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
max-width: 120px;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
flex-shrink: 0;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Actions Button */
|
||||
.actions-container {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.home-icon-button,
|
||||
.search-icon-button,
|
||||
.plus-icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.workspace-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 10px !important;
|
||||
margin: 0 !important;
|
||||
|
||||
&.active {
|
||||
.check-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin-btn:not(.pinned) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.workspace-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow || '#f0c674'};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.dropdown?.mutedText || props.theme.text?.muted || '#888'};
|
||||
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
opacity: 0;
|
||||
|
||||
&.pinned {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown?.hoverBg || props.theme.sidebar?.collection?.item?.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown?.mutedText || props.theme.text?.muted || '#888'};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collection-dropdown {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.tippy-box {
|
||||
top: -0.5rem;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,181 +0,0 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconPlus, IconFolder, IconDownload, IconHome, IconSearch, IconDeviceDesktop } from '@tabler/icons';
|
||||
|
||||
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { importCollectionInWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import WorkspaceSelector from './WorkspaceSelector';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TitleBar = ({ showSearch, setShowSearch }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
const [importData, setImportData] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
|
||||
const actionsDropdownTippyRef = useRef();
|
||||
const onActionsDropdownCreate = (ref) => (actionsDropdownTippyRef.current = ref);
|
||||
|
||||
const handleImportCollection = ({ rawData, type }) => {
|
||||
setImportCollectionModalOpen(false);
|
||||
|
||||
if (activeWorkspace && activeWorkspace.type !== 'default') {
|
||||
dispatch(importCollectionInWorkspace(rawData, activeWorkspace.uid, undefined, type))
|
||||
.catch((err) => {
|
||||
toast.error('An error occurred while importing the collection');
|
||||
});
|
||||
} else {
|
||||
setImportData({ rawData, type });
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (convertedCollection, collectionLocation) => {
|
||||
dispatch(importCollection(convertedCollection, collectionLocation))
|
||||
.then(() => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportData(null);
|
||||
toast.success('Collection imported successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
toast.error('An error occurred while importing the collection');
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleSearch = () => {
|
||||
if (setShowSearch) {
|
||||
setShowSearch((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
const options = {};
|
||||
if (activeWorkspace?.pathname) {
|
||||
options.workspaceId = activeWorkspace.pathname;
|
||||
}
|
||||
|
||||
dispatch(openCollection(options)).catch((err) => {
|
||||
toast.error('An error occurred while opening the collection');
|
||||
});
|
||||
};
|
||||
|
||||
const openDevTools = () => {
|
||||
ipcRenderer.invoke('renderer:open-devtools');
|
||||
};
|
||||
|
||||
const renderModals = () => (
|
||||
<>
|
||||
{createCollectionModalOpen && (
|
||||
<CreateCollection
|
||||
onClose={() => setCreateCollectionModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{importCollectionModalOpen && (
|
||||
<ImportCollection
|
||||
onClose={() => setImportCollectionModalOpen(false)}
|
||||
handleSubmit={handleImportCollection}
|
||||
/>
|
||||
)}
|
||||
{importCollectionLocationModalOpen && importData && (
|
||||
<ImportCollectionLocation
|
||||
rawData={importData.rawData}
|
||||
format={importData.type}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 py-2">
|
||||
{renderModals()}
|
||||
<div className="titlebar-container">
|
||||
<WorkspaceSelector />
|
||||
|
||||
<div className="actions-container">
|
||||
<button className="home-icon-button" onClick={() => dispatch(showHomePage())} title="Home">
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</button>
|
||||
|
||||
{setShowSearch && (
|
||||
<button className="search-icon-button" onClick={handleToggleSearch} title="Toggle search">
|
||||
<IconSearch size={16} stroke={1.5} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Dropdown
|
||||
onCreate={onActionsDropdownCreate}
|
||||
icon={(
|
||||
<button className="plus-icon-button">
|
||||
<IconPlus size={16} stroke={1.5} />
|
||||
</button>
|
||||
)}
|
||||
placement="bottom-end"
|
||||
style="new"
|
||||
>
|
||||
<div className="label-item">Collections</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
setCreateCollectionModalOpen(true);
|
||||
actionsDropdownTippyRef.current?.hide();
|
||||
}}
|
||||
>
|
||||
<IconPlus size={16} stroke={1.5} className="icon" />
|
||||
Create collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
actionsDropdownTippyRef.current?.hide();
|
||||
setImportCollectionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<IconDownload size={16} stroke={1.5} className="icon" />
|
||||
Import collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
handleOpenCollection();
|
||||
actionsDropdownTippyRef.current?.hide();
|
||||
}}
|
||||
>
|
||||
<IconFolder size={16} stroke={1.5} className="icon" />
|
||||
Open collection
|
||||
</div>
|
||||
<div className="dropdown-separator"></div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
actionsDropdownTippyRef.current?.hide();
|
||||
openDevTools();
|
||||
}}
|
||||
>
|
||||
<IconDeviceDesktop size={16} stroke={1.5} className="icon" />
|
||||
Devtools
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleBar;
|
||||
@@ -1,4 +1,4 @@
|
||||
import TitleBar from './TitleBar';
|
||||
import SidebarHeader from './SidebarHeader';
|
||||
import Collections from './Collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 221;
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 220;
|
||||
const MAX_LEFT_SIDEBAR_WIDTH = 600;
|
||||
|
||||
const Sidebar = () => {
|
||||
@@ -14,6 +14,8 @@ const Sidebar = () => {
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
|
||||
const lastWidthRef = useRef(leftSidebarWidth);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [activeView, setActiveView] = useState('collections'); // 'collections' or any other future tab
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
@@ -80,9 +82,21 @@ const Sidebar = () => {
|
||||
<aside className="sidebar" 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">
|
||||
<TitleBar />
|
||||
<Collections />
|
||||
<div className="flex flex-col flex-grow" style={{ minHeight: 0, overflow: 'hidden' }}>
|
||||
<SidebarHeader
|
||||
setShowSearch={setShowSearch}
|
||||
activeView={activeView}
|
||||
onViewChange={setActiveView}
|
||||
/>
|
||||
{activeView === 'collections' ? (
|
||||
<Collections showSearch={showSearch} />
|
||||
) : (
|
||||
<div className="second-tab-placeholder">
|
||||
<p className="text-center text-muted py-8 px-4 text-sm opacity-60">
|
||||
Second tab content will appear here
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import Cookies from 'components/Cookies';
|
||||
import Notifications from 'components/Notifications';
|
||||
import Portal from 'components/Portal';
|
||||
import { showPreferences, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { showPreferences } from 'providers/ReduxStore/slices/app';
|
||||
import { openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { useApp } from 'providers/App';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -18,7 +18,6 @@ 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();
|
||||
|
||||
@@ -70,16 +69,6 @@ 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 preferences-button"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { showInFolder, openCollection } from 'providers/ReduxStore/slices/collec
|
||||
import toast from 'react-hot-toast';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import CloseWorkspace from 'components/Sidebar/TitleBar/CloseWorkspace';
|
||||
import CloseWorkspace from 'components/Sidebar/SidebarHeader/CloseWorkspace';
|
||||
import WorkspaceCollections from './WorkspaceCollections';
|
||||
import WorkspaceDocs from './WorkspaceDocs';
|
||||
import WorkspaceEnvironments from './WorkspaceEnvironments';
|
||||
|
||||
@@ -5,6 +5,7 @@ import RequestTabs from 'components/RequestTabs';
|
||||
import RequestTabPanel from 'components/RequestTabPanel';
|
||||
import Sidebar from 'components/Sidebar';
|
||||
import StatusBar from 'components/StatusBar';
|
||||
import AppTitleBar from 'components/AppTitleBar';
|
||||
// import ErrorCapture from 'components/ErrorCapture';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
@@ -87,6 +88,7 @@ export default function Main() {
|
||||
return (
|
||||
// <ErrorCapture>
|
||||
<div id="main-container" className="flex flex-col h-screen max-h-screen overflow-hidden">
|
||||
<AppTitleBar />
|
||||
{showRosettaBanner ? (
|
||||
<Portal>
|
||||
<div className="fixed bottom-0 left-0 right-0 z-10 bg-amber-100 border border-amber-400 text-amber-700 px-4 py-3" role="alert">
|
||||
@@ -105,7 +107,7 @@ export default function Main() {
|
||||
className="flex-1 min-h-0 flex"
|
||||
data-app-state="loading"
|
||||
style={{
|
||||
height: isConsoleOpen ? `calc(100vh - 22px - ${isConsoleOpen ? '300px' : '0px'})` : 'calc(100vh - 22px)'
|
||||
height: isConsoleOpen ? `calc(100vh - 60px - ${isConsoleOpen ? '300px' : '0px'})` : 'calc(100vh - 60px)'
|
||||
}}
|
||||
>
|
||||
<StyledWrapper className={className} style={{ height: '100%', zIndex: 1 }}>
|
||||
|
||||
@@ -20,9 +20,24 @@ export const AppProvider = (props) => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const platform = get(navigator, 'platform', '');
|
||||
if (platform && platform.toLowerCase().indexOf('mac') > -1) {
|
||||
const platform = get(navigator, 'platform', '').toLowerCase();
|
||||
|
||||
if (!platform) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (platform.includes('mac')) {
|
||||
document.body.classList.add('os-mac');
|
||||
return;
|
||||
}
|
||||
|
||||
if (platform.includes('win')) {
|
||||
document.body.classList.add('os-windows');
|
||||
return;
|
||||
}
|
||||
|
||||
if (platform.includes('linux')) {
|
||||
document.body.classList.add('os-linux');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import brunoClipboard from 'utils/bruno-clipboard';
|
||||
const initialState = {
|
||||
isDragging: false,
|
||||
idbConnectionReady: false,
|
||||
leftSidebarWidth: 222,
|
||||
leftSidebarWidth: 250,
|
||||
sidebarCollapsed: false,
|
||||
screenWidth: 500,
|
||||
showHomePage: false,
|
||||
|
||||
@@ -338,3 +338,23 @@ export const prettifyJsonString = (jsonDataString) => {
|
||||
}
|
||||
return jsonDataString;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the given string value converted to title case.
|
||||
* - If the value is falsy, returns an empty string.
|
||||
* - Special-case: if the value is 'default', returns 'Default'.
|
||||
* - Otherwise, splits the string on whitespace, hyphens, or underscores,
|
||||
* uppercases the first letter of each word, and lowercases the rest.
|
||||
*
|
||||
* @param {string} str - The input string to convert.
|
||||
* @returns {string} - The converted title-case string.
|
||||
*/
|
||||
|
||||
export const toTitleCase = (str) => {
|
||||
if (!str) return '';
|
||||
if (str === 'default') return 'Default';
|
||||
return str
|
||||
.split(/[\s-_]+/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
@@ -78,6 +78,8 @@ const contentSecurityPolicy = [
|
||||
setContentSecurityPolicy(contentSecurityPolicy.join(';') + ';');
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
let mainWindow;
|
||||
|
||||
@@ -118,7 +120,11 @@ app.on('ready', async () => {
|
||||
webviewTag: true
|
||||
},
|
||||
title: 'Bruno',
|
||||
icon: path.join(__dirname, 'about/256x256.png')
|
||||
icon: path.join(__dirname, 'about/256x256.png'),
|
||||
// Custom title bar – ensure React titlebar occupies the window chrome on all OSes
|
||||
titleBarStyle: isMac ? 'hiddenInset' : isWindows ? 'hidden' : 'default',
|
||||
titleBarOverlay: isWindows ? { height: 36 } : undefined,
|
||||
trafficLightPosition: isMac ? { x: 12, y: 10 } : undefined
|
||||
// we will bring this back
|
||||
// see https://github.com/usebruno/bruno/issues/440
|
||||
// autoHideMenuBar: true
|
||||
@@ -166,6 +172,15 @@ app.on('ready', async () => {
|
||||
|
||||
mainWindow.on('maximize', () => saveMaximized(true));
|
||||
mainWindow.on('unmaximize', () => saveMaximized(false));
|
||||
|
||||
// Full screen events for title bar padding adjustment
|
||||
mainWindow.on('enter-full-screen', () => {
|
||||
mainWindow.webContents.send('main:enter-full-screen');
|
||||
});
|
||||
mainWindow.on('leave-full-screen', () => {
|
||||
mainWindow.webContents.send('main:leave-full-screen');
|
||||
});
|
||||
|
||||
mainWindow.on('close', (e) => {
|
||||
e.preventDefault();
|
||||
terminalManager.cleanup(mainWindow.webContents);
|
||||
|
||||
@@ -146,7 +146,7 @@ test.describe('Draft indicator in collection and folder settings', () => {
|
||||
await keyFileChooser.setFiles('./tests/collection/draft/fixtures/grpcbin.proto');
|
||||
|
||||
// Click Add button
|
||||
await page.getByRole('button', { name: 'Add' }).click();
|
||||
await page.getByTestId('add-client-cert').click();
|
||||
|
||||
// Verify draft indicator appears
|
||||
await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();
|
||||
|
||||
@@ -4,7 +4,7 @@ test.describe('Sidebar Toggle', () => {
|
||||
test('should toggle sidebar visibility when clicking the toggle button', async ({ page }) => {
|
||||
// Get the sidebar and toggle button elements
|
||||
const sidebar = page.locator('aside.sidebar');
|
||||
const toggleButton = page.getByLabel('Toggle Sidebar');
|
||||
const toggleButton = page.getByTestId('toggle-sidebar-button');
|
||||
const dragHandle = page.locator('.sidebar-drag-handle');
|
||||
|
||||
// Initial state - sidebar and drag handle should be visible
|
||||
|
||||
@@ -23,7 +23,7 @@ test.describe('Response Pane Actions', () => {
|
||||
await page.getByPlaceholder('Request Name').fill('copy-test');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
// Using httpbin.org for a simple JSON response
|
||||
await page.locator('textarea').fill('https://httpbin.org/json');
|
||||
await page.locator('textarea').fill('https://www.httpfaker.org/api/random/json?size=1kb');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user