Merge branch 'develop' into feat/read-only-token

This commit is contained in:
Shivam Mishra
2026-06-11 14:15:39 +05:30
committed by GitHub
16 changed files with 133 additions and 20 deletions
@@ -175,6 +175,9 @@ useEventListener(document, 'touchend', onResizeEnd);
const inboxes = useMapGetter('inboxes/getInboxes');
const labels = useMapGetter('labels/getLabelsOnSidebar');
const allUnreadCount = useMapGetter(
'conversationUnreadCounts/getAllUnreadCount'
);
const getInboxUnreadCount = useMapGetter(
'conversationUnreadCounts/getInboxUnreadCount'
);
@@ -297,6 +300,7 @@ const menuItems = computed(() => {
{
name: 'All',
label: t('SIDEBAR.ALL_CONVERSATIONS'),
badgeCount: allUnreadCount.value,
activeOn: ['inbox_conversation'],
to: accountScopedRoute('home'),
},
@@ -2,15 +2,21 @@ import ConversationAPI from '../../api/conversations';
import types from '../mutation-types';
export const state = {
allCount: 0,
inboxes: {},
labels: {},
teams: {},
};
const normalizeCount = count => {
const parsedCount = Number(count);
return Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 0;
};
const normalizeCounts = counts => {
return Object.entries(counts || {}).reduce((result, [id, count]) => {
const parsedCount = Number(count);
if (Number.isFinite(parsedCount) && parsedCount > 0) {
const parsedCount = normalizeCount(count);
if (parsedCount > 0) {
result[String(id)] = parsedCount;
}
@@ -19,6 +25,9 @@ const normalizeCounts = counts => {
};
export const getters = {
getAllUnreadCount($state) {
return $state.allCount;
},
getInboxUnreadCount: $state => inboxId => {
return $state.inboxes[String(inboxId)] || 0;
},
@@ -55,6 +64,7 @@ export const actions = {
export const mutations = {
[types.SET_CONVERSATION_UNREAD_COUNTS]($state, payload = {}) {
$state.allCount = normalizeCount(payload.all_count);
$state.inboxes = normalizeCounts(payload.inboxes);
$state.labels = normalizeCounts(payload.labels);
$state.teams = normalizeCounts(payload.teams);
@@ -15,6 +15,7 @@ describe('#actions', () => {
describe('#get', () => {
it('commits unread counts when API is successful', async () => {
const payload = {
all_count: 2,
inboxes: { 1: '2' },
labels: { 3: 4 },
teams: { 5: 6 },
@@ -3,6 +3,7 @@ import { getters } from '../../conversationUnreadCounts';
describe('#getters', () => {
it('returns inbox unread count by id', () => {
const state = {
allCount: 0,
inboxes: { 1: 2 },
labels: {},
teams: {},
@@ -15,6 +16,7 @@ describe('#getters', () => {
it('returns label unread count by id', () => {
const state = {
allCount: 0,
inboxes: {},
labels: { 3: 4 },
teams: {},
@@ -27,6 +29,7 @@ describe('#getters', () => {
it('returns team unread count by id', () => {
const state = {
allCount: 0,
inboxes: {},
labels: {},
teams: { 5: 6 },
@@ -37,8 +40,20 @@ describe('#getters', () => {
expect(getters.getTeamUnreadCount(state)(6)).toBe(0);
});
it('returns all unread count', () => {
const state = {
allCount: 7,
inboxes: {},
labels: {},
teams: {},
};
expect(getters.getAllUnreadCount(state)).toBe(7);
});
it('returns unread count maps', () => {
const state = {
allCount: 0,
inboxes: { 1: 2 },
labels: { 3: 4 },
teams: { 5: 6 },
@@ -4,9 +4,10 @@ import { mutations } from '../../conversationUnreadCounts';
describe('#mutations', () => {
describe('#SET_CONVERSATION_UNREAD_COUNTS', () => {
it('normalizes unread count payload', () => {
const state = { inboxes: {}, labels: {}, teams: {} };
const state = { allCount: 0, inboxes: {}, labels: {}, teams: {} };
mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, {
all_count: '3',
inboxes: {
1: '2',
2: 0,
@@ -23,6 +24,7 @@ describe('#mutations', () => {
});
expect(state).toEqual({
allCount: 3,
inboxes: { 1: 2 },
labels: { 4: 5 },
teams: { 6: 7 },
@@ -31,6 +33,7 @@ describe('#mutations', () => {
it('clears counts when payload is empty', () => {
const state = {
allCount: 2,
inboxes: { 1: 2 },
labels: { 4: 5 },
teams: { 6: 7 },
@@ -39,10 +42,21 @@ describe('#mutations', () => {
mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, {});
expect(state).toEqual({
allCount: 0,
inboxes: {},
labels: {},
teams: {},
});
});
it('normalizes invalid aggregate counts to zero', () => {
const state = { allCount: 2, inboxes: {}, labels: {}, teams: {} };
mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, {
all_count: 'invalid',
});
expect(state.allCount).toBe(0);
});
});
});
@@ -36,6 +36,9 @@ class ActionCableConnector extends BaseActionCableConnector {
onReconnect = () => {
this.syncLatestMessages();
// Re-fetch conversation attributes so a status change (e.g. auto-resolve)
// that happened while disconnected is reflected, keeping the reply box state correct.
this.app.$store.dispatch('conversationAttributes/getAttributes');
};
setLastMessageId = () => {
@@ -0,0 +1,53 @@
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
import ActionCableConnector from '../actionCable';
vi.mock('@rails/actioncable', () => ({
createConsumer: () => ({
subscriptions: { create: () => ({}) },
disconnect: vi.fn(),
}),
}));
describe('Widget ActionCableConnector', () => {
let app;
let mockDispatch;
let connector;
beforeEach(() => {
vi.useFakeTimers();
mockDispatch = vi.fn();
app = {
$store: {
dispatch: mockDispatch,
getters: {
getCurrentAccountId: 1,
getCurrentUserID: 1,
},
},
};
connector = new ActionCableConnector(app, 'test-token');
mockDispatch.mockClear();
});
afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});
it('registers the conversation.status_changed event handler', () => {
expect(connector.events['conversation.status_changed']).toBe(
connector.onStatusChange
);
});
it('re-fetches conversation attributes on reconnect so a status change missed while disconnected is reflected', () => {
connector.onReconnect();
expect(mockDispatch).toHaveBeenCalledWith(
'conversation/syncLatestMessages'
);
expect(mockDispatch).toHaveBeenCalledWith(
'conversationAttributes/getAttributes'
);
});
});