feat: implement sidebar accordion sections (#6373)

* feat: implement sidebar accordion sections

- Added SidebarAccordionContext for managing expanded sections.
- Introduced SidebarContent component to render sections dynamically.
- Created CollectionsSection and ApiSpecsSection for sidebar organization.
- Updated Sidebar component to utilize new sections and context.
- Enhanced StyledWrapper for improved layout and styling of sidebar sections.

* refactor: streamline Sidebar component and enhance styling

* feat: enhance SidebarSection with ActionIcon and improved hover styles

* fix: update useEffect dependencies in SidebarAccordionContext and enhance accessibility in SidebarSection

* style: increase gap in StyledWrapper and reintroduce cursor pointer for better user interaction

* style: remove custom scrollbar styles from Sidebar components for a cleaner look
This commit is contained in:
Abhishek S Lal
2025-12-10 19:04:46 +05:30
committed by GitHub
parent f8548225e1
commit 632f8705e5
13 changed files with 827 additions and 80 deletions

View File

@@ -1,7 +1,7 @@
import { setActiveApiSpecUid } from 'providers/ReduxStore/slices/apiSpec';
import { showApiSpecPage as _showApiSpecPage } from 'providers/ReduxStore/slices/app';
import Dropdown from 'components/Dropdown';
import { IconDots } from '@tabler/icons';
import { IconDots, IconX } from '@tabler/icons';
import { useState, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import CloseApiSpec from '../CloseApiSpec/index';
@@ -53,7 +53,10 @@ const ApiSpecItem = ({ apiSpec }) => {
setCloseApiSpecModal(true);
}}
>
Close
<span className="dropdown-icon">
<IconX size={16} strokeWidth={2} />
</span>
Remove
</div>
</Dropdown>
</div>

View File

@@ -1,7 +1,27 @@
import styled from 'styled-components';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
flex: 1 1 0%;
min-height: 0;
height: 100%;
overflow: hidden;
padding-top: 4px;
padding-bottom: 4px;
.api-specs-list {
flex: 1 1 0%;
min-height: 0;
padding-top: 4px;
padding-bottom: 4px;
overflow-y: auto;
overflow-x: hidden;
}
.api-spec-item {
height: 1.6rem;
cursor: pointer;
&.active {
background: ${(props) => props.theme.sidebar.collection.item.bg};
}
@@ -36,14 +56,6 @@ const Wrapper = styled.div`
top: -0.625rem;
}
div.dropdown-item.close-item {
color: ${(props) => props.theme.colors.danger};
&:hover {
background-color: ${(props) => props.theme.colors.bg.danger};
color: white;
}
}
.placeholder {
color: ${(props) => props.theme.colors.text.muted};
}

View File

@@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';
import ApiSpecItem from './ApiSpecItem';
import ApiSpecsBadge from './ApiSpecBadge';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
@@ -47,8 +46,7 @@ const ApiSpecs = () => {
if (!apiSpecs || !apiSpecs.length) {
return (
<StyledWrapper>
<ApiSpecsBadge />
<div className="text-xs text-center placeholder mt-4">
<div className="text-xs text-center placeholder py-4">
<div>No API Specs found.</div>
<div className="mt-2">
<OpenLink /> API Spec.
@@ -60,15 +58,12 @@ const ApiSpecs = () => {
return (
<StyledWrapper>
<div className="relative">
<ApiSpecsBadge />
<div className="flex flex-col top-32 bottom-10 left-0 right-0 py-4">
{apiSpecs && apiSpecs.length
? apiSpecs.map((apiSpec) => {
return <ApiSpecItem apiSpec={apiSpec} key={apiSpec.uid} />;
})
: null}
</div>
<div className="api-specs-list">
{apiSpecs && apiSpecs.length
? apiSpecs.map((apiSpec) => {
return <ApiSpecItem apiSpec={apiSpec} key={apiSpec.uid} />;
})
: null}
</div>
</StyledWrapper>
);

View File

@@ -3,32 +3,20 @@ import styled from 'styled-components';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
flex: 1;
flex: 1 1 0%;
min-height: 0;
overflow: hidden;
padding-top: 4px;
padding-bottom: 4px;
.collections-list {
flex: 1 1 0%;
min-height: 0;
padding-top: 4px;
padding-bottom: 4px;
overflow-y: auto;
overflow-x: hidden;
&::-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};
}
}
`;

View File

@@ -7,7 +7,6 @@ import CreateOrOpenCollection from './CreateOrOpenCollection';
import CollectionSearch from './CollectionSearch/index';
import { useMemo } from 'react';
import { normalizePath } from 'utils/common/path';
import ApiSpecs from '../ApiSpecs/index';
const Collections = ({ showSearch }) => {
const [searchText, setSearchText] = useState('');
@@ -44,7 +43,7 @@ const Collections = ({ showSearch }) => {
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
)}
<div className="collections-list flex flex-col flex-1 overflow-hidden hover:overflow-y-auto">
<div className="collections-list">
{workspaceCollections && workspaceCollections.length
? workspaceCollections.map((c) => {
return (
@@ -52,8 +51,6 @@ const Collections = ({ showSearch }) => {
);
})
: null}
<div className="w-full my-2" style={{ height: 1 }}></div>
<ApiSpecs />
</div>
</StyledWrapper>
);

View File

@@ -0,0 +1,79 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { IconFileCode, IconPlus } from '@tabler/icons';
import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';
import MenuDropdown from 'ui/MenuDropdown';
import ActionIcon from 'ui/ActionIcon';
import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';
import ApiSpecs from 'components/Sidebar/ApiSpecs';
import SidebarSection from 'components/Sidebar/SidebarSection';
const ApiSpecsSection = () => {
const dispatch = useDispatch();
const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false);
const handleOpenApiSpec = () => {
dispatch(openApiSpec()).catch((err) => {
console.error(err);
toast.error('An error occurred while opening the API spec');
});
};
const addDropdownItems = [
{
id: 'create-api-spec',
leftSection: IconPlus,
label: 'Create API Spec',
onClick: () => {
setCreateApiSpecModalOpen(true);
}
},
{
id: 'open-api-spec',
leftSection: IconFileCode,
label: 'Open API Spec',
onClick: () => {
handleOpenApiSpec();
}
}
];
const sectionActions = (
<>
<MenuDropdown
data-testid="api-specs-header-add-menu"
items={addDropdownItems}
placement="bottom-end"
>
<ActionIcon
label="Add new API Spec"
>
<IconPlus size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
</MenuDropdown>
</>
);
return (
<>
{createApiSpecModalOpen && (
<CreateApiSpec
onClose={() => setCreateApiSpecModalOpen(false)}
/>
)}
<SidebarSection
id="api-specs"
title="API Specs"
icon={IconFileCode}
actions={sectionActions}
className="api-specs-section"
>
<ApiSpecs />
</SidebarSection>
</>
);
};
export default ApiSpecsSection;

View File

@@ -0,0 +1,255 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useDispatch, useSelector } from 'react-redux';
import {
IconArrowsSort,
IconDotsVertical,
IconDownload,
IconFolder,
IconPlus,
IconSearch,
IconSortAscendingLetters,
IconSortDescendingLetters,
IconSquareX
} from '@tabler/icons';
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 MenuDropdown from 'ui/MenuDropdown';
import ActionIcon from 'ui/ActionIcon';
import ImportCollection from 'components/Sidebar/ImportCollection';
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
import RemoveCollectionsModal from 'components/Sidebar/Collections/RemoveCollectionsModal/index';
import CreateCollection from 'components/Sidebar/CreateCollection';
import Collections from 'components/Sidebar/Collections';
import SidebarSection from 'components/Sidebar/SidebarSection';
import { IconBox } from '@tabler/icons';
const CollectionsSection = () => {
const [showSearch, setShowSearch] = useState(false);
const dispatch = useDispatch();
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
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 handleToggleSearch = () => {
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 addDropdownItems = [
{
id: 'create',
leftSection: IconPlus,
label: 'Create collection',
onClick: () => {
setCreateCollectionModalOpen(true);
}
},
{
id: 'import',
leftSection: IconDownload,
label: 'Import collection',
onClick: () => {
setImportCollectionModalOpen(true);
}
},
{
id: 'open',
leftSection: IconFolder,
label: 'Open collection',
onClick: () => {
handleOpenCollection();
}
}
];
const actionsDropdownItems = [
{
id: 'sort',
leftSection: getSortIcon(),
label: getSortLabel(),
onClick: () => {
handleSortCollections();
}
},
{
id: 'close-all',
leftSection: IconSquareX,
label: 'Close all',
onClick: () => {
selectAllCollectionsToClose();
}
}
];
const sectionActions = (
<>
<ActionIcon
onClick={handleToggleSearch}
label="Search requests"
>
<IconSearch size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
<MenuDropdown
data-testid="collections-header-add-menu"
items={addDropdownItems}
placement="bottom-end"
>
<ActionIcon
label="Add new collection"
>
<IconPlus size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
</MenuDropdown>
<MenuDropdown
data-testid="collections-header-actions-menu"
items={actionsDropdownItems}
placement="bottom-end"
>
<ActionIcon
label="More actions"
>
<IconDotsVertical size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
</MenuDropdown>
{collectionsToClose.length > 0 && (
<RemoveCollectionsModal collectionUids={collectionsToClose} onClose={clearCollectionsToClose} />
)}
</>
);
return (
<>
{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}
/>
)}
<SidebarSection
id="collections"
title="Collections"
icon={IconBox}
actions={sectionActions}
>
<Collections showSearch={showSearch} />
</SidebarSection>
</>
);
};
export default CollectionsSection;

