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/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/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/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/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"
>