Skip to content
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
20201fc
created flat sidebar tree into flat array with specs
sachin-thakur-bruno Aug 5, 2026
6f7f53a
added item row component and moved row related things to it
sachin-thakur-bruno Aug 5, 2026
cf3d17e
colelction row refactor
sachin-thakur-bruno Aug 5, 2026
a9f29e5
created row component and use Virtuoso for virtualization
sachin-thakur-bruno Aug 6, 2026
0c3e9f9
fixed locators for sidebar.
sachin-thakur-bruno Aug 6, 2026
b47e029
add empty row cta delay to prevent flicker
sachin-thakur-bruno Aug 6, 2026
16f277b
added scroll into view for active tab
sachin-thakur-bruno Aug 6, 2026
d77f935
moved examples expand state to redux so that they can survive the vir…
sachin-thakur-bruno Aug 6, 2026
1e6d41d
removed unused files
sachin-thakur-bruno Aug 6, 2026
9cf2dec
updated comments
sachin-thakur-bruno Aug 6, 2026
0e5971e
increased the overscan for fast scroll
sachin-thakur-bruno Aug 6, 2026
a39f809
Merge remote-tracking branch 'upstream/main' into feat/virtualize-sid…
sachin-thakur-bruno Aug 15, 2026
103a9df
fix test locators for flat dom nodes instead of nesting
sachin-thakur-bruno Aug 15, 2026
3382290
code refatcor
sachin-thakur-bruno Aug 16, 2026
1d04cb4
fixed failed test due to virtualized list
sachin-thakur-bruno Aug 16, 2026
13f46d3
cleanup
sachin-thakur-bruno Aug 16, 2026
d008794
Merge branch 'main' into feat/virtualize-sidebar-items
sachin-thakur-bruno Aug 25, 2026
0f17452
collection environment table lag fixes
sachin-thakur-bruno Aug 25, 2026
8131e47
Merge branch 'feat/virtualize-sidebar-items' of github.com:sachin-tha…
sachin-thakur-bruno Aug 25, 2026
4ea7ce7
Merge branch 'main' into feat/virtualize-sidebar-items
sachin-thakur-bruno Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { getEmptyImage } from 'react-dnd-html5-backend';
import range from 'lodash/range';
import filter from 'lodash/filter';
import classnames from 'classnames';
import { useDrag, useDrop } from 'react-dnd';
import {
Expand All @@ -25,28 +24,26 @@ import {
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { handleCollectionItemDrop, sendRequest, showInFolder, pasteItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { toggleCollectionItem, addResponseExample } from 'providers/ReduxStore/slices/collections';
import { toggleCollectionItem, toggleRequestExamples, addResponseExample } from 'providers/ReduxStore/slices/collections';
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
import { uuid } from 'utils/common';
import { copyRequest, setFocusedSidebarPath } from 'providers/ReduxStore/slices/app';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
import NewApp from 'components/Sidebar/NewApp';
import RenameCollectionItem from './RenameCollectionItem';
import CloneCollectionItem from './CloneCollectionItem';
import DeleteCollectionItem from './DeleteCollectionItem';
import IgnoreCollectionItem from './IgnoreCollectionItem';
import RunCollectionItem from './RunCollectionItem';
import GenerateCodeItem from './GenerateCodeItem';
import RenameCollectionItem from '../RenameCollectionItem';
import CloneCollectionItem from '../CloneCollectionItem';
import DeleteCollectionItem from '../DeleteCollectionItem';
import IgnoreCollectionItem from '../IgnoreCollectionItem';
import RunCollectionItem from '../RunCollectionItem';
import GenerateCodeItem from '../GenerateCodeItem';
import { isItemARequest, isItemAFolder } from 'utils/tabs';
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
import { getDefaultRequestPaneTab } from 'utils/collections';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import StyledWrapper from '../StyledWrapper';
import NetworkError from 'components/ResponsePane/NetworkError/index';
import CollectionItemInfo from './CollectionItemInfo/index';
import CollectionItemIcon from './CollectionItemIcon';
import ExampleItem from './ExampleItem';
import CollectionItemInfo from '../CollectionItemInfo/index';
import CollectionItemIcon from '../CollectionItemIcon';
import ExampleIcon from 'components/Icons/ExampleIcon';
import { scrollToTheActiveTab } from 'utils/tabs';
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
Expand All @@ -56,14 +53,12 @@ import {
isTabForItemPresent as isTabForItemPresentSelector
} from 'src/selectors/tab';
import { isEqual } from 'lodash';
import { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest';
import {
canCollectionItemBeDropped,
determineCollectionItemDrop,
getInitialExampleName,
findParentItemInCollection
} from 'utils/collections/index';
import { sortByNameThenSequence } from 'utils/common/index';
import { getRevealInFolderLabel } from 'utils/common/platform';
import CreateExampleModal from 'components/ResponseExample/CreateExampleModal';
import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
Expand All @@ -72,7 +67,11 @@ import MenuDropdown from 'ui/MenuDropdown';
import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';
import useKeybinding from 'hooks/useKeybinding';

const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {
/**
* CollectionItemRow — the presentation + interaction for a single sidebar item row
* (folder / app / request), and its response-example rows.
*/
const CollectionItemRow = ({ item, collectionUid, collectionPathname, searchText, depth, children }) => {
const isMockServerEnabled = useBetaFeature(BETA_FEATURES.MOCK_SERVER);
const { dropdownContainerRef } = useSidebarAccordion();
const selectorInput = {
Expand Down Expand Up @@ -110,8 +109,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const [newAppModalOpen, setNewAppModalOpen] = useState(false);
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
const [examplesExpanded, setExamplesExpanded] = useState(false);
const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);

const examplesExpanded = !!item.examplesExpanded;
const hasSearchText = searchText && searchText?.trim()?.length;
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
const isFolder = isItemAFolder(item);
Expand Down Expand Up @@ -163,17 +163,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
dragPreview(getEmptyImage(), { captureDraggingState: true });
}, []);

// Auto-scroll to show this item when its tab becomes active
useEffect(() => {
if (isTabForItemActive && ref.current) {
try {
ref.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (err) {
// ignore scroll errors (some environments may not support smooth scrolling)
}
}
}, [isTabForItemActive]);

const resolveDropFromMonitor = (monitor) => {
return determineCollectionItemDrop({
item,
Expand Down Expand Up @@ -338,7 +327,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const handleExamplesCollapse = (e) => {
e.stopPropagation();
e.preventDefault();
setExamplesExpanded(!examplesExpanded);
dispatch(toggleRequestExamples({ collectionUid, itemUid: item.uid }));
};

// prevent the parent's double-click handler from firing
Expand All @@ -354,7 +343,8 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
menuDropdownRef.current?.show();
};

const indents = range(item.depth);
const rowDepth = typeof depth === 'number' ? depth : item.depth;
const indents = range(rowDepth);

// Build menu items for MenuDropdown
const buildMenuItems = () => {
Expand Down Expand Up @@ -512,27 +502,10 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
'is-sidebar-dragging': isSidebarDragging
});

if (searchText && searchText.length) {
if (isItemARequest(item)) {
if (!doesRequestMatchSearchText(item, searchText)) {
return null;
}
} else {
if (!doesFolderHaveItemsMatchSearchText(item, searchText)) {
return null;
}
}
}

const handleDoubleClick = (event) => {
dispatch(makeTabPermanent({ uid: tabUidForItem || item.uid }));
};

// Sort items by their "seq" property.
const sortItemsBySequence = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
};

const handleShowInFolder = () => {
dispatch(showInFolder(item.pathname)).catch((error) => {
console.error('Error opening the folder', error);
Expand Down Expand Up @@ -589,14 +562,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
setCreateExampleModalOpen(false);
};

const folderItems = sortByNameThenSequence(filter(item.items, (i) => isItemAFolder(i) && !i.isTransient));
const appItems = sortItemsBySequence(filter(item.items, (i) => i.type === 'app' && !i.isTransient));
const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i) && !i.isTransient));
const showEmptyFolderMessage
= isFolder && !hasSearchText && !folderItems?.length && !appItems?.length && !requestItems?.length;

const emptyFolderMenuItems = createEmptyStateMenuItems({ dispatch, collection, itemUid: item.uid });

const handleGenerateCode = () => {
if (
(item?.request?.url !== '')
Expand Down Expand Up @@ -781,64 +746,10 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
</div>
</div>
</div>
{!itemIsCollapsed ? (
<div>
{folderItems && folderItems.length
? folderItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
})
: null}
{appItems && appItems.length
? appItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
})
: null}
{requestItems && requestItems.length
? requestItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
})
: null}
{showEmptyFolderMessage ? (
<div className="empty-folder-message">
{range(item.depth + 1).map((i) => (
<div className="indent-block" key={i} style={{ width: 16, minWidth: 16, height: '100%' }}>
&nbsp;
</div>
))}
<div style={{ paddingLeft: 8 }}>
<MenuDropdown
data-testid="add-request-cta-folder"
items={emptyFolderMenuItems}
placement="bottom-start"
appendTo={dropdownContainerRef?.current || document.body}
popperOptions={{ strategy: 'fixed' }}
>
<button className="ml-1 add-request-link">+ Add request</button>
</MenuDropdown>
</div>
</div>
) : null}
</div>
) : null}