View File

@@ -0,0 +1,61 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
const SidebarAccordionContext = createContext();
export const useSidebarAccordion = () => {
const context = useContext(SidebarAccordionContext);
if (!context) {
throw new Error('useSidebarAccordion must be used within SidebarAccordionProvider');
}
return context;
};
export const SidebarAccordionProvider = ({ children, defaultExpanded = ['collections'] }) => {
const [expandedSections, setExpandedSections] = useState(new Set(defaultExpanded));
const toggleSection = useCallback((sectionId) => {
setExpandedSections((prev) => {
const newSet = new Set(prev);
if (newSet.has(sectionId)) {
newSet.delete(sectionId);
} else {
newSet.add(sectionId);
}
return newSet;
});
}, []);
const setSectionExpanded = useCallback((sectionId, expanded) => {
setExpandedSections((prev) => {
const newSet = new Set(prev);
if (expanded) {
newSet.add(sectionId);
} else {
newSet.delete(sectionId);
}
return newSet;
});
}, []);
const isExpanded = useCallback((sectionId) => {
return expandedSections.has(sectionId);
}, [expandedSections]);
const getExpandedCount = useCallback(() => {
return expandedSections.size;
}, [expandedSections]);
return (
<SidebarAccordionContext.Provider
value={{
expandedSections,
toggleSection,
setSectionExpanded,
isExpanded,
getExpandedCount
}}
>
{children}
</SidebarAccordionContext.Provider>
);
};

