chore: collapse conversation sidebar sections (folders, teams, inboxes and labels) - CW-7059 (#14509)

## Description

Added ability to collapse conversation sidebar sections (folders, teams,
inboxes and labels)

Fixes #CW-7059

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested locally. Added specs. Attaching the loom for them same.


https://github.com/user-attachments/assets/40d613e7-6c82-4078-abf4-79739a00f718


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
Sony Mathew
2026-06-14 06:54:18 +05:30
committed by GitHub
co-authored by Sivin Varghese iamsivin
parent 27404b4a27
commit 274e92e0e4
8 changed files with 423 additions and 142 deletions
@@ -300,6 +300,7 @@ const menuItems = computed(() => {
{
name: 'All',
label: t('SIDEBAR.ALL_CONVERSATIONS'),
icon: 'i-lucide-inbox',
badgeCount: allUnreadCount.value,
activeOn: ['inbox_conversation'],
to: accountScopedRoute('home'),
@@ -307,12 +308,14 @@ const menuItems = computed(() => {
{
name: 'Mentions',
label: t('SIDEBAR.MENTIONED_CONVERSATIONS'),
icon: 'i-lucide-at-sign',
activeOn: ['conversation_through_mentions'],
to: accountScopedRoute('conversation_mentions'),
},
{
name: 'Participating',
label: t('SIDEBAR.PARTICIPATING_CONVERSATIONS'),
icon: 'i-lucide-user-round-check',
activeOn: ['conversation_through_participating'],
to: accountScopedRoute('conversation_participating'),
},
@@ -320,6 +323,7 @@ const menuItems = computed(() => {
name: 'Unattended',
activeOn: ['conversation_through_unattended'],
label: t('SIDEBAR.UNATTENDED_CONVERSATIONS'),
icon: 'i-lucide-clock-alert',
to: accountScopedRoute('conversation_unattended'),
},
{
@@ -327,6 +331,8 @@ const menuItems = computed(() => {
label: t('SIDEBAR.CUSTOM_VIEWS_FOLDER'),
icon: 'i-lucide-folder',
activeOn: ['conversations_through_folders'],
collapsible: true,
showTreeLine: true,
children: conversationCustomViews.value.map(view => ({
name: `${view.name}-${view.id}`,
label: view.name,
@@ -338,6 +344,8 @@ const menuItems = computed(() => {
label: t('SIDEBAR.TEAMS'),
icon: 'i-lucide-users',
activeOn: ['conversations_through_team'],
collapsible: true,
showTreeLine: true,
children: sortedTeams.value.map(team => ({
name: `${team.name}-${team.id}`,
label: team.name,
@@ -350,6 +358,8 @@ const menuItems = computed(() => {
label: t('SIDEBAR.CHANNELS'),
icon: 'i-lucide-mailbox',
activeOn: ['conversation_through_inbox'],
collapsible: true,
showTreeLine: true,
children: sortedInboxes.value.map(inbox => ({
name: `${inbox.name}-${inbox.id}`,
label: inbox.name,
@@ -370,6 +380,8 @@ const menuItems = computed(() => {
label: t('SIDEBAR.LABELS'),
icon: 'i-lucide-tag',
activeOn: ['conversations_through_label'],
collapsible: true,
showTreeLine: true,
children: sortedLabels.value.map(label => ({
name: `${label.title}-${label.id}`,
label: label.title,
@@ -481,6 +493,8 @@ const menuItems = computed(() => {
name: 'Segments',
icon: 'i-lucide-group',
label: t('SIDEBAR.CUSTOM_VIEWS_SEGMENTS'),
collapsible: true,
showTreeLine: true,
children: contactCustomViews.value.map(view => ({
name: `${view.name}-${view.id}`,
label: view.name,
@@ -499,6 +513,8 @@ const menuItems = computed(() => {
name: 'Tagged With',
icon: 'i-lucide-tag',
label: t('SIDEBAR.TAGGED_WITH'),
collapsible: true,
showTreeLine: true,
children: labels.value.map(label => ({
name: `${label.title}-${label.id}`,
label: label.title,
@@ -99,20 +99,39 @@ const handleWindowBlur = () => {
closeActivePopover();
};
const accessibleItems = computed(() => {
const hasAccessibleSubChildren = child => {
return child.children?.some(
subChild => subChild.to && isAllowed(subChild.to)
);
};
const visibleChildren = computed(() => {
if (!hasChildren.value) return [];
return props.children.filter(child => {
// If a item has no link, it means it's just a subgroup header
// So we don't need to check for permissions here, because there's nothing to
// access here anyway
if (child.children) return hasAccessibleSubChildren(child);
return child.to && isAllowed(child.to);
});
});
const hasAccessibleChildren = computed(() => {
return accessibleItems.value.length > 0;
const accessibleItems = computed(() => {
if (!hasChildren.value) return [];
return visibleChildren.value
.flatMap(child => child.children || child)
.filter(child => child.to && isAllowed(child.to));
});
const hasAccessibleChildren = computed(() => {
return visibleChildren.value.length > 0;
});
const isLastVisibleChild = child => {
const lastChild = visibleChildren.value[visibleChildren.value.length - 1];
return lastChild === child;
};
const isActive = computed(() => {
if (props.to) {
if (route.path === resolvePath(props.to)) return true;
@@ -274,14 +293,18 @@ watch(
<ul
v-if="hasChildren"
v-show="isExpanded || hasActiveChild"
class="grid m-0 list-none sidebar-group-children min-w-0"
class="grid m-0 list-none min-w-0"
>
<template v-for="child in children" :key="child.name">
<template v-for="child in visibleChildren" :key="child.name">
<SidebarSubGroup
v-if="child.children"
:name="`${name}:${child.name}`"
:label="child.label"
:icon="child.icon"
:children="child.children"
:collapsible="child.collapsible"
:show-tree-line="child.showTreeLine"
:end-tree-line="child.showTreeLine && isLastVisibleChild(child)"
:is-expanded="isExpanded"
:active-child="activeChild"
/>
@@ -299,59 +322,3 @@ watch(
</template>
</Policy>
</template>
<style>
.sidebar-group-children .child-item::before {
content: '';
position: absolute;
width: 0.125rem;
/* 0.5px */
height: 100%;
}
.sidebar-group-children .child-item:first-child::before {
border-radius: 4px 4px 0 0;
}
/* This selects the last child in a group */
/* https://codepen.io/scmmishra/pen/yLmKNLW */
.sidebar-group-children > .child-item:last-child::before,
.sidebar-group-children
> *:last-child
> *:last-child
> .child-item:last-child::before {
height: 20%;
}
.sidebar-group-children > .child-item:last-child::after,
.sidebar-group-children
> *:last-child
> *:last-child
> .child-item:last-child::after {
content: '';
position: absolute;
width: 10px;
height: 12px;
bottom: calc(50% - 2px);
border-bottom-width: 0.125rem;
border-left-width: 0.125rem;
border-right-width: 0px;
border-top-width: 0px;
border-radius: 0 0 0 4px;
left: 0;
}
#app[dir='rtl'] .sidebar-group-children > .child-item:last-child::after,
#app[dir='rtl']
.sidebar-group-children
> *:last-child
> *:last-child
> .child-item:last-child::after {
right: 0;
border-bottom-width: 0.125rem;
border-right-width: 0.125rem;
border-left-width: 0px;
border-top-width: 0px;
border-radius: 0 0 4px 0px;
}
</style>
@@ -12,6 +12,8 @@ const props = defineProps({
active: { type: Boolean, default: false },
component: { type: Function, default: null },
badgeCount: { type: [Number, String], default: 0 },
hideTreeLine: { type: Boolean, default: false },
thinTreeLine: { type: Boolean, default: false },
});
const { resolvePermissions, resolveFeatureFlag } = useSidebarContext();
@@ -19,21 +21,30 @@ const { resolvePermissions, resolveFeatureFlag } = useSidebarContext();
const shouldRenderComponent = computed(() => {
return typeof props.component === 'function' || isVNode(props.component);
});
// Tree-line connector per leaf: vertical line (::before) + rounded elbow on the
// last child (::after). Logical props (start / border-s / rounded-es)
const TREE_CONNECTOR =
"child-item before:content-[''] before:absolute before:start-0 before:w-0.5 before:h-full before:bg-n-slate-4 first:before:rounded-t last:before:h-1/5 last:after:content-[''] last:after:absolute last:after:start-0 last:after:bottom-[calc(50%_-_2px)] last:after:h-3 last:after:w-2.5 last:after:border-b-2 last:after:border-s-2 last:after:rounded-es last:after:border-n-slate-4";
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<Policy
:permissions="resolvePermissions(to)"
:feature-flag="resolveFeatureFlag(to)"
as="li"
class="py-0.5 ltr:pl-2 rtl:pr-2 rtl:mr-3 ltr:ml-3 relative text-n-slate-11 child-item before:bg-n-slate-4 after:bg-transparent after:border-n-slate-4 before:left-0 rtl:before:right-0 min-w-0"
class="py-0.5 ps-2 ms-3 relative text-n-slate-11 min-w-0"
:class="{
[TREE_CONNECTOR]: !hideTreeLine,
'before:!w-px last:after:!border-b last:after:!border-s':
!hideTreeLine && thinTreeLine,
}"
>
<component
:is="to ? 'router-link' : 'div'"
:to="to"
:title="label"
class="flex h-8 items-center gap-2 px-2 py-1 rounded-lg hover:bg-gradient-to-r from-transparent via-n-slate-3/70 to-n-slate-3/70 group min-w-0"
class="flex h-8 items-center gap-2 px-2 py-1 rounded-lg ltr:hover:bg-gradient-to-r rtl:hover:bg-gradient-to-l from-transparent via-n-slate-3/70 to-n-slate-3/70 group min-w-0"
:class="{
'text-n-slate-12 bg-n-alpha-2 active': active,
}"
@@ -2,24 +2,69 @@
import Icon from 'next/icon/Icon.vue';
defineProps({
collapsible: {
type: Boolean,
default: false,
},
isExpanded: {
type: Boolean,
default: true,
},
label: {
type: String,
default: '',
},
icon: {
type: String,
type: [Object, String],
default: '',
},
showTreeLine: {
type: Boolean,
default: false,
},
endTreeLine: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['toggle']);
const TREE_VERTICAL_LINE =
"before:content-[''] before:absolute before:-top-1 before:w-0.5 before:bg-n-slate-4 before:start-[-0.5rem]";
const TREE_ELBOW =
"after:content-[''] after:absolute after:w-2.5 after:h-3 after:bottom-1/2 after:start-[-0.5rem] after:border-b-2 after:border-s-2 after:rounded-es after:border-n-slate-4";
</script>
<template>
<div
class="flex items-center gap-2 px-2 py-1.5 rounded-lg h-8 text-n-slate-10 select-none pointer-events-none"
<component
:is="collapsible ? 'button' : 'div'"
:type="collapsible ? 'button' : undefined"
:aria-expanded="collapsible ? isExpanded : undefined"
:title="label"
class="relative justify-between flex items-center gap-2 px-2 py-1.5 rounded-lg h-8 text-n-slate-10 select-none min-w-0"
:class="[
showTreeLine && TREE_VERTICAL_LINE,
showTreeLine &&
(endTreeLine ? `before:h-3 ${TREE_ELBOW}` : 'before:-bottom-1'),
{
'w-full': !collapsible,
'pointer-events-none': !collapsible,
'ms-5 cursor-pointer hover:bg-n-alpha-2': collapsible,
},
]"
@click.stop="emit('toggle')"
>
<Icon v-if="icon" :icon="icon" class="size-4" />
<span class="text-sm font-medium leading-5 flex-grow">
{{ label }}
</span>
</div>
<div class="min-w-0 inline-flex gap-2 items-center">
<Icon v-if="icon" :icon="icon" class="size-4 flex-shrink-0" />
<span class="text-sm font-medium leading-5 flex-grow truncate text-start">
{{ label }}
</span>
</div>
<span
v-if="collapsible"
class="size-3 flex-shrink-0 text-n-slate-10 me-0.5"
:class="isExpanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
/>
</component>
</template>
@@ -1,5 +1,9 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import Icon from 'next/icon/Icon.vue';
import SidebarGroupLeaf from './SidebarGroupLeaf.vue';
import SidebarGroupSeparator from './SidebarGroupSeparator.vue';
@@ -7,15 +11,42 @@ import { useSidebarContext } from './provider';
import { useEventListener } from '@vueuse/core';
const props = defineProps({
name: { type: String, required: true },
isExpanded: { type: Boolean, default: false },
label: { type: String, required: true },
icon: { type: [Object, String], required: true },
children: { type: Array, default: undefined },
activeChild: { type: Object, default: undefined },
collapsible: { type: Boolean, default: false },
showTreeLine: { type: Boolean, default: false },
endTreeLine: { type: Boolean, default: false },
});
const { isAllowed } = useSidebarContext();
const scrollableContainer = ref(null);
const accountId = useMapGetter('getCurrentAccountId');
const minimizedSectionsKey = LOCAL_STORAGE_KEYS.SIDEBAR_MINIMIZED_SECTIONS;
const getMinimizedSections = () => {
const minimizedSections = LocalStorage.get(minimizedSectionsKey);
return minimizedSections &&
typeof minimizedSections === 'object' &&
!Array.isArray(minimizedSections)
? minimizedSections
: {};
};
const minimizedSections = ref(getMinimizedSections());
const storageKey = computed(() =>
accountId.value ? `${accountId.value}:${props.name}` : props.name
);
const isSubGroupExpanded = computed(
() => !props.collapsible || !minimizedSections.value[storageKey.value]
);
const hasActiveChild = computed(() =>
props.children.some(child => child.name === props.activeChild?.name)
);
const accessibleItems = computed(() =>
props.children.filter(child => {
@@ -28,84 +59,117 @@ const hasAccessibleItems = computed(() => {
});
const isScrollable = computed(() => {
return accessibleItems.value.length > 7;
return (
props.isExpanded &&
isSubGroupExpanded.value &&
accessibleItems.value.length > 7
);
});
const scrollEnd = ref(false);
const CHILDREN_TRUNK =
"before:content-[''] before:absolute before:top-0 before:bottom-0 before:w-0.5 before:bg-n-slate-4 before:start-[-0.5rem]";
const hideLeafTreeLine = computed(
() => props.showTreeLine && !props.isExpanded
);
const toggleSubGroup = () => {
if (!props.collapsible) return;
if (isSubGroupExpanded.value) {
LocalStorage.updateJsonStore(minimizedSectionsKey, storageKey.value, true);
} else {
LocalStorage.deleteFromJsonStore(minimizedSectionsKey, storageKey.value);
}
minimizedSections.value = getMinimizedSections();
};
const expandSubGroupOnActiveChild = () => {
if (!props.collapsible || !hasActiveChild.value || isSubGroupExpanded.value) {
return;
}
LocalStorage.deleteFromJsonStore(minimizedSectionsKey, storageKey.value);
minimizedSections.value = getMinimizedSections();
};
const shouldShowItem = child => {
return (
isSubGroupExpanded.value &&
(props.isExpanded || props.activeChild?.name === child.name)
);
};
// set scrollEnd to true when the scroll reaches the end
useEventListener(scrollableContainer, 'scroll', () => {
const { scrollHeight, scrollTop, clientHeight } = scrollableContainer.value;
scrollEnd.value = scrollHeight - scrollTop === clientHeight;
});
useEventListener(window, 'storage', event => {
if (event.key === minimizedSectionsKey) {
minimizedSections.value = getMinimizedSections();
}
});
watch([hasActiveChild, storageKey], expandSubGroupOnActiveChild, {
immediate: true,
});
</script>
<template>
<SidebarGroupSeparator
v-if="hasAccessibleItems"
v-show="isExpanded"
:label
:icon
class="my-1"
/>
<ul
v-if="children.length"
class="m-0 list-none reset-base relative group min-w-0"
>
<!-- Each element has h-8, which is 32px, we will show 7 items with one hidden at the end,
which is 14rem. Then we add 16px so that we have some text visible from the next item -->
<div
ref="scrollableContainer"
class="min-w-0"
:class="{
'max-h-[calc(14rem+16px)] overflow-y-scroll no-scrollbar': isScrollable,
}"
>
<SidebarGroupLeaf
v-for="child in children"
v-show="isExpanded || activeChild?.name === child.name"
v-bind="child"
:key="child.name"
:active="activeChild?.name === child.name"
<li class="relative flex flex-col list-none min-w-0">
<template v-if="hasAccessibleItems">
<SidebarGroupSeparator
v-show="isExpanded"
:label
:icon
:collapsible
:is-expanded="isSubGroupExpanded"
:show-tree-line="showTreeLine"
:end-tree-line="endTreeLine"
class="my-1"
@toggle="toggleSubGroup"
/>
</div>
<div
v-if="isScrollable && isExpanded"
v-show="!scrollEnd"
class="absolute bg-gradient-to-t from-n-background w-full h-12 to-transparent -bottom-1 pointer-events-none flex items-end justify-end px-2 animate-fade-in-up"
>
<svg
width="16"
height="24"
viewBox="0 0 16 24"
fill="none"
class="text-n-slate-9 opacity-50 group-hover:opacity-100"
xmlns="http://www.w3.org/2000/svg"
<ul
v-if="children.length"
class="m-0 list-none reset-base relative group min-w-0"
:class="[
{ 'ms-5': collapsible },
showTreeLine && !endTreeLine && CHILDREN_TRUNK,
]"
>
<path
d="M4 4L8 8L12 4"
stroke="currentColor"
opacity="0.5"
stroke-width="1.33333"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4 10L8 14L12 10"
stroke="currentColor"
opacity="0.75"
stroke-width="1.33333"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4 16L8 20L12 16"
stroke="currentColor"
stroke-width="1.33333"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
</ul>
<div
ref="scrollableContainer"
class="min-w-0"
:class="{
'max-h-60 overflow-y-scroll no-scrollbar': isScrollable,
}"
>
<SidebarGroupLeaf
v-for="child in children"
v-show="shouldShowItem(child)"
v-bind="child"
:key="child.name"
:active="activeChild?.name === child.name"
:hide-tree-line="hideLeafTreeLine"
thin-tree-line
/>
</div>
<div
v-if="isScrollable && isExpanded"
v-show="!scrollEnd"
class="absolute bg-gradient-to-t from-n-background w-full h-12 to-transparent -bottom-1 pointer-events-none flex items-end justify-end px-2 animate-fade-in-up"
>
<Icon
icon="i-woot-chevrons-down"
class="w-4 h-6 text-n-slate-9 opacity-50 group-hover:opacity-100"
/>
</div>
</ul>
</template>
</li>
</template>
@@ -0,0 +1,172 @@
import { mount } from '@vue/test-utils';
import { ref } from 'vue';
import SidebarSubGroup from '../SidebarSubGroup.vue';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { provideSidebarContext } from '../provider';
vi.mock('dashboard/composables/store', () => ({
useMapGetter: () => ref(1),
}));
vi.mock('dashboard/composables/usePolicy', () => ({
usePolicy: () => ({
shouldShow: () => true,
}),
}));
vi.mock('vue-router', () => ({
useRouter: () => ({
resolve: () => ({ path: '/' }),
getRoutes: () => [],
}),
}));
const children = [
{
name: 'Sales-1',
label: 'Sales',
to: { name: 'team_conversations' },
},
];
const mountSubGroup = props => {
return mount(
{
components: { SidebarSubGroup },
setup() {
provideSidebarContext({});
},
template: '<SidebarSubGroup v-bind="$attrs" />',
},
{
attrs: {
name: 'Conversation:Teams',
label: 'Teams',
icon: 'i-lucide-users',
children,
isExpanded: true,
collapsible: true,
...props,
},
global: {
stubs: {
SidebarGroupLeaf: {
props: {
label: { type: String, required: true },
hideTreeLine: { type: Boolean, default: false },
thinTreeLine: { type: Boolean, default: false },
},
template:
'<li class="sidebar-leaf" :data-hide-tree-line="String(hideTreeLine)" :data-thin-tree-line="String(thinTreeLine)">{{ label }}</li>',
},
},
},
}
);
};
describe('SidebarSubGroup', () => {
let localStorageStore;
beforeEach(() => {
localStorageStore = {};
Object.defineProperty(window, 'localStorage', {
value: {
getItem: key => localStorageStore[key] || null,
setItem: (key, value) => {
localStorageStore[key] = String(value);
},
removeItem: key => {
delete localStorageStore[key];
},
clear: () => {
localStorageStore = {};
},
},
configurable: true,
});
window.localStorage.clear();
});
it('keeps collapsible sections expanded by default', () => {
const wrapper = mountSubGroup();
expect(wrapper.find('button').attributes('aria-expanded')).toBe('true');
expect(wrapper.find('.sidebar-leaf').isVisible()).toBe(true);
});
it('renders the tree line on the separator, positioned relative to it', () => {
const wrapper = mountSubGroup({ showTreeLine: true });
const button = wrapper.find('button');
expect(button.classes()).toContain('relative');
expect(button.classes()).toContain('before:bg-n-slate-4');
expect(button.classes()).toContain('before:-bottom-1');
});
it('renders the end curve on the last separator', () => {
const wrapper = mountSubGroup({ showTreeLine: true, endTreeLine: true });
const button = wrapper.find('button');
expect(button.classes()).toContain('before:h-3');
expect(button.classes()).toContain('after:border-b-2');
});
it('draws nested item tree lines via the leaf connectors', () => {
const wrapper = mountSubGroup({ showTreeLine: true });
expect(
wrapper.find('.sidebar-leaf').attributes('data-hide-tree-line')
).toBe('false');
});
it('marks nested item connectors as thin', () => {
const wrapper = mountSubGroup({ showTreeLine: true });
expect(
wrapper.find('.sidebar-leaf').attributes('data-thin-tree-line')
).toBe('true');
});
it('minimizes the section and stores it by account and section name', async () => {
const wrapper = mountSubGroup();
await wrapper.find('button').trigger('click');
const storedSections = JSON.parse(
window.localStorage.getItem(LOCAL_STORAGE_KEYS.SIDEBAR_MINIMIZED_SECTIONS)
);
expect(wrapper.find('button').attributes('aria-expanded')).toBe('false');
expect(wrapper.find('.sidebar-leaf').isVisible()).toBe(false);
expect(storedSections).toEqual({ '1:Conversation:Teams': true });
});
it('uses the stored minimized state when mounted again', () => {
window.localStorage.setItem(
LOCAL_STORAGE_KEYS.SIDEBAR_MINIMIZED_SECTIONS,
JSON.stringify({ '1:Conversation:Teams': true })
);
const wrapper = mountSubGroup();
expect(wrapper.find('button').attributes('aria-expanded')).toBe('false');
expect(wrapper.find('.sidebar-leaf').isVisible()).toBe(false);
});
it('expands a stored minimized section when one of its children is active', () => {
window.localStorage.setItem(
LOCAL_STORAGE_KEYS.SIDEBAR_MINIMIZED_SECTIONS,
JSON.stringify({ '1:Conversation:Teams': true })
);
const wrapper = mountSubGroup({ activeChild: children[0] });
const storedSections = JSON.parse(
window.localStorage.getItem(LOCAL_STORAGE_KEYS.SIDEBAR_MINIMIZED_SECTIONS)
);
expect(wrapper.find('button').attributes('aria-expanded')).toBe('true');
expect(wrapper.find('.sidebar-leaf').isVisible()).toBe(true);
expect(storedSections).toEqual({});
});
});
@@ -7,4 +7,5 @@ export const LOCAL_STORAGE_KEYS = {
DISMISSED_LABEL_SUGGESTIONS: 'labelSuggestionsDismissed',
MESSAGE_REPLY_TO: 'messageReplyTo',
RECENT_SEARCHES: 'recentSearches',
SIDEBAR_MINIMIZED_SECTIONS: 'sidebarMinimizedSections',
};