Files
chatwoot/app/javascript/dashboard/components-next/sidebar/SidebarSortMenu.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

210 lines
5.1 KiB
Vue

<script setup>
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
import { vOnClickOutside } from '@vueuse/components';
import { useI18n } from 'vue-i18n';
import { useDropdownPosition } from 'dashboard/composables/useDropdownPosition';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
import { SIDEBAR_SORT_KEYS } from 'dashboard/helper/sidebarSort';
const props = defineProps({
activeSort: {
type: String,
default: '',
},
options: {
type: Array,
default: () => [],
},
openOnHover: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['sort', 'toggle']);
const SORT_OPTION_GROUPS = [
{
key: 'created',
options: [SIDEBAR_SORT_KEYS.CREATED_DESC, SIDEBAR_SORT_KEYS.CREATED_ASC],
},
{
key: 'alphabetical',
options: [
SIDEBAR_SORT_KEYS.ALPHABETICAL_ASC,
SIDEBAR_SORT_KEYS.ALPHABETICAL_DESC,
],
},
{
key: 'unread_count',
options: [
SIDEBAR_SORT_KEYS.UNREAD_COUNT_DESC,
SIDEBAR_SORT_KEYS.UNREAD_COUNT_ASC,
],
},
];
const { t } = useI18n();
const isOpen = ref(false);
const triggerRef = ref(null);
const popoverRef = ref(null);
let closeTimer;
const { fixedPosition, updatePosition } = useDropdownPosition(
triggerRef,
popoverRef,
isOpen,
{ align: 'start' }
);
const getSortOptionLabel = option => {
if (option === SIDEBAR_SORT_KEYS.CREATED_DESC) {
return t('SIDEBAR.SORT_OPTIONS.CREATED_DESC');
}
if (option === SIDEBAR_SORT_KEYS.CREATED_ASC) {
return t('SIDEBAR.SORT_OPTIONS.CREATED_ASC');
}
if (option === SIDEBAR_SORT_KEYS.ALPHABETICAL_ASC) {
return t('SIDEBAR.SORT_OPTIONS.ALPHABETICAL_ASC');
}
if (option === SIDEBAR_SORT_KEYS.ALPHABETICAL_DESC) {
return t('SIDEBAR.SORT_OPTIONS.ALPHABETICAL_DESC');
}
if (option === SIDEBAR_SORT_KEYS.UNREAD_COUNT_DESC) {
return t('SIDEBAR.SORT_OPTIONS.UNREAD_COUNT_DESC');
}
if (option === SIDEBAR_SORT_KEYS.UNREAD_COUNT_ASC) {
return t('SIDEBAR.SORT_OPTIONS.UNREAD_COUNT_ASC');
}
return '';
};
const getSortGroupLabel = groupKey => {
if (groupKey === 'created') {
return t('SIDEBAR.SORT_GROUPS.CREATED');
}
if (groupKey === 'alphabetical') {
return t('SIDEBAR.SORT_GROUPS.ALPHABETICAL');
}
if (groupKey === 'unread_count') {
return t('SIDEBAR.SORT_GROUPS.UNREAD_COUNT');
}
return '';
};
const sortMenuSections = computed(() =>
SORT_OPTION_GROUPS.map(group => ({
title: getSortGroupLabel(group.key),
items: group.options
.filter(option => props.options.includes(option))
.map(option => ({
label: getSortOptionLabel(option),
value: option,
action: 'sort',
isActive: option === props.activeSort,
})),
})).filter(section => section.items.length)
);
const clearCloseTimer = () => {
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
};
const openMenu = async () => {
clearCloseTimer();
isOpen.value = true;
emit('toggle', true);
await nextTick();
updatePosition();
};
const closeMenu = () => {
clearCloseTimer();
isOpen.value = false;
emit('toggle', false);
};
const scheduleClose = () => {
clearCloseTimer();
closeTimer = setTimeout(closeMenu, 150);
};
const handleTriggerEnter = () => {
if (props.openOnHover) openMenu();
};
const handleTriggerLeave = () => {
if (props.openOnHover) scheduleClose();
};
const handleClickOutside = event => {
if (triggerRef.value?.contains(event.target)) return;
closeMenu();
};
const handleSortChange = ({ value }) => {
emit('sort', value);
closeMenu();
};
onBeforeUnmount(clearCloseTimer);
</script>
<template>
<div
ref="triggerRef"
class="relative invisible flex-shrink-0 opacity-0 pointer-events-none transition-opacity duration-150 group-hover/sidebar-section:visible group-hover/sidebar-section:opacity-100 group-hover/sidebar-section:pointer-events-auto"
:class="{ '!visible !opacity-100 !pointer-events-auto': isOpen }"
@mouseenter="handleTriggerEnter"
@mouseleave="handleTriggerLeave"
>
<Button
:title="t('SIDEBAR.SORT_TOOLTIP')"
icon="i-lucide-arrow-up-down"
ghost
slate
xs
class="!size-6 !text-n-slate-11 hover:!text-n-slate-12"
:class="{ '!bg-n-alpha-2': isOpen }"
@click.stop="openMenu"
/>
<TeleportWithDirection>
<DropdownMenu
v-if="isOpen"
ref="popoverRef"
v-on-click-outside="handleClickOutside"
data-popover-content
:menu-sections="sortMenuSections"
:class="fixedPosition.class"
:style="fixedPosition.style"
class="w-60 !fixed"
@action="handleSortChange"
@mouseenter="clearCloseTimer"
@mouseleave="handleTriggerLeave"
>
<template #trailing-icon="{ item }">
<span
v-if="item.isActive"
class="i-lucide-check ms-auto size-4 flex-shrink-0 text-n-slate-11"
/>
</template>
</DropdownMenu>
</TeleportWithDirection>
</div>
</template>