fix: collection settings access, UI overflow fixes, and auto-focus URL bar (#7861)

* fix: collection settings access, UI overflow fixes, and auto-focus URL bar

- Move collection icon outside dropdown; clicking it opens collection
  settings/overview, clicking the name opens the switcher dropdown
- Auto-focus URL bar when creating a transient request (#2919)
- Fix long collection/folder name overflow with ellipsis truncation
- Reduce dropdown width and truncate large collection names
- Simplify breadcrumb collapse: show collection name and last folder,
  collapse middle items into a dropdown
- Fix modal width to prevent shrinking with short collection names
- Show "Create Collection" option when saving a draft with zero collections
- Use IconBox consistently for collection icons

* Replace Chevron component with IconChevronRight

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
Co-authored-by: Sid <siddharth@usebruno.com>
This commit is contained in:
Chirag Chandrashekhar
2026-04-30 17:05:40 +05:30
committed by GitHub
parent 8269d51df4
commit 118ba801aa
10 changed files with 222 additions and 75 deletions

View File

@@ -8,10 +8,12 @@ const Overview = ({ collection }) => {
return (
<div className="h-full">
<div className="grid grid-cols-5 gap-5 h-full">
<div className="col-span-2">
<div className="text-lg font-medium flex items-center gap-2">
<IconBox size={20} stroke={1.5} />
{collection?.name}
<div className="col-span-2 overflow-clip text-ellipsis">
<div className="flex gap-2 items-center min-w-0">
<IconBox size={20} stroke={1.5} className="flex-shrink-0" />
<span className="overflow-hidden text-lg font-medium whitespace-nowrap text-ellipsis">
{collection?.name}
</span>
</div>
<Info collection={collection} />
<RequestsNotLoaded collection={collection} />

View File

@@ -179,6 +179,17 @@ const Wrapper = styled.div`
}
}
.breadcrumb-collapsed-dropdown {
max-width: 250px;
}
.breadcrumb-collapsed-item {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown-separator {
height: 1px;
background-color: ${(props) => props.theme.dropdown.separator};

View File

@@ -38,6 +38,12 @@ const QueryUrl = ({ item, collection, handleRun }) => {
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
const hasChanges = useMemo(() => hasRequestChanges(item), [item]);
useEffect(() => {
if (item.isTransient && !url && editorRef.current?.editor) {
setTimeout(() => editorRef.current?.editor?.focus(), 0);
}
}, [item.uid]);
const onSave = () => {
dispatch(saveRequest(item.uid, collection.uid));
};

View File

@@ -25,7 +25,7 @@ const StyledWrapper = styled.div`
}
.switcher-name {
max-width: 300px;
max-width: 124px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -151,6 +151,14 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.colors.text.danger};
margin-left: 8px;
}
.display-icon{
padding: 4px;
box-sizing: content-box;
&:hover {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
border-radius: ${(props) => props.theme.border.radius.sm}
}
}
`;
export default StyledWrapper;

View File

@@ -393,6 +393,16 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
&& currentWorkspace.type !== 'default'
&& !isRenamingWorkspace;
const handleDisplayIconClick = (e) => {
const uid = isScratchCollection ? `${collection.uid}-overview` : collection.uid;
const type = isScratchCollection ? 'workspaceOverview' : 'collection-settings';
dispatch(addTab({
uid: uid,
collectionUid: collection.uid,
type: type
}));
};
return (
<StyledWrapper>
{closeWorkspaceModalOpen && currentWorkspace?.uid && (
@@ -411,7 +421,7 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
<div className="collection-switcher">
{isRenamingWorkspace ? (
<div className="workspace-rename-container" ref={workspaceRenameContainerRef}>
<DisplayIcon size={18} strokeWidth={1.5} />
<DisplayIcon size={18} strokeWidth={1.5} className="cursor-pointer display-icon" />
<div className="workspace-input-wrapper">
<input
ref={workspaceNameInputRef}
@@ -459,69 +469,71 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
)}
</div>
) : (
<Dropdown
placement="bottom-start"
onCreate={onSwitcherCreate}
appendTo={() => document.body}
icon={(
<button className="switcher-trigger">
<DisplayIcon size={18} strokeWidth={1.5} />
<span className={classNames('switcher-name', { 'scratch-collection': isScratchCollection })}>{displayName}</span>
<IconChevronDown size={14} strokeWidth={1.5} className="chevron" />
</button>
)}
>
{/* Workspace section */}
{currentWorkspace && (
<>
<div className="label-item">Workspace</div>
<div
className={classNames('dropdown-item', {
'dropdown-item-active': isScratchCollection
})}
onClick={() => handleSwitchToWorkspace(currentWorkspace.uid)}
>
<div className="dropdown-icon">
<IconCategory size={16} strokeWidth={1.5} />
</div>
<span className="dropdown-label">
{currentWorkspace.name || 'Untitled Workspace'}
</span>
{workspaceTabCount > 0 && (
<span className="dropdown-tab-count">{workspaceTabCount}</span>
)}
</div>
</>
)}
{/* Collections section */}
{mountedCollections.length > 0 && (
<>
<div className="dropdown-separator" />
<div className="label-item">Collections</div>
{mountedCollections.map((col) => {
const colTabCount = getTabCount(col.uid);
return (
<div className="flex flex-row justify-center items-center gap-x-1">
<DisplayIcon size={18} strokeWidth={1.5} className="cursor-pointer display-icon" onClick={handleDisplayIconClick} />
<Dropdown
placement="bottom-start"
onCreate={onSwitcherCreate}
appendTo={() => document.body}
icon={(
<button className="switcher-trigger">
<span className={classNames('switcher-name', { 'scratch-collection': isScratchCollection })}>{displayName}</span>
<IconChevronDown size={14} strokeWidth={1.5} className="chevron" />
</button>
)}
>
<div className="max-w-124 overflow-hidden">
{currentWorkspace && (
<>
<div className="label-item">Workspace</div>
<div
key={col.uid}
className={classNames('dropdown-item', {
'dropdown-item-active': !isScratchCollection && collection.uid === col.uid
'dropdown-item-active': isScratchCollection
})}
onClick={() => handleSwitchToCollection(col)}
onClick={() => handleSwitchToWorkspace(currentWorkspace.uid)}
>
<div className="dropdown-icon">
<IconBox size={16} strokeWidth={1.5} />
<IconCategory size={16} strokeWidth={1.5} />
</div>
<span className="dropdown-label">{col.name || 'Untitled Collection'}</span>
{colTabCount > 0 && (
<span className="dropdown-tab-count">{colTabCount}</span>
<span className="dropdown-label collection-header-dropdown-label">
{currentWorkspace.name || 'Untitled Workspace'}
</span>
{workspaceTabCount > 0 && (
<span className="dropdown-tab-count">{workspaceTabCount}</span>
)}
</div>
);
})}
</>
)}
</Dropdown>
</>
)}
{mountedCollections.length > 0 && (
<>
<div className="dropdown-separator" />
<div className="label-item">Collections</div>
{mountedCollections.map((col) => {
const colTabCount = getTabCount(col.uid);
return (
<div
key={col.uid}
className={classNames('dropdown-item', {
'dropdown-item-active': !isScratchCollection && collection.uid === col.uid
})}
onClick={() => handleSwitchToCollection(col)}
>
<div className="dropdown-icon">
<IconBox size={16} strokeWidth={1.5} />
</div>
<span className="dropdown-label collection-header-dropdown-label">{col.name || 'Untitled Collection'}</span>
{colTabCount > 0 && (
<span className="dropdown-tab-count">{colTabCount}</span>
)}
</div>
);
})}
</>
)}
</div>
</Dropdown>
</div>
)}
{/* Workspace actions dropdown */}

View File

@@ -1,6 +1,6 @@
import React, { useMemo, useCallback, memo } from 'react';
import { useSelector } from 'react-redux';
import { IconDatabase, IconLoader2 } from '@tabler/icons';
import { IconBox, IconLoader2 } from '@tabler/icons';
import { areItemsLoading } from 'utils/collections';
const CollectionListItem = memo(({ collectionUid, collectionPath, collectionName, isSelected, onSelect }) => {
@@ -26,7 +26,7 @@ const CollectionListItem = memo(({ collectionUid, collectionPath, collectionName
onClick={handleClick}
>
<div className="collection-item-content">
<IconDatabase size={16} strokeWidth={1.5} />
<IconBox size={16} strokeWidth={1.5} />
<span className="collection-item-name">{collectionName}</span>
</div>
{isLoading && (

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { IconChevronRight } from '@tabler/icons';
import { IconChevronRight, IconDots } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
const FolderBreadcrumbs = ({
collectionName,
@@ -8,30 +9,64 @@ const FolderBreadcrumbs = ({
onNavigateToRoot,
onNavigateToBreadcrumb
}) => {
const collapsed = breadcrumbs.length > 1 ? breadcrumbs.slice(0, -1) : [];
const last = breadcrumbs.length > 0 ? breadcrumbs[breadcrumbs.length - 1] : null;
return (
<>
<div className="breadcrumb-container">
<span
className={!isAtRoot ? 'collection-name-breadcrumb' : ''}
className={`breadcrumb-collection-name ${!isAtRoot ? 'collection-name-breadcrumb' : ''}`}
onClick={!isAtRoot ? onNavigateToRoot : undefined}
title={collectionName}
>
{collectionName}
</span>
{breadcrumbs.map((breadcrumb, index) => (
<React.Fragment key={breadcrumb.uid}>
{collapsed.length > 0 && (
<>
<IconChevronRight size={16} strokeWidth={1.5} className="collection-name-chevron" />
<Dropdown
placement="bottom-start"
icon={(
<span className="breadcrumb-ellipsis-btn">
<IconDots size={16} strokeWidth={2} />
</span>
)}
>
<div className="breadcrumb-collapsed-dropdown">
{collapsed.map((breadcrumb, i) => (
<div
key={breadcrumb.uid}
className="dropdown-item breadcrumb-collapsed-item"
onClick={() => onNavigateToBreadcrumb(i)}
title={breadcrumb.name}
>
{breadcrumb.name}
</div>
))}
</div>
</Dropdown>
</>
)}
{last && (
<>
<IconChevronRight size={16} strokeWidth={1.5} className="collection-name-chevron" />
<span
className="collection-name-breadcrumb"
className="collection-name-breadcrumb breadcrumb-last"
onClick={(e) => {
e.stopPropagation();
onNavigateToBreadcrumb(index);
onNavigateToBreadcrumb(breadcrumbs.length - 1);
}}
title={last.name}
>
{breadcrumb.name}
{last.name}
</span>
</React.Fragment>
))}
</>
)}
{isAtRoot && <IconChevronRight size={16} strokeWidth={1.5} className="collection-name-chevron" />}
</>
</div>
);
};

View File

@@ -1,6 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.bruno-modal-card.modal-sm {
width: 500px;
}
.save-request-form {
display: flex;
flex-direction: column;
@@ -54,6 +58,7 @@ const StyledWrapper = styled.div`
font-size: 14px;
margin-bottom: 12px;
color: ${(props) => props.theme.colors.text.muted};
min-width: 0;
}
.collection-name-clickable {
@@ -66,6 +71,49 @@ const StyledWrapper = styled.div`
.collection-name-chevron {
margin: 0 4px;
flex-shrink: 0;
}
.breadcrumb-container {
display: flex;
align-items: center;
overflow: hidden;
white-space: nowrap;
min-width: 0;
}
.breadcrumb-collection-name,
.breadcrumb-last {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 40px;
flex: 0 1 auto;
}
.breadcrumb-ellipsis-btn {
display: flex;
align-items: center;
cursor: pointer;
padding: 2px 4px;
border-radius: ${(props) => props.theme.border.radius.sm};
flex-shrink: 0;
color: ${(props) => props.theme.colors.text.yellow};
&:hover {
background-color: ${(props) => props.theme.plainGrid.hoverBg};
}
}
.breadcrumb-dropdown {
min-width: 120px;
max-width: 250px;
.dropdown-item {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.search-container {
@@ -114,10 +162,19 @@ const StyledWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
overflow: hidden;
svg {
flex-shrink: 0;
}
}
.folder-item-name {
color: ${(props) => props.theme.text};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.folder-empty-state {
@@ -157,6 +214,7 @@ const StyledWrapper = styled.div`
border-radius: ${(props) => props.theme.border.radius.sm};
user-select: none;
border: 1px solid ${(props) => props.theme.border.border1};
overflow: hidden;
&:hover {
background-color: ${(props) => props.theme.plainGrid.hoverBg};
@@ -168,11 +226,20 @@ const StyledWrapper = styled.div`
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
overflow: hidden;
svg {
flex-shrink: 0;
}
}
.collection-item-name {
color: ${(props) => props.theme.text};
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.collection-empty-state {

View File

@@ -361,7 +361,7 @@ const SaveTransientRequest = ({ item: itemProp, collection: collectionProp, isOp
return (
<StyledWrapper>
<Modal
size="md"
size="sm"
title={isSelectingCollection ? 'Select Collection' : 'Save Request'}
handleCancel={handleCancel}
handleConfirm={handleConfirm}

View File

@@ -187,6 +187,12 @@ const GlobalStyle = createGlobalStyle`
}
.collection-header-dropdown-label {
max-width: 124px;
overflow: hidden;
text-overflow: ellipsis;
}
// scrollbar styling
// the below media query targets non-touch devices
@media not all and (pointer: coarse) {