From 274e92e0e421fe4afd193ba1e1d7b68e6faa0c5f Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Sun, 14 Jun 2026 06:54:18 +0530 Subject: [PATCH] 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 --- .../components-next/sidebar/Sidebar.vue | 16 ++ .../components-next/sidebar/SidebarGroup.vue | 95 +++------ .../sidebar/SidebarGroupLeaf.vue | 17 +- .../sidebar/SidebarGroupSeparator.vue | 61 +++++- .../sidebar/SidebarSubGroup.vue | 198 ++++++++++++------ .../sidebar/specs/SidebarSubGroup.spec.js | 172 +++++++++++++++ .../dashboard/constants/localStorage.js | 1 + theme/icons.js | 5 + 8 files changed, 423 insertions(+), 142 deletions(-) create mode 100644 app/javascript/dashboard/components-next/sidebar/specs/SidebarSubGroup.spec.js diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index f71652ca0..ab037618c 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -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, diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue index 048a99cf8..f618ad9ba 100644 --- a/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue +++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue @@ -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(