View File

@@ -0,0 +1,69 @@
import { useSidebarAccordion } from './SidebarAccordionContext';
/**
* Sections configuration
*
* All sections use the same generic accordion behavior with the class 'accordion-section-wrapper'.
* Layout behavior is fully automatic based on section order and expansion state:
* - Single expanded: When only one section is expanded, it fills available space
* - Multi-expanded: When multiple sections are expanded, they split space equally
* - Automatic pinning: Sections below an expanded section are automatically pinned to bottom
*
* To add a new section, simply add a new entry to this array:
*
* {
* id: 'my-section', // Unique identifier
* component: MySectionComponent, // React component to render
* getProps: (context) => ({ ... }) // Function to get props for component
* }
*/
const SidebarContent = ({ sections }) => {
const { isExpanded, getExpandedCount } = useSidebarAccordion();
const expandedCount = getExpandedCount();
const getWrapperClassName = (section, sectionIndex) => {
const sectionExpanded = isExpanded(section.id);
// Use generic accordion-section-wrapper class for all sections
const classes = ['accordion-section-wrapper'];
// Multi-expanded: when multiple sections are expanded
if (expandedCount > 1 && sectionExpanded) {
classes.push('multi-expanded');
}
// Single expanded wrapper behavior: when only one section is expanded, it fills space
if (sectionExpanded && expandedCount === 1) {
classes.push('single-expanded-wrapper');
}
// Automatic pinning: if section is not expanded and any section above it (earlier in array) is expanded
if (!sectionExpanded) {
// Check if any section before this one (earlier in array) is expanded
const hasExpandedAbove = sections.slice(0, sectionIndex).some((s) => isExpanded(s.id));
if (hasExpandedAbove) {
classes.push('pinned-to-bottom');
}
}
return classes.join(' ');
};
return (
<>
{sections.map((section, index) => {
const SectionComponent = section.component;
const wrapperClassName = getWrapperClassName(section, index);
return (
<div key={section.id} className={wrapperClassName}>
<SectionComponent />
</div>
);
})}
</>
);
};
export default SidebarContent;

