Files
chatwoot/app/javascript/dashboard/components-next/sidebar/SidebarSubGroup.vue
T
d8a16278b9 fix: added ordering capability for sidebar sections folders, teams, channels and labels (CW-7193) (#14609)
## Description

* Added the ability to sort for 4 sub-sections under conversations
folders, teams, channels and labels.
* the sort options are basically created at, alphabetical and unread
counts along with both directions.
* for folders we don't have an unread count, so we sort it by only
created and alphabetical.
* all the sort preferences are stored on the frontend - easiest
implementation for now.

Fixes # CW-7193

## 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 this locally by visually verifying the changes. Also ran the
newly added tests for component level changes.

Here is the screenshot of the changes:

Added the sort option to sub sections in the conversation sidebar:
<img width="282" height="848" alt="Screenshot 2026-06-02 at 1 58 12 AM"
src="https://github.com/user-attachments/assets/4a7c6061-86e3-438a-92ae-ee643a0128b6"
/>

The sort options looks like this:
<img width="783" height="698" alt="Screenshot 2026-06-02 at 1 58 49 AM"
src="https://github.com/user-attachments/assets/a15bb0a7-b810-4423-a88c-fbd84d0476c0"
/>


## 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>
2026-06-15 21:34:19 +05:30

183 lines
5.5 KiB
Vue

<script setup>
import { computed, ref, watch } from 'vue';
import { useEventListener } from '@vueuse/core';
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';
import { useSidebarContext } from './provider';
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 },
sortOptions: { type: Array, default: () => [] },
activeSort: { type: String, default: '' },
collapsible: { type: Boolean, default: false },
showTreeLine: { type: Boolean, default: false },
endTreeLine: { type: Boolean, default: false },
});
const emit = defineEmits(['update-sort']);
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 => {
return child.to && isAllowed(child.to);
})
);
const hasAccessibleItems = computed(() => {
return accessibleItems.value.length > 0;
});
const isScrollable = computed(() => {
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>
<li class="group/sidebar-section 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"
:sort-options="sortOptions"
:active-sort="activeSort"
class="my-1"
@toggle="toggleSubGroup"
@update-sort="sortBy => emit('update-sort', sortBy)"
/>
<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,
]"
>
<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>