feat: sidebar enhancement (#10157)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run

* feat: sidebar enhancement (#9748)

* fix: sidebar enhancement

* fix: new source btn changes

* fix: shortcut order changes

* fix: changes in more section collapse behaviour

* fix: sidebar shortcut changes, consistency, cleanup in collapse mode

* fix: sidebar pin, tooltip and other changes

* feat: updated alignment issues

* fix: sidenav enhancement - fixes

* fix: code fix

* fix: sidenav enhancement

* feat: addressed comments and feedback

* feat: fix default shortcut empty issue

* feat: code clean and improvements

* feat: refactor and cleanup

* feat: refactor and addressed comment

* feat: removed isscrolled

* feat: corrected the ref intialization
This commit is contained in:
SagarRajput-7
2026-02-03 13:28:21 +05:30
committed by GitHub
parent 5b3f121431
commit c9cd974dca
11 changed files with 595 additions and 188 deletions

View File

@@ -48,7 +48,7 @@
}
.app-content {
width: calc(100% - 64px); // width of the sidebar
width: calc(100% - 54px); // width of the sidebar
z-index: 0;
margin: 0 auto;

View File

@@ -168,7 +168,7 @@
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 64px);
width: calc(100% - 54px);
background: rgb(18, 19, 23);
padding: 16px;
margin: 0;

View File

@@ -442,7 +442,7 @@
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 64px);
width: calc(100% - 54px);
background: var(--bg-ink-500);
padding: 16px;
margin: 0;

View File

@@ -58,7 +58,7 @@
overflow-y: auto;
box-sizing: border-box;
height: calc(100% - 64px);
height: calc(100% - 54px);
&::-webkit-scrollbar {
height: 1rem;

View File

@@ -194,7 +194,7 @@
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 64px);
width: calc(100% - 54px);
background: var(--bg-ink-500);
padding: 16px;
margin: 0;

View File