View File

@@ -0,0 +1,116 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
height: 100%;
.sidebar-section {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
&.expanded {
flex: 1 1 0%;
min-height: 0;
}
&:not(.expanded) {
flex: 0 0 auto;
}
&.multi-expanded {
flex: 1 1 0%;
margin-bottom: 0;
}
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 6px 4px 6px 8px;
min-height: 28px;
height: 28px;
user-select: none;
transition: background-color 0.15s ease;
flex-shrink: 0;
border-bottom: 1px solid transparent;
.section-header-left {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
cursor: pointer;
&:hover {
.section-toggle {
display: flex;
}
.section-toggle {
background: ${(props) => props.theme.dropdown.hoverBg};
color: ${(props) => props.theme.text} !important;
}
.section-icon {
display: none;
}
}
}
}
.section-icon-wrapper {
width: 24px;
height: 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.section-toggle {
display: none;
}
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: ${(props) => props.theme.sidebar.muted};
}
.section-title {
color: ${(props) => props.theme.sidebar.color};
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section-actions {
display: flex;
align-items: center;
gap: 1px;
flex-shrink: 0;
}
}
.section-content {
display: flex;
flex-direction: column;
flex: 1 1 0%;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,85 @@
import { useState, useEffect, useRef } from 'react';
import { IconChevronRight, IconChevronDown } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import { useSidebarAccordion } from '../SidebarAccordionContext';
import ActionIcon from 'ui/ActionIcon/index';
const SidebarSection = ({
id,
title,
icon: Icon,
actions,
children,
className = ''
}) => {
const { isExpanded, setSectionExpanded, getExpandedCount } = useSidebarAccordion();
const [localExpanded, setLocalExpanded] = useState(() => isExpanded(id));
const sectionRef = useRef(null);
// Sync with context
useEffect(() => {
const expanded = isExpanded(id);
setLocalExpanded(expanded);
}, [id, isExpanded]);
const handleToggle = () => {
const newExpanded = !localExpanded;
setLocalExpanded(newExpanded);
setSectionExpanded(id, newExpanded);
};
const expandedCount = getExpandedCount();
// Check if this is the only expanded section
const isOnlyExpanded = expandedCount === 1 && localExpanded;
return (
<StyledWrapper className={className}>
<div
ref={sectionRef}
className={`sidebar-section ${localExpanded ? 'expanded' : ''} ${isOnlyExpanded ? 'single-expanded' : ''} ${expandedCount > 1 && localExpanded ? 'multi-expanded' : ''}`}
>
<div
className="section-header"
onClick={handleToggle}
>
<div className="section-header-left">
<div
className="section-icon-wrapper"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); handleToggle();
}
}}
>
<ActionIcon size="sm" className="section-toggle">
{localExpanded ? (
<IconChevronDown size={12} stroke={1.5} />
) : (
<IconChevronRight size={12} stroke={1.5} />
)}
</ActionIcon>
{Icon && <Icon size={14} stroke={1.5} className="section-icon" />}
</div>
<span className="section-title">{title}</span>
</div>
{actions && (
<div
className="section-actions"
onClick={(e) => e.stopPropagation()}
>
{actions}
</div>
)}
</div>
{localExpanded && (
<div className="section-content">
{children}
</div>
)}
</div>
</StyledWrapper>
);
};
export default SidebarSection;

View File

