diff --git a/app/javascript/dashboard/store/captain/copilotMessages.js b/app/javascript/dashboard/store/captain/copilotMessages.js
index 83b7fddce..2b296cdc1 100644
--- a/app/javascript/dashboard/store/captain/copilotMessages.js
+++ b/app/javascript/dashboard/store/captain/copilotMessages.js
@@ -6,9 +6,9 @@ export default createStore({
API: CopilotMessagesAPI,
getters: {
getMessagesByThreadId: state => copilotThreadId => {
- return state.records.filter(
- record => record.copilot_thread?.id === Number(copilotThreadId)
- );
+ return state.records
+ .filter(record => record.copilot_thread?.id === Number(copilotThreadId))
+ .sort((a, b) => a.id - b.id);
},
},
actions: mutationTypes => ({
diff --git a/app/javascript/dashboard/store/modules/accounts.js b/app/javascript/dashboard/store/modules/accounts.js
index 662853720..0d5fdc748 100644
--- a/app/javascript/dashboard/store/modules/accounts.js
+++ b/app/javascript/dashboard/store/modules/accounts.js
@@ -63,8 +63,11 @@ export const actions = {
});
}
},
- update: async ({ commit }, updateObj) => {
- commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
+ update: async ({ commit }, { options, ...updateObj }) => {
+ if (options?.silent !== true) {
+ commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
+ }
+
try {
const response = await AccountAPI.update('', updateObj);
commit(types.default.EDIT_ACCOUNT, response.data);
diff --git a/app/javascript/dashboard/store/modules/contacts/actions.js b/app/javascript/dashboard/store/modules/contacts/actions.js
index e8da81482..02911d9d9 100644
--- a/app/javascript/dashboard/store/modules/contacts/actions.js
+++ b/app/javascript/dashboard/store/modules/contacts/actions.js
@@ -75,6 +75,21 @@ export const actions = {
}
},
+ active: async ({ commit }, { page = 1, sortAttr } = {}) => {
+ commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
+ try {
+ const {
+ data: { payload, meta },
+ } = await ContactAPI.active(page, sortAttr);
+ commit(types.CLEAR_CONTACTS);
+ commit(types.SET_CONTACTS, payload);
+ commit(types.SET_CONTACT_META, meta);
+ commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
+ } catch (error) {
+ commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
+ }
+ },
+
show: async ({ commit }, { id }) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetchingItem: true });
try {
diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js
index 8296f25de..d8ad826ca 100644
--- a/app/javascript/dashboard/store/modules/conversations/actions.js
+++ b/app/javascript/dashboard/store/modules/conversations/actions.js
@@ -327,6 +327,16 @@ const actions = {
}
},
+ deleteConversation: async ({ commit, dispatch }, conversationId) => {
+ try {
+ await ConversationApi.delete(conversationId);
+ commit(types.DELETE_CONVERSATION, conversationId);
+ dispatch('conversationStats/get', {}, { root: true });
+ } catch (error) {
+ throw new Error(error);
+ }
+ },
+
addConversation({ commit, state, dispatch, rootState }, conversation) {
const { currentInbox, appliedFilters } = state;
const {
diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js
index f5b83e546..9f5744fbb 100644
--- a/app/javascript/dashboard/store/modules/conversations/getters.js
+++ b/app/javascript/dashboard/store/modules/conversations/getters.js
@@ -18,13 +18,34 @@ const getters = {
getAllConversations: ({ allConversations, chatSortFilter: sortKey }) => {
return allConversations.sort((a, b) => sortComparator(a, b, sortKey));
},
- getFilteredConversations: ({
- allConversations,
- chatSortFilter,
- appliedFilters,
- }) => {
+ getFilteredConversations: (
+ { allConversations, chatSortFilter, appliedFilters },
+ _,
+ __,
+ rootGetters
+ ) => {
+ const currentUser = rootGetters.getCurrentUser;
+ const currentUserId = rootGetters.getCurrentUser.id;
+ const currentAccountId = rootGetters.getCurrentAccountId;
+
+ const permissions = getUserPermissions(currentUser, currentAccountId);
+ const userRole = getUserRole(currentUser, currentAccountId);
+
return allConversations
- .filter(conversation => matchesFilters(conversation, appliedFilters))
+ .filter(conversation => {
+ const matchesFilterResult = matchesFilters(
+ conversation,
+ appliedFilters
+ );
+ const allowedForRole = applyRoleFilter(
+ conversation,
+ userRole,
+ permissions,
+ currentUserId
+ );
+
+ return matchesFilterResult && allowedForRole;
+ })
.sort((a, b) => sortComparator(a, b, chatSortFilter));
},
getSelectedChat: ({ selectedChatId, allConversations }) => {
diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js b/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js
index 2591c2c01..d64d3c56b 100644
--- a/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js
+++ b/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js
@@ -47,6 +47,7 @@
* 3. Nested properties in custom_attributes (conversation_type, etc.)
*/
import jsonLogic from 'json-logic-js';
+import { coerceToDate } from '@chatwoot/utils';
/**
* Gets a value from a conversation based on the attribute key
@@ -157,6 +158,20 @@ const contains = (filterValue, conversationValue) => {
return false;
};
+/**
+ * Compares two date values using a comparison function
+ * @param {*} conversationValue - The conversation value to compare
+ * @param {*} filterValue - The filter value to compare against
+ * @param {Function} compareFn - The comparison function to apply
+ * @returns {Boolean} - Returns true if the comparison succeeds, false otherwise
+ */
+const compareDates = (conversationValue, filterValue, compareFn) => {
+ const conversationDate = coerceToDate(conversationValue);
+ const filterDate = coerceToDate(filterValue);
+ if (conversationDate === null || filterDate === null) return false;
+ return compareFn(conversationDate, filterDate);
+};
+
/**
* Checks if a value matches a filter condition
* @param {*} conversationValue - The value to check
@@ -195,10 +210,10 @@ const matchesCondition = (conversationValue, filter) => {
return false; // We already handled null/undefined above
case 'is_greater_than':
- return new Date(conversationValue) > new Date(filterValue);
+ return compareDates(conversationValue, filterValue, (a, b) => a > b);
case 'is_less_than':
- return new Date(conversationValue) < new Date(filterValue);
+ return compareDates(conversationValue, filterValue, (a, b) => a < b);
case 'days_before': {
const today = new Date();
@@ -347,6 +362,7 @@ export const matchesFilters = (conversation, filters) => {
conversation,
filters[0].attribute_key
);
+
return matchesCondition(value, filters[0]);
}
diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js b/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js
index 6d709f085..0c9e6a5c0 100644
--- a/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js
+++ b/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js
@@ -463,6 +463,241 @@ describe('filterHelpers', () => {
expect(matchesFilters(conversation, filters)).toBe(true);
});
+ // Test conversation with 10-digit timestamp (seconds) vs standard date filter
+ it('should match conversation with 10-digit timestamp against date string filter', () => {
+ const conversation = { created_at: 1647777600 }; // March 20, 2022 in seconds (10 digits)
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19', // Standard YYYY-MM-DD format
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test conversation with 13-digit timestamp (milliseconds) vs standard date filter
+ it('should match conversation with 13-digit timestamp against date string filter', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022 in milliseconds (13 digits)
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19', // Standard YYYY-MM-DD format
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test conversation with string timestamp vs standard date filter
+ it('should match conversation with string 10-digit timestamp against date string filter', () => {
+ const conversation = { created_at: '1647777600' }; // March 20, 2022 as string (10 digits)
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19', // Standard YYYY-MM-DD format
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test conversation with string 13-digit timestamp vs standard date filter
+ it('should match conversation with string 13-digit timestamp against date string filter', () => {
+ const conversation = { created_at: '1647777600000' }; // March 20, 2022 as string (13 digits)
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19', // Standard YYYY-MM-DD format
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test conversation with mixed format vs standard date filter with time
+ it('should match conversation with numeric timestamp against ISO date string filter', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022 12:00:00 GMT (numeric)
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19T10:30:00Z', // Standard ISO format from filter
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test parseDate with date string without time (should default to 00:00:00)
+ it('should match conversation with is_greater_than operator using date string without time', () => {
+ const conversation = { created_at: 1647820800000 }; // March 21, 2022 00:00:00 GMT
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-20', // March 20, 2022 (should become 00:00:00)
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test parseDate with ISO date string
+ it('should match conversation with is_greater_than operator using ISO date string', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19T00:00:00.000Z', // March 19, 2022 ISO format
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test parseDate with null/undefined values
+ it('should handle null filter values in date comparison', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: null,
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(false);
+ });
+
+ it('should handle undefined filter values in date comparison', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: undefined,
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(false);
+ });
+
+ // Test parseDate with invalid date strings
+ it('should handle invalid date strings in date comparison', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: 'invalid-date-string',
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(false);
+ });
+
+ it('should handle non-date string values in date comparison', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: 'not-a-date',
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(false);
+ });
+
+ // Test is_less_than with various date formats
+ it('should match conversation with is_less_than operator using numeric timestamp', () => {
+ const conversation = { created_at: 1647691200000 }; // March 19, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_less_than',
+ values: 1647777600, // March 20, 2022 as 10-digit timestamp
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ it('should not match conversation with is_less_than operator when date is later', () => {
+ const conversation = { created_at: 1647864000000 }; // March 21, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_less_than',
+ values: '2022-03-20T12:00:00Z', // March 20, 2022 with time
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(false);
+ });
+
+ // Edge case: Test with conversation having string timestamp
+ it('should handle conversation with string timestamp value', () => {
+ const conversation = { created_at: '1647777600000' }; // March 20, 2022 as string
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19',
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Edge case: Test with conversation having 10-digit timestamp
+ it('should handle conversation with 10-digit timestamp value', () => {
+ const conversation = { created_at: 1647777600 }; // March 20, 2022 as seconds (10 digits)
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19',
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test date string with different time formats
+ it('should handle date string with space-separated time', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: '2022-03-19 10:30:00', // Date with space-separated time
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ // Test parseDate with object input (should return null and fail comparison)
+ it('should handle non-string, non-number filter values', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: { date: '2022-03-19' }, // Object instead of string/number
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(false);
+ });
+
describe('days_before operator', () => {
beforeEach(() => {
// Set the date to March 25, 2022
diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js
index fc6f10979..2ee6fd061 100644
--- a/app/javascript/dashboard/store/modules/conversations/index.js
+++ b/app/javascript/dashboard/store/modules/conversations/index.js
@@ -204,6 +204,12 @@ export const mutations = {
_state.allConversations.push(conversation);
},
+ [types.DELETE_CONVERSATION](_state, conversationId) {
+ _state.allConversations = _state.allConversations.filter(
+ c => c.id !== conversationId
+ );
+ },
+
[types.UPDATE_CONVERSATION](_state, conversation) {
const { allConversations } = _state;
const index = allConversations.findIndex(c => c.id === conversation.id);
diff --git a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js
index ec75ec968..79852a42e 100644
--- a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js
@@ -70,6 +70,30 @@ describe('#actions', () => {
});
});
+ describe('#active', () => {
+ it('sends correct mutations if API is success', async () => {
+ axios.get.mockResolvedValue({
+ data: { payload: contactList, meta: { count: 100, current_page: 1 } },
+ });
+ await actions.active({ commit });
+ expect(commit.mock.calls).toEqual([
+ [types.SET_CONTACT_UI_FLAG, { isFetching: true }],
+ [types.CLEAR_CONTACTS],
+ [types.SET_CONTACTS, contactList],
+ [types.SET_CONTACT_META, { count: 100, current_page: 1 }],
+ [types.SET_CONTACT_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ it('sends correct mutations if API is error', async () => {
+ axios.get.mockRejectedValue({ message: 'Incorrect header' });
+ await actions.active({ commit });
+ expect(commit.mock.calls).toEqual([
+ [types.SET_CONTACT_UI_FLAG, { isFetching: true }],
+ [types.SET_CONTACT_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ });
+
describe('#update', () => {
it('sends correct mutations if API is success', async () => {
axios.patch.mockResolvedValue({ data: { payload: contactList[0] } });
diff --git a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js
index dc2bf8d3a..161ae914b 100644
--- a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js
@@ -513,6 +513,28 @@ describe('#deleteMessage', () => {
expect(commit.mock.calls).toEqual([]);
});
+ describe('#deleteConversation', () => {
+ it('send correct actions if API is success', async () => {
+ axios.delete.mockResolvedValue({
+ data: { id: 1 },
+ });
+ await actions.deleteConversation({ commit, dispatch }, 1);
+ expect(commit.mock.calls).toEqual([[types.DELETE_CONVERSATION, 1]]);
+ expect(dispatch.mock.calls).toEqual([
+ ['conversationStats/get', {}, { root: true }],
+ ]);
+ });
+
+ it('send no actions if API is error', async () => {
+ axios.delete.mockRejectedValue({ message: 'Incorrect header' });
+ await expect(
+ actions.deleteConversation({ commit, dispatch }, 1)
+ ).rejects.toThrow(Error);
+ expect(commit.mock.calls).toEqual([]);
+ expect(dispatch.mock.calls).toEqual([]);
+ });
+ });
+
describe('#updateCustomAttributes', () => {
it('update conversation custom attributes', async () => {
axios.post.mockResolvedValue({
diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
index 8ac89f49a..7b6c38456 100644
--- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
@@ -325,4 +325,308 @@ describe('#getters', () => {
});
});
});
+
+ describe('#getFilteredConversations', () => {
+ const mockConversations = [
+ {
+ id: 1,
+ status: 'open',
+ meta: { assignee: { id: 1 } },
+ last_activity_at: 1000,
+ },
+ {
+ id: 2,
+ status: 'open',
+ meta: {},
+ last_activity_at: 2000,
+ },
+ {
+ id: 3,
+ status: 'resolved',
+ meta: { assignee: { id: 2 } },
+ last_activity_at: 3000,
+ },
+ ];
+
+ const mockRootGetters = {
+ getCurrentUser: {
+ id: 1,
+ accounts: [{ id: 1, role: 'agent', permissions: [] }],
+ },
+ getCurrentAccountId: 1,
+ };
+
+ it('filters conversations based on role permissions for administrator', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_desc',
+ appliedFilters: [],
+ };
+
+ const rootGetters = {
+ ...mockRootGetters,
+ getCurrentUser: {
+ ...mockRootGetters.getCurrentUser,
+ accounts: [{ id: 1, role: 'administrator', permissions: [] }],
+ },
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ rootGetters
+ );
+
+ expect(result).toEqual([
+ mockConversations[2],
+ mockConversations[1],
+ mockConversations[0],
+ ]);
+ });
+
+ it('filters conversations based on role permissions for agent', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_desc',
+ appliedFilters: [],
+ };
+
+ const rootGetters = {
+ ...mockRootGetters,
+ getCurrentUser: {
+ ...mockRootGetters.getCurrentUser,
+ accounts: [{ id: 1, role: 'agent', permissions: [] }],
+ },
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ rootGetters
+ );
+
+ expect(result).toEqual([
+ mockConversations[2],
+ mockConversations[1],
+ mockConversations[0],
+ ]);
+ });
+
+ it('filters conversations for custom role with conversation_manage permission', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_desc',
+ appliedFilters: [],
+ };
+
+ const rootGetters = {
+ ...mockRootGetters,
+ getCurrentUser: {
+ ...mockRootGetters.getCurrentUser,
+ accounts: [
+ {
+ id: 1,
+ custom_role_id: 5,
+ permissions: ['conversation_manage'],
+ },
+ ],
+ },
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ rootGetters
+ );
+
+ expect(result).toEqual([
+ mockConversations[2],
+ mockConversations[1],
+ mockConversations[0],
+ ]);
+ });
+
+ it('filters conversations for custom role with conversation_unassigned_manage permission', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_desc',
+ appliedFilters: [],
+ };
+
+ const rootGetters = {
+ ...mockRootGetters,
+ getCurrentUser: {
+ ...mockRootGetters.getCurrentUser,
+ accounts: [
+ {
+ id: 1,
+ custom_role_id: 5,
+ permissions: ['conversation_unassigned_manage'],
+ },
+ ],
+ },
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ rootGetters
+ );
+
+ // Should include conversation assigned to user (id: 1) and unassigned conversation
+ expect(result).toEqual([mockConversations[1], mockConversations[0]]);
+ });
+
+ it('filters conversations for custom role with conversation_participating_manage permission', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_desc',
+ appliedFilters: [],
+ };
+
+ const rootGetters = {
+ ...mockRootGetters,
+ getCurrentUser: {
+ ...mockRootGetters.getCurrentUser,
+ accounts: [
+ {
+ id: 1,
+ custom_role_id: 5,
+ permissions: ['conversation_participating_manage'],
+ },
+ ],
+ },
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ rootGetters
+ );
+
+ // Should only include conversation assigned to user (id: 1)
+ expect(result).toEqual([mockConversations[0]]);
+ });
+
+ it('filters conversations for custom role with no permissions', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_desc',
+ appliedFilters: [],
+ };
+
+ const rootGetters = {
+ ...mockRootGetters,
+ getCurrentUser: {
+ ...mockRootGetters.getCurrentUser,
+ accounts: [
+ {
+ id: 1,
+ custom_role_id: 5,
+ permissions: [],
+ },
+ ],
+ },
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ rootGetters
+ );
+
+ // Should return empty array as user has no permissions
+ expect(result).toEqual([]);
+ });
+
+ it('applies filters and role permissions together', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_desc',
+ appliedFilters: [
+ {
+ attribute_key: 'status',
+ filter_operator: 'equal_to',
+ values: ['open'],
+ query_operator: 'and',
+ },
+ ],
+ };
+
+ const rootGetters = {
+ ...mockRootGetters,
+ getCurrentUser: {
+ ...mockRootGetters.getCurrentUser,
+ accounts: [
+ {
+ id: 1,
+ custom_role_id: 5,
+ permissions: ['conversation_participating_manage'],
+ },
+ ],
+ },
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ rootGetters
+ );
+
+ // Should only include open conversation assigned to user (id: 1)
+ expect(result).toEqual([mockConversations[0]]);
+ });
+
+ it('returns empty array when no conversations match filters', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_desc',
+ appliedFilters: [
+ {
+ attribute_key: 'status',
+ filter_operator: 'equal_to',
+ values: ['pending'],
+ query_operator: 'and',
+ },
+ ],
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ mockRootGetters
+ );
+
+ expect(result).toEqual([]);
+ });
+
+ it('sorts filtered conversations according to chatSortFilter', () => {
+ const state = {
+ allConversations: mockConversations,
+ chatSortFilter: 'last_activity_at_asc',
+ appliedFilters: [],
+ };
+
+ const result = getters.getFilteredConversations(
+ state,
+ {},
+ {},
+ mockRootGetters
+ );
+
+ expect(result).toEqual([
+ mockConversations[0],
+ mockConversations[1],
+ mockConversations[2],
+ ]);
+ });
+ });
});
diff --git a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
index 091dc3daa..b8660b20d 100644
--- a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
@@ -884,6 +884,17 @@ describe('#mutations', () => {
});
});
+ describe('#DELETE_CONVERSATION', () => {
+ it('should delete a conversation', () => {
+ const state = {
+ allConversations: [{ id: 1, messages: [] }],
+ };
+
+ mutations[types.DELETE_CONVERSATION](state, 1);
+ expect(state.allConversations).toEqual([]);
+ });
+ });
+
describe('#SET_LIST_LOADING_STATUS', () => {
it('should set listLoadingStatus to true', () => {
const state = {
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index f3817c45a..a63fec2d1 100644
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -56,6 +56,7 @@ export default {
SET_ALL_ATTACHMENTS: 'SET_ALL_ATTACHMENTS',
ADD_CONVERSATION_ATTACHMENTS: 'ADD_CONVERSATION_ATTACHMENTS',
DELETE_CONVERSATION_ATTACHMENTS: 'DELETE_CONVERSATION_ATTACHMENTS',
+ DELETE_CONVERSATION: 'DELETE_CONVERSATION',
SET_CONVERSATION_CAN_REPLY: 'SET_CONVERSATION_CAN_REPLY',
diff --git a/app/javascript/v3/components/Form/Select.vue b/app/javascript/v3/components/Form/Select.vue
index 0fd9ad002..f91c1ebe2 100644
--- a/app/javascript/v3/components/Form/Select.vue
+++ b/app/javascript/v3/components/Form/Select.vue
@@ -64,11 +64,11 @@ export default {
:selected="modelValue"
:name="name"
:class="{
- 'text-ash-400': !modelValue,
- 'text-ash-900': modelValue,
+ 'text-n-slate-9': !modelValue,
+ 'text-n-slate-12': modelValue,
'pl-9': icon,
}"
- class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm outline-none appearance-none rounded-xl select-caret ring-ash-200 ring-1 ring-inset placeholder:text-ash-900 focus:ring-2 focus:ring-inset focus:ring-primary-500 text-sm leading-6"
+ class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm appearance-none rounded-xl select-caret leading-6"
@input="onInput"
>