@@ -13,6 +13,12 @@
.nav-item-active-marker {
background: #4e74f8;
}
.nav-item-data {
.nav-item-label {
color: var(--bg-vanilla-100, #fff);
}
}
}
&.disabled {
@@ -27,14 +33,14 @@
.nav-item-data {
color: white;
background: var(--Slate-500, #161922);
background: var(--bg-slate-500, #161922);
}
}
&.active {
.nav-item-data {
color: white;
background: var(--Slate-500, #161922);
background: var(--bg-slate-500, #161922);
// color: #3f5ecc;
}
}
@@ -50,9 +56,9 @@
.nav-item-data {
flex-grow: 1;
max-width: calc(100% - 24px);
max-width: calc(100% - 20px);
display: flex;
margin: 0px 8px;
margin: 0px 0px 0px 6px;
padding: 2px 8px;
flex-direction: row;
align-items: center;
@@ -68,7 +74,7 @@
background: transparent;
transition: 0.2s all linear;
transition: 0.08s all ease;
border-radius: 3px;
@@ -100,7 +106,7 @@
&:hover {
.nav-item-label {
color: var(--Vanilla-100, #fff);
color: var(--bg-vanilla-100, #fff);
}
.nav-item-pin-icon {
@@ -120,6 +126,12 @@
.nav-item-active-marker {
background: #4e74f8;
}
.nav-item-data {
.nav-item-label {
color: var(--bg-slate-500);
}
}
}
&:hover {

View File

@@ -1,11 +1,12 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { Tag } from 'antd';
import { Tag, Tooltip } from 'antd';
import cx from 'classnames';
import { Pin, PinOff } from 'lucide-react';
import { SidebarItem } from '../sideNav.types';
import './NavItem.styles.scss';
import './NavItem.styles.scss';
export default function NavItem({
@@ -74,21 +75,25 @@ export default function NavItem({
)}
{onTogglePin && !isPinned && (
<Pin
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
<Tooltip title="Add to shortcuts" placement="right">
<Pin
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
</Tooltip>
)}
{onTogglePin && isPinned && (
<PinOff
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
<Tooltip title="Remove from shortcuts" placement="right">
<PinOff
size={12}
className="nav-item-pin-icon"
onClick={handleTogglePinClick}
color="var(--Vanilla-400, #c0c1c3)"
/>
</Tooltip>
)}
</div>
</div>

View File

@@ -1,5 +1,5 @@
.sidenav-container {
width: 64px;
width: 54px;
height: 100%;
position: relative;
z-index: 1;
@@ -10,47 +10,60 @@
}
.sideNav {
flex: 0 0 64px;
flex: 0 0 54px;
height: 100%;
max-width: 64px;
min-width: 64px;
width: 64px;
border-right: 1px solid var(--Slate-500, #161922);
background: var(--Ink-500, #0b0c0e);
max-width: 54px;
min-width: 54px;
width: 54px;
border-right: 1px solid var(--bg-slate-500, #161922);
background: var(--bg-ink-500, #0b0c0e);
padding-bottom: 48px;
transition: all 0.2s, background 0s, border 0s;
transition: all 0.08s ease, background 0s, border 0s;
.brand-container {
padding: 8px 18px;
padding: 8px 15px;
max-width: 100%;
background: transparent;
}
.brand-company-meta {
display: flex;
gap: 8px;
align-items: center;
gap: 6px;
flex-shrink: 0;
width: 100%;
justify-content: center;
}
.brand {
display: flex;
align-items: center;
justify-content: center;
max-width: 100%;
overflow: hidden;
gap: 32px;
height: 32px;
width: 100%;
.brand-logo {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: center;
gap: 8px;
flex-shrink: 0;
width: 20px;
height: 16px;
position: relative;
cursor: pointer;
img {
height: 16px;
width: auto;
display: block;
}
.brand-logo-name {
@@ -66,6 +79,10 @@
.brand-title-section {
display: none;
flex-shrink: 0;
align-items: center;
gap: 0;
position: relative;
.license-type {
display: flex;
@@ -76,7 +93,7 @@
color: var(--bg-vanilla-100);
border-radius: 4px 0px 0px 4px;
background: var(--Slate-400, #1d212d);
background: var(--bg-slate-400, #1d212d);
text-align: center;
font-family: Inter;
@@ -98,11 +115,11 @@
gap: 6px;
border-radius: 0px 4px 4px 0px;
background: var(--Slate-300, #242834);
background: var(--bg-slate-300, #242834);
}
.version {
color: var(--Vanilla-400, #c0c1c3);
color: var(--bg-vanilla-400, #c0c1c3);
text-align: center;
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'salt' on;
@@ -156,24 +173,48 @@
.get-started-nav-items {
display: flex;
margin: 4px 13px 12px 10px;
margin: 4px 10px 12px 8px;
.get-started-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
margin-left: 2px;
gap: 8px;
width: 100%;
height: 32px;
border-radius: 3px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Slate-500, #161922);
border: 1px solid var(--bg-slate-400, #1d212d);
background: var(--bg-slate-500, #161922);
box-shadow: none !important;
color: var(--bg-vanilla-400, #c0c1c3);
svg {
color: var(--bg-vanilla-400, #c0c1c3);
}
.nav-item-label {
color: var(--bg-vanilla-400, #c0c1c3);
}
&:hover:not(:disabled) {
background: var(--bg-slate-400, #1d212d);
border-color: var(--bg-slate-400, #1d212d);
color: var(--bg-vanilla-100, #fff);
svg {
color: var(--bg-vanilla-100, #fff);
}
.nav-item-label {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
@@ -192,6 +233,10 @@
width: 100%;
}
.nav-item {
margin-bottom: 6px;
}
.nav-top-section {
display: flex;
flex-direction: column;
@@ -227,7 +272,7 @@
}
.nav-section-title {
color: var(--Slate-50, #62687c);
color: var(--bg-slate-50, #62687c);
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -241,7 +286,7 @@
align-items: center;
gap: 8px;
padding: 0 20px;
padding: 0 17px;
.nav-section-title-text {
display: none;
@@ -250,11 +295,17 @@
.nav-section-title-icon {
display: flex;
align-items: center;
transition: opacity 0.08s ease, transform 0.08s ease;
&.reorder {
display: none;
cursor: pointer;
margin-left: auto;
transition: color 0.2s;
&:hover {
color: var(--bg-vanilla-100, #fff);
}
}
}
@@ -268,7 +319,7 @@
}
.nav-section-subtitle {
color: var(--Vanilla-400, #c0c1c3);
color: var(--bg-vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -276,20 +327,20 @@
line-height: 14px; /* 150% */
letter-spacing: 0.4px;
padding: 0 20px;
padding: 6px 20px;
opacity: 0.6;
display: none;
transition: all 0.3s, background 0s, border 0s;
transition-delay: 0.1s;
transition: all 0.08s ease, background 0s, border 0s;
transition-delay: 0.03s;
}
.nav-items-section {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
transition: all 0.08s ease;
}
}
@@ -302,7 +353,7 @@
.nav-items-section {
opacity: 0;
transform: translateY(-10px);
transition: all 0.4s ease;
transition: all 0.1s ease;
overflow: hidden;
height: 0;
}
@@ -312,11 +363,34 @@
.nav-items-section {
opacity: 1;
transform: translateY(0);
transition: all 0.4s ease;
transition: all 0.1s ease;
overflow: hidden;
height: auto;
}
}
&.sidebar-collapsed {
.nav-title-section {
display: none;
}
.nav-items-section {
margin-top: 0;
opacity: 1;
transform: translateY(0);
transition: all 0.08s ease;
height: auto;
overflow: visible;
}
}
}
.shortcut-nav-items {
&.sidebar-collapsed {
.nav-items-section {
margin-top: 0;
}
}
}
.scroll-for-more-container {
@@ -326,7 +400,7 @@
width: 100%;
bottom: 12px;
bottom: 8px;
margin-left: 50px;
margin-left: 43px;
.scroll-for-more {
display: flex;
@@ -370,8 +444,6 @@
overflow-y: auto;
overflow-x: hidden;
padding-top: 12px;
.secondary-nav-items {
display: flex;
flex-direction: column;
@@ -381,10 +453,10 @@
overflow-x: hidden;
padding: 8px 0;
max-width: 100%;
width: 64px;
width: 54px;
// width: 100%; // temp
transition: all 0.2s, background 0s, border 0s;
transition: all 0.08s ease, background 0s, border 0s;
background: linear-gradient(180deg, rgba(11, 12, 14, 0) 0%, #0b0c0e 27.11%);
@@ -413,7 +485,7 @@
&.scroll-available {
.nav-bottom-section {
border-top: 1px solid var(--Slate-500, #161922);
border-top: 1px solid var(--bg-slate-500, #161922);
}
}
}
@@ -424,24 +496,53 @@
}
&.collapsed {
flex: 0 0 64px;
max-width: 64px;
min-width: 64px;
width: 64px;
flex: 0 0 54px;
max-width: 54px;
min-width: 54px;
width: 54px;
.nav-wrapper {
.nav-top-section {
.shortcut-nav-items {
.nav-section-title,
.nav-section-title {
display: none;
}
.nav-section-subtitle {
display: none;
}
.nav-items-section {
display: flex;
margin-top: 0;
}
.nav-title-section {
margin-top: 0;
margin-bottom: 0;
gap: 0;
}
}
.more-nav-items {
.nav-section-title {
display: none;
}
.nav-items-section {
display: flex;
margin-top: 0;
}
.nav-title-section {
display: none;
}
}
}
.nav-bottom-section {
.secondary-nav-items {
width: 64px;
width: 54px;
}
}
}
@@ -466,7 +567,7 @@
border-radius: 12px;
background: var(--Robin-500, #4e74f8);
color: var(--Vanilla-100, #fff);
color: var(--bg-vanilla-100, #fff);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
font-family: Inter;
@@ -479,7 +580,7 @@
}
.sidenav-beta-tag {
color: var(--Vanilla-100, #fff);
color: var(--bg-vanilla-100, #fff);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
font-family: Inter;
@@ -494,7 +595,47 @@
background: var(--bg-slate-300);
}
&:hover {
&:not(.pinned) {
.nav-item {
.nav-item-data {
justify-content: center;
}
}
.shortcut-nav-items,
.more-nav-items {
.nav-section-title {
padding: 0 17px;
.nav-section-title-icon {
display: none;
}
}
}
&.dropdown-open {
.nav-item {
.nav-item-data {
flex-grow: 1;
justify-content: flex-start;
}
}
.shortcut-nav-items,
.more-nav-items {
.nav-section-title {
padding: 0 17px;
.nav-section-title-icon {
display: flex;
}
}
}
}
}
&:not(.pinned):hover,
&.dropdown-open {
flex: 0 0 240px;
max-width: 240px;
min-width: 240px;
@@ -505,8 +646,17 @@
z-index: 10;
background: #0b0c0e;
.brand-container {
padding: 8px 15px;
}
.brand {
justify-content: space-between;
justify-content: flex-start;
.brand-company-meta {
justify-content: flex-start;
width: 100%;
}
.brand-title-section {
display: flex;
@@ -533,6 +683,11 @@
.nav-section-title-icon {
&.reorder {
display: flex;
transition: color 0.2s;
&:hover {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
@@ -574,7 +729,7 @@
flex-direction: row;
gap: 3px;
border-radius: 20px;
background: var(--Slate-400, #1d212d);
background: var(--bg-slate-400, #1d212d);
/* Drop Shadow */
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
@@ -590,7 +745,7 @@
width: 140px;
.scroll-for-more-label {
color: var(--Vanilla-400, #c0c1c3);
color: var(--bg-vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -631,6 +786,13 @@
align-items: flex-start;
}
}
.nav-item {
.nav-item-data {
flex-grow: 1;
justify-content: flex-start;
}
}
}
.get-started-nav-items {
@@ -664,8 +826,17 @@
z-index: 10;
background: #0b0c0e;
.brand-container {
padding: 8px 15px;
}
.brand {
justify-content: space-between;
justify-content: flex-start;
.brand-company-meta {
justify-content: flex-start;
width: 100%;
}
.brand-title-section {
display: flex;
@@ -692,6 +863,11 @@
.nav-section-title-icon {
&.reorder {
display: flex;
transition: color 0.2s;
&:hover {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
@@ -733,7 +909,7 @@
flex-direction: row;
gap: 3px;
border-radius: 20px;
background: var(--Slate-400, #1d212d);
background: var(--bg-slate-400, #1d212d);
/* Drop Shadow */
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
@@ -751,7 +927,7 @@
.scroll-for-more-label {
display: block;
color: var(--Vanilla-400, #c0c1c3);
color: var(--bg-vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -856,7 +1032,7 @@
.ant-dropdown-menu-item {
.ant-dropdown-menu-title-content {
color: var(--Vanilla-400, #c0c1c3);
color: var(--bg-vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -864,6 +1040,12 @@
line-height: normal;
letter-spacing: 0.14px;
}
&:hover:not(.ant-dropdown-menu-item-disabled) {
.ant-dropdown-menu-title-content {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
}
@@ -875,7 +1057,7 @@
gap: 8px;
.user-settings-dropdown-label-text {
color: var(--Slate-50, #62687c);
color: var(--bg-slate-50, #62687c);
font-family: Inter;
font-size: 10px;
font-family: Inter;
@@ -887,7 +1069,7 @@
}
.user-settings-dropdown-label-email {
color: var(--Vanilla-400, #c0c1c3);
color: var(--bg-vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
@@ -897,12 +1079,16 @@
}
.ant-dropdown-menu-item-divider {
background-color: var(--Slate-500, #161922) !important;
background-color: var(--bg-slate-500, #161922) !important;
}
.ant-dropdown-menu-item-disabled {
opacity: 0.7;
}
.ant-dropdown-menu {
width: 100% !important;
}
}
.settings-dropdown,
@@ -912,6 +1098,27 @@
}
}
.secondary-nav-items {
.nav-item {
position: relative;
.nav-item-active-marker {
position: absolute;
left: -5px;
top: 50%;
transform: translateY(-50%);
margin: 0;
width: 8px;
height: 24px;
z-index: 1;
}
}
.nav-item-data {
margin-left: 8px !important;
}
}
.reorder-shortcut-nav-items-modal {
width: 384px !important;
@@ -1028,7 +1235,6 @@
display: flex;
align-items: center;
border-radius: 2px;
border-radius: 2px;
background: var(--Robin-500, #4e74f8) !important;
color: var(--bg-vanilla-100) !important;
font-family: Inter;
@@ -1038,10 +1244,10 @@
line-height: 24px;
&.secondary-btn {
background-color: var(--Slate-500, #161922) !important;
background-color: var(--bg-slate-500, #161922) !important;
border: 1px solid var(--bg-slate-500) !important;
color: var(--Vanilla-400, #c0c1c3) !important;
color: var(--bg-vanilla-400, #c0c1c3) !important;
/* button/ small */
font-family: Inter;
@@ -1064,6 +1270,10 @@
}
}
.help-support-dropdown li.ant-dropdown-menu-item-divider {
background-color: var(--bg-slate-500, #161922) !important;
}
.lightMode {
.sideNav {
background: var(--bg-vanilla-100);
@@ -1095,8 +1305,32 @@
.get-started-nav-items {
.get-started-btn {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
color: var(--bg-ink-400);
background: var(--bg-vanilla-200);
color: var(--bg-slate-50, #62687c);
svg {
color: var(--bg-slate-50, #62687c);
}
.nav-item-label {
color: var(--bg-ink-400, #62687c);
}
// Hover state (light mode)
&:hover:not(:disabled) {
background: var(--bg-vanilla-300);
border-color: var(--bg-vanilla-300);
color: var(--bg-slate-500, #161922);
svg {
color: var(--bg-slate-500, #161922);
}
.nav-item-label {
color: var(--bg-slate-500, #161922);
}
}
}
}
@@ -1108,7 +1342,25 @@
}
}
.brand-container {
background: transparent;
}
.nav-wrapper {
.nav-top-section {
.shortcut-nav-items {
.nav-section-title {
.nav-section-title-icon {
&.reorder {
&:hover {
color: var(--bg-slate-400, #1d212d);
}
}
}
}
}
}
.secondary-nav-items {
border-top: 1px solid var(--bg-vanilla-300);
@@ -1123,8 +1375,43 @@
}
}
&:hover {
&.pinned {
.nav-wrapper {
.nav-top-section {
.shortcut-nav-items {
.nav-section-title {
.nav-section-title-icon {
&.reorder {
&:hover {
color: var(--bg-slate-400, #1d212d);
}
}
}
}
}
}
}
}
&:not(.pinned):hover,
&.dropdown-open {
background: var(--bg-vanilla-100);
.nav-wrapper {
.nav-top-section {
.shortcut-nav-items {
.nav-section-title {
.nav-section-title-icon {
&.reorder {
&:hover {
color: var(--bg-slate-400, #1d212d);
}
}
}
}
}
}
}
}
}
@@ -1134,6 +1421,12 @@
.ant-dropdown-menu-title-content {
color: var(--bg-ink-400);
}
&:hover:not(.ant-dropdown-menu-item-disabled) {
.ant-dropdown-menu-title-content {
color: var(--bg-ink-500);
}
}
}
}
}
@@ -1210,6 +1503,10 @@
color: var(--bg-ink-400);
}
}
.help-support-dropdown li.ant-dropdown-menu-item-divider {
background-color: var(--bg-vanilla-300) !important;
}
}
.version-tooltip-overlay {
@@ -1222,7 +1519,7 @@
border-radius: 2px;
border: 1px solid var(--bg-slate-500);
color: var(--Vanilla-100, #fff);
color: var(--bg-vanilla-100, #fff);
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -1237,7 +1534,7 @@
gap: 4px;
.version-update-notification-tooltip-title {
color: var(--Vanilla-100, #fff);
color: var(--bg-vanilla-100, #fff);
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -1247,7 +1544,7 @@
}
.version-update-notification-tooltip-content {
color: var(--Vanilla-100, #fff);
color: var(--bg-vanilla-100, #fff);
font-family: Inter;
font-size: 10px;
font-style: normal;

View File

@@ -157,18 +157,27 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
DefaultHelpSupportDropdownMenuItems,
);
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
const [tempPinnedMenuItems, setTempPinnedMenuItems] = useState<SidebarItem[]>(
[],
);
const [secondaryMenuItems, setSecondaryMenuItems] = useState<SidebarItem[]>(
[],
);
const [hasScroll, setHasScroll] = useState(false);
const navTopSectionRef = useRef<HTMLDivElement>(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
const [secondaryMenuItems, setSecondaryMenuItems] = useState<SidebarItem[]>(
[],
);
const handleMouseEnter = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseLeave = useCallback(() => {
setIsHovered(false);
}, []);
const checkScroll = useCallback((): void => {
if (navTopSectionRef.current) {
@@ -217,63 +226,68 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isAdmin = user.role === USER_ROLES.ADMIN;
const isEditor = user.role === USER_ROLES.EDITOR;
useEffect(() => {
const navShortcuts = (userPreferences?.find(
// Compute initial pinned items and secondary menu items synchronously to avoid flash
const computedPinnedMenuItems = useMemo(() => {
const navShortcutsPreference = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.NAV_SHORTCUTS,
)?.value as unknown) as string[];
);
const navShortcuts = (navShortcutsPreference?.value as unknown) as
| string[]
| undefined;
const shouldShowIntegrations =
(isCloudUser || isEnterpriseSelfHostedUser) && (isAdmin || isEditor);
// If userPreferences not loaded yet, return empty to avoid showing defaults before preferences load
if (userPreferences === null) {
return [];
}
if (navShortcuts && isArray(navShortcuts) && navShortcuts.length > 0) {
// nav shortcuts is array of strings
const pinnedItems = navShortcuts
// If preference exists with non-empty array, use stored shortcuts
if (isArray(navShortcuts) && navShortcuts.length > 0) {
return navShortcuts
.map((shortcut) =>
defaultMoreMenuItems.find((item) => item.itemKey === shortcut),
)
.filter((item): item is SidebarItem => item !== undefined);
// Set pinned items in the order they were stored
setPinnedMenuItems(pinnedItems);
setSecondaryMenuItems(
defaultMoreMenuItems.map((item) => ({
...item,
isPinned: pinnedItems.some((pinned) => pinned.itemKey === item.itemKey),
isEnabled:
item.key === ROUTES.INTEGRATIONS
? shouldShowIntegrations
: item.isEnabled,
})),
);
} else {
// Set default pinned items
const defaultPinnedItems = defaultMoreMenuItems.filter(
(item) => item.isPinned,
);
setPinnedMenuItems(defaultPinnedItems);
setSecondaryMenuItems(
defaultMoreMenuItems.map((item) => ({
...item,
isPinned: defaultPinnedItems.some(
(pinned) => pinned.itemKey === item.itemKey,
),
isEnabled:
item.key === ROUTES.INTEGRATIONS
? shouldShowIntegrations
: item.isEnabled,
})),
);
}
// No preference, or empty array → use defaults
return defaultMoreMenuItems.filter((item) => item.isPinned);
}, [userPreferences]);
const computedSecondaryMenuItems = useMemo(() => {
const shouldShowIntegrationsValue =
(isCloudUser || isEnterpriseSelfHostedUser) && (isAdmin || isEditor);
return defaultMoreMenuItems.map((item) => ({
...item,
isPinned: computedPinnedMenuItems.some(
(pinned) => pinned.itemKey === item.itemKey,
),
isEnabled:
item.key === ROUTES.INTEGRATIONS
? shouldShowIntegrationsValue
: item.isEnabled,
}));
}, [
userPreferences,
computedPinnedMenuItems,
isCloudUser,
isEnterpriseSelfHostedUser,
isAdmin,
isEditor,
]);
// Track if we've done the initial sync (to avoid overwriting user actions during session)
const hasInitializedRef = useRef(false);
// Sync state only on initial load when userPreferences first becomes available
useEffect(() => {
// Only sync once: when userPreferences loads for the first time
if (!hasInitializedRef.current && userPreferences !== null) {
setPinnedMenuItems(computedPinnedMenuItems);
setSecondaryMenuItems(computedSecondaryMenuItems);
hasInitializedRef.current = true;
}
}, [computedPinnedMenuItems, computedSecondaryMenuItems, userPreferences]);
const isOnboardingV3Enabled = featureFlags?.find(
(flag) => flag.name === FeatureKeys.ONBOARDING_V3,
)?.active;
@@ -327,6 +341,17 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
.map((item) => item.itemKey)
.filter(Boolean) as string[];
// Update context immediately (optimistically) so computed values reflect the change
updateUserPreferenceInContext({
name: USER_PREFERENCES.NAV_SHORTCUTS,
description: USER_PREFERENCES.NAV_SHORTCUTS,
valueType: 'array',
defaultValue: false,
allowedValues: [],
allowedScopes: ['user'],
value: navShortcuts,
});
updateUserPreferenceMutation(
{
name: USER_PREFERENCES.NAV_SHORTCUTS,
@@ -335,6 +360,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
{
onSuccess: (response) => {
if (response.data) {
// Update context again on success to ensure consistency
updateUserPreferenceInContext({
name: USER_PREFERENCES.NAV_SHORTCUTS,
description: USER_PREFERENCES.NAV_SHORTCUTS,
@@ -368,13 +394,13 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
if (isCurrentlyPinned) {
return prevItems.filter((i) => i.key !== item.key);
}
return [item, ...prevItems];
return [...prevItems, item];
});
// Get the updated pinned menu items for preference update
const updatedPinnedItems = pinnedMenuItems.some((i) => i.key === item.key)
? pinnedMenuItems.filter((i) => i.key !== item.key)
: [item, ...pinnedMenuItems];
: [...pinnedMenuItems, item];
// Update user preference with the ordered list of item keys
updateNavShortcutsPreference(updatedPinnedItems);
@@ -455,6 +481,10 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
pathname,
]);
const isSettingsPage = useMemo(() => pathname.startsWith(ROUTES.SETTINGS), [
pathname,
]);
const userSettingsDropdownMenuItems: MenuProps['items'] = useMemo(
() =>
[
@@ -594,7 +624,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
},
{
type: 'group',
label: "WHAT's NEW",
label: "WHAT'S NEW",
},
...dropdownItems,
{
@@ -750,6 +780,15 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
[secondaryMenuItems],
);
// Get active "More" items that should be visible in collapsed state
const activeMoreMenuItems = useMemo(
() => moreMenuItems.filter((item) => activeMenuKey === item.key),
[moreMenuItems, activeMenuKey],
);
// Check if sidebar is collapsed (not pinned, not hovered, and no dropdown open)
const isCollapsed = !isPinned && !isHovered && !isDropdownOpen;
const renderNavItems = (
items: SidebarItem[],
allowPin?: boolean,
@@ -901,7 +940,15 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
return (
<div className={cx('sidenav-container', isPinned && 'pinned')}>
<div className={cx('sideNav', isPinned && 'pinned')}>
<div
className={cx(
'sideNav',
isPinned && 'pinned',
isDropdownOpen && 'dropdown-open',
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="brand-container">
<div className="brand">
<div className="brand-company-meta">
@@ -999,35 +1046,43 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
{renderNavItems(primaryMenuItems)}
</div>
<div className="shortcut-nav-items">
<div className="nav-title-section">
<div className="nav-section-title">
<div className="nav-section-title-icon">
<MousePointerClick size={16} />
</div>
{(pinnedMenuItems.length > 0 || !isCollapsed) && (
<div
className={cx('shortcut-nav-items', isCollapsed && 'sidebar-collapsed')}
>
{!isCollapsed && (
<div className="nav-title-section">
<div className="nav-section-title">
<div className="nav-section-title-icon">
<MousePointerClick size={16} />
</div>
<div className="nav-section-title-text">SHORTCUTS</div>
<div className="nav-section-title-text">SHORTCUTS</div>
{pinnedMenuItems.length > 1 && (
<div
className="nav-section-title-icon reorder"
onClick={(): void => {
logEvent('Sidebar V2: Manage shortcuts clicked', {});
setIsReorderShortcutNavItemsModalOpen(true);
}}
>
<Logs size={16} />
{pinnedMenuItems.length > 1 && (
<Tooltip title="Manage shortcuts" placement="right">
<div
className="nav-section-title-icon reorder"
onClick={(): void => {
logEvent('Sidebar V2: Manage shortcuts clicked', {});
setIsReorderShortcutNavItemsModalOpen(true);
}}
>
<Logs size={16} />
</div>
</Tooltip>
)}
</div>
)}
</div>
{pinnedMenuItems.length === 0 && (
<div className="nav-section-subtitle">
You have not added any shortcuts yet.
{pinnedMenuItems.length === 0 && (
<div className="nav-section-subtitle">
You have not added any shortcuts yet.
</div>
)}
</div>
)}
{pinnedMenuItems.length > 0 && (
{(pinnedMenuItems.length > 0 || isCollapsed) && (
<div className="nav-items-section">
{renderNavItems(
pinnedMenuItems.filter((item) => item.isEnabled),
@@ -1036,46 +1091,60 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
</div>
)}
</div>
</div>
)}
{moreMenuItems.length > 0 && (
<div
className={cx(
'more-nav-items',
isMoreMenuCollapsed ? 'collapsed' : 'expanded',
isCollapsed && 'sidebar-collapsed',
)}
>
<div className="nav-title-section">
<div
className="nav-section-title"
onClick={(): void => {
logEvent('Sidebar V2: More menu clicked', {
action: isMoreMenuCollapsed ? 'expand' : 'collapse',
});
setIsMoreMenuCollapsed(!isMoreMenuCollapsed);
}}
>
<div className="nav-section-title-icon">
<Ellipsis size={16} />
</div>
{!isCollapsed && (
<div className="nav-title-section">
<div
className="nav-section-title"
onClick={(): void => {
// Only allow toggling when sidebar is open (pinned, hovered, or dropdown open)
if (isCollapsed) {
return;
}
const newCollapsedState = !isMoreMenuCollapsed;
logEvent('Sidebar V2: More menu clicked', {
action: isMoreMenuCollapsed ? 'expand' : 'collapse',
});
setIsMoreMenuCollapsed(newCollapsedState);
}}
>
<div className="nav-section-title-icon">
<Ellipsis size={16} />
</div>
<div className="nav-section-title-text">MORE</div>
<div className="nav-section-title-text">MORE</div>
<div className="collapse-expand-section-icon">
{isMoreMenuCollapsed ? (
<ChevronDown size={16} />
) : (
<ChevronUp size={16} />
)}
<div className="collapse-expand-section-icon">
{isMoreMenuCollapsed ? (
<ChevronDown size={16} />
) : (
<ChevronUp size={16} />
)}
</div>
</div>
</div>
</div>
)}
<div className="nav-items-section">
{renderNavItems(
moreMenuItems.filter((item) => item.isEnabled),
true,
)}
{/* Show all items when expanded, only active items when collapsed */}
{isCollapsed
? renderNavItems(
activeMoreMenuItems.filter((item) => item.isEnabled),
true,
)
: renderNavItems(
moreMenuItems.filter((item) => item.isEnabled),
true,
)}
</div>
</div>
)}
@@ -1102,6 +1171,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
placement="topLeft"
overlayClassName="nav-dropdown-overlay help-support-dropdown"
trigger={['click']}
onOpenChange={(open): void => setIsDropdownOpen(open)}
>
<div className="nav-item">
<div className="nav-item-data" data-testid="help-support-nav-item">
@@ -1122,8 +1192,10 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
placement="topLeft"
overlayClassName="nav-dropdown-overlay settings-dropdown"
trigger={['click']}
onOpenChange={(open): void => setIsDropdownOpen(open)}
>
<div className="nav-item">
<div className={cx('nav-item', isSettingsPage && 'active')}>
<div className="nav-item-active-marker" />
<div className="nav-item-data" data-testid="settings-nav-item">
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>

View File

@@ -33,6 +33,19 @@
height: calc(100vh - 48px);
border-right: 1px solid var(--Slate-500, #161922);
background: var(--Ink-500, #0b0c0e);
margin-top: 4px;
.nav-item {
.nav-item-data {
margin: 0px 8px 0px 4px;
}
&.active {
.nav-item-data .nav-item-label {
color: var(--bg-vanilla-100, #fff);
}
}
}
}
.settings-page-content {
@@ -81,6 +94,14 @@
.settings-page-sidenav {
border-right: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.nav-item {
&.active {
.nav-item-data .nav-item-label {
color: var(--bg-ink-500);
}
}
}
}
.settings-page-content {

View File

@@ -13,7 +13,7 @@ import { SidebarItem } from 'container/SideNav/sideNav.types';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { Wrench } from 'lucide-react';
import { Cog } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
@@ -236,7 +236,7 @@ function SettingsPage(): JSX.Element {
className="settings-page-header-title"
data-testid="settings-page-title"
>
<Wrench size={16} />
<Cog size={16} />
Settings
</div>
</header>