@@ -2,32 +2,104 @@ import styled from 'styled-components';
const Wrapper = styled.div`
color: ${(props) => props.theme.sidebar.color};
max-height: 100%;
aside {
background-color: ${(props) => props.theme.sidebar.bg};
overflow: hidden;
.collection-title {
line-height: 1.5;
.collection-dropdown {
.dropdown-icon {
display: none;
color: rgb(110 110 110);
}
}
.sidebar-sections-container {
display: flex;
flex-direction: column;
}
&:hover {
background: #f7f7f7;
.dropdown-icon {
display: flex;
}
}
.sidebar-sections {
min-height: 0;
display: flex;
flex-direction: column;
height: 100%;
}
div.tippy-box {
position: relative;
top: -0.625rem;
/* Expanded sections grow to fill available space but are constrained */
.sidebar-section.expanded {
flex: 1 1 0%;
min-height: 0;
.section-header {
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
}
/* Single expanded section: add margin-bottom to push others down */
.sidebar-section.single-expanded {
margin-bottom: auto !important;
flex: 1 1 0% !important;
min-height: 0;
max-height: 100%;
}
/* Multiple expanded sections: equal split, no margin-bottom */
.sidebar-section.multi-expanded {
margin-bottom: 0;
flex: 1 1 0% !important;
min-height: 0;
overflow: hidden;
max-height: 100%;
}
/* Collapsed sections only take header height */
.sidebar-section:not(.expanded) {
flex: 0 0 auto;
}
/* Always push bottom accordions wrapper to the bottom */
.bottom-accordions-wrapper {
display: flex;
flex-direction: column;
flex: 0 0 auto;
}
/* Generic accordion section wrapper - applies to all accordion sections */
.accordion-section-wrapper {
display: flex;
flex-direction: column;
min-height: 0;
position: relative;
overflow: visible;
}
/* Add border-top to all accordion items except the first child */
.accordion-section-wrapper:not(:first-child) {
border-top: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
/* When a section is single expanded, wrapper should fill space but respect pinned sections */
.accordion-section-wrapper.single-expanded-wrapper {
flex: 1 1 0% !important;
min-height: 0;
overflow: hidden;
}
/* Normal flow: sections not pinned and not multi-expanded */
.accordion-section-wrapper:not(.pinned-to-bottom):not(.multi-expanded) {
flex: 0 0 auto;
}
/* When a section is pinned to bottom */
.accordion-section-wrapper.pinned-to-bottom {
flex: 0 0 auto;
margin-top: auto;
}
/* When multiple sections are expanded, split space equally */
.accordion-section-wrapper.multi-expanded {
flex: 1 1 0% !important;
min-height: 0;
margin-top: 0 !important;
height: auto !important;
}
}
div.sidebar-drag-handle {

View File

@@ -1,20 +1,32 @@
import SidebarHeader from './SidebarHeader';
import Collections from './Collections';
import { SidebarAccordionProvider } from './SidebarAccordionContext';
import SidebarContent from './SidebarContent';
import StyledWrapper from './StyledWrapper';
import { useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app';
import CollectionsSection from './Sections/CollectionsSection/index';
import ApiSpecsSection from './Sections/ApiSpecsSection/index';
const MIN_LEFT_SIDEBAR_WIDTH = 220;
const MAX_LEFT_SIDEBAR_WIDTH = 600;
const SIDEBAR_SECTIONS = [
{
id: 'collections',
component: CollectionsSection
},
{
id: 'api-specs',
component: ApiSpecsSection
}
];
const Sidebar = () => {
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
const lastWidthRef = useRef(leftSidebarWidth);
const [showSearch, setShowSearch] = useState(false);
const dispatch = useDispatch();
const [dragging, setDragging] = useState(false);
@@ -77,26 +89,29 @@ const Sidebar = () => {
}, [leftSidebarWidth]);
return (
<StyledWrapper className="flex relative h-full">
<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" style={{ minHeight: 0, overflow: 'hidden' }}>
<SidebarHeader
setShowSearch={setShowSearch}
/>
<Collections showSearch={showSearch} />
<SidebarAccordionProvider defaultExpanded={['collections']}>
<StyledWrapper className="flex relative h-full">
<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 sidebar-sections-container" style={{ minHeight: 0, overflow: 'hidden' }}>
<div className="sidebar-sections flex flex-col flex-1">
<SidebarContent
sections={SIDEBAR_SECTIONS}
/>
</div>
</div>
</div>
</div>
</div>
</aside>
</aside>
{!sidebarCollapsed && (
<div className="absolute sidebar-drag-handle h-full" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
</div>
)}
</StyledWrapper>
{!sidebarCollapsed && (
<div className="absolute sidebar-drag-handle h-full" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
</div>
)}
</StyledWrapper>
</SidebarAccordionProvider>
);
};