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>
This commit is contained in:
Sony Mathew
2026-06-15 21:34:19 +05:30
committed by GitHub
co-authored by Sivin Varghese iamsivin
parent 4816e923b6
commit d8a16278b9
16 changed files with 1071 additions and 87 deletions
@@ -0,0 +1,82 @@
import { LocalStorage } from 'shared/helpers/localStorage';
import {
DEFAULT_SIDEBAR_SORT_PREFERENCES,
isValidSidebarSort,
normalizeSidebarSortPreferences,
} from 'dashboard/helper/sidebarSort';
const STORAGE_NAME = 'chatwoot_sidebar_sort_preferences';
export const SET_SIDEBAR_SORT_PREFERENCES = 'SET_SIDEBAR_SORT_PREFERENCES';
const getPreferenceScope = rootGetters => {
const currentUserId = rootGetters.getCurrentUserID;
const currentAccountId = rootGetters.getCurrentAccountId;
if (!currentUserId || !currentAccountId) return null;
return `${currentUserId}:${currentAccountId}`;
};
export const state = {
preferences: { ...DEFAULT_SIDEBAR_SORT_PREFERENCES },
storageKey: null,
};
export const getters = {
getSectionSort: $state => section => {
return (
$state.preferences[section] || DEFAULT_SIDEBAR_SORT_PREFERENCES[section]
);
},
};
export const actions = {
initialize({ commit, rootGetters }) {
const storageKey = getPreferenceScope(rootGetters);
const storedPreferences = storageKey
? LocalStorage.getFromJsonStore(STORAGE_NAME, storageKey)
: {};
commit(SET_SIDEBAR_SORT_PREFERENCES, {
preferences: normalizeSidebarSortPreferences(storedPreferences),
storageKey,
});
},
setSectionSort({ commit, rootGetters, state: currentState }, payload = {}) {
const { section, sortBy } = payload;
if (!isValidSidebarSort(section, sortBy)) return;
const storageKey =
currentState.storageKey || getPreferenceScope(rootGetters);
const preferences = {
...currentState.preferences,
[section]: sortBy,
};
commit(SET_SIDEBAR_SORT_PREFERENCES, {
preferences,
storageKey,
});
if (storageKey) {
LocalStorage.updateJsonStore(STORAGE_NAME, storageKey, preferences);
}
},
};
export const mutations = {
[SET_SIDEBAR_SORT_PREFERENCES]($state, payload = {}) {
const { preferences = {}, storageKey = null } = payload;
$state.preferences = normalizeSidebarSortPreferences(preferences);
$state.storageKey = storageKey;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
@@ -0,0 +1,106 @@
import { LocalStorage } from 'shared/helpers/localStorage';
import {
DEFAULT_SIDEBAR_SORT_PREFERENCES,
SIDEBAR_SORT_KEYS,
SIDEBAR_SORT_SECTIONS,
} from 'dashboard/helper/sidebarSort';
import {
SET_SIDEBAR_SORT_PREFERENCES,
actions,
} from '../../sidebarSortPreferences';
vi.mock('shared/helpers/localStorage', () => ({
LocalStorage: {
getFromJsonStore: vi.fn(),
updateJsonStore: vi.fn(),
},
}));
const rootGetters = {
getCurrentUserID: 1,
getCurrentAccountId: 2,
};
describe('#actions', () => {
const commit = vi.fn();
beforeEach(() => {
commit.mockClear();
LocalStorage.getFromJsonStore.mockReset();
LocalStorage.updateJsonStore.mockReset();
});
describe('#initialize', () => {
it('loads scoped preferences from local storage', () => {
LocalStorage.getFromJsonStore.mockReturnValue({
[SIDEBAR_SORT_SECTIONS.FOLDERS]: SIDEBAR_SORT_KEYS.ALPHABETICAL_ASC,
});
actions.initialize({ commit, rootGetters });
expect(LocalStorage.getFromJsonStore).toHaveBeenCalledWith(
'chatwoot_sidebar_sort_preferences',
'1:2'
);
expect(commit).toHaveBeenCalledWith(SET_SIDEBAR_SORT_PREFERENCES, {
preferences: {
...DEFAULT_SIDEBAR_SORT_PREFERENCES,
[SIDEBAR_SORT_SECTIONS.FOLDERS]: SIDEBAR_SORT_KEYS.ALPHABETICAL_ASC,
},
storageKey: '1:2',
});
});
});
describe('#setSectionSort', () => {
it('persists valid preferences to local storage', () => {
const state = {
preferences: DEFAULT_SIDEBAR_SORT_PREFERENCES,
storageKey: '1:2',
};
actions.setSectionSort(
{ commit, rootGetters, state },
{
section: SIDEBAR_SORT_SECTIONS.LABELS,
sortBy: SIDEBAR_SORT_KEYS.ALPHABETICAL_DESC,
}
);
const preferences = {
...DEFAULT_SIDEBAR_SORT_PREFERENCES,
[SIDEBAR_SORT_SECTIONS.LABELS]: SIDEBAR_SORT_KEYS.ALPHABETICAL_DESC,
};
expect(commit).toHaveBeenCalledWith(SET_SIDEBAR_SORT_PREFERENCES, {
preferences,
storageKey: '1:2',
});
expect(LocalStorage.updateJsonStore).toHaveBeenCalledWith(
'chatwoot_sidebar_sort_preferences',
'1:2',
preferences
);
});
it('ignores invalid preferences', () => {
actions.setSectionSort(
{
commit,
rootGetters,
state: {
preferences: DEFAULT_SIDEBAR_SORT_PREFERENCES,
storageKey: '1:2',
},
},
{
section: SIDEBAR_SORT_SECTIONS.FOLDERS,
sortBy: SIDEBAR_SORT_KEYS.UNREAD_COUNT_DESC,
}
);
expect(commit).not.toHaveBeenCalled();
expect(LocalStorage.updateJsonStore).not.toHaveBeenCalled();
});
});
});
@@ -0,0 +1,31 @@
import {
DEFAULT_SIDEBAR_SORT_PREFERENCES,
SIDEBAR_SORT_KEYS,
SIDEBAR_SORT_SECTIONS,
} from 'dashboard/helper/sidebarSort';
import { getters } from '../../sidebarSortPreferences';
describe('#getters', () => {
it('returns section sort preference', () => {
const state = {
preferences: {
...DEFAULT_SIDEBAR_SORT_PREFERENCES,
[SIDEBAR_SORT_SECTIONS.TEAMS]: SIDEBAR_SORT_KEYS.CREATED_ASC,
},
};
expect(getters.getSectionSort(state)(SIDEBAR_SORT_SECTIONS.TEAMS)).toBe(
SIDEBAR_SORT_KEYS.CREATED_ASC
);
});
it('falls back to default section sort preference', () => {
const state = {
preferences: {},
};
expect(getters.getSectionSort(state)(SIDEBAR_SORT_SECTIONS.LABELS)).toBe(
SIDEBAR_SORT_KEYS.UNREAD_COUNT_DESC
);
});
});
@@ -0,0 +1,34 @@
import {
DEFAULT_SIDEBAR_SORT_PREFERENCES,
SIDEBAR_SORT_KEYS,
SIDEBAR_SORT_SECTIONS,
} from 'dashboard/helper/sidebarSort';
import {
SET_SIDEBAR_SORT_PREFERENCES,
mutations,
} from '../../sidebarSortPreferences';
describe('#mutations', () => {
it('sets normalized preferences', () => {
const state = {
preferences: {},
storageKey: null,
};
mutations[SET_SIDEBAR_SORT_PREFERENCES](state, {
preferences: {
[SIDEBAR_SORT_SECTIONS.FOLDERS]: SIDEBAR_SORT_KEYS.ALPHABETICAL_DESC,
[SIDEBAR_SORT_SECTIONS.LABELS]: 'invalid',
},
storageKey: '1:2',
});
expect(state).toEqual({
preferences: {
...DEFAULT_SIDEBAR_SORT_PREFERENCES,
[SIDEBAR_SORT_SECTIONS.FOLDERS]: SIDEBAR_SORT_KEYS.ALPHABETICAL_DESC,
},
storageKey: '1:2',
});
});
});