{/* Show examples when expanded (only for HTTP requests) */}
{isItemARequest(item) && item.type === 'http-request' && examplesExpanded && hasExamples && (
<div>
{(item.examples || []).map((example, index) => {
return (
<ExampleItem
key={example.uid || index}
example={example}
item={item}
index={index}
collection={collection}
/>
);
})}
</div>
)}

{children}
</StyledWrapper>
);
};

export default React.memo(CollectionItem);
export default React.memo(CollectionItemRow);
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';

const ExampleItem = ({ example, item, collection }) => {
const ExampleItem = ({ example, item, collection, depth }) => {
const { dropdownContainerRef } = useSidebarAccordion();
const dispatch = useDispatch();
const activeTabUid = useSelector((state) => state.tabs?.activeTabUid);
Expand All @@ -33,8 +33,7 @@ const ExampleItem = ({ example, item, collection }) => {
const exampleRef = useRef(null);
const menuDropdownRef = useRef(null);

// Calculate indentation: item depth + 1 for examples
const indents = range((item.depth || 0) + 1);
const indents = range(depth);

const handleExampleClick = () => {
const exampleIndex = item?.examples?.findIndex((ex) => ex.uid === example.uid);
Expand Down Expand Up @@ -64,16 +63,6 @@ const ExampleItem = ({ example, item, collection }) => {
setEditName(example.name);
}, [example.name]);

useEffect(() => {
if (isExampleActive && exampleRef.current) {
try {
exampleRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (err) {
// ignore scroll errors
}
}
}, [isExampleActive]);

const handleClone = async () => {
// Calculate the index where the cloned example will be saved
// It will be at the end of the examples array
Expand Down
Loading
Loading