diff --git a/app/builders/messages/instagram/base_message_builder.rb b/app/builders/messages/instagram/base_message_builder.rb
index b8f892d97..767115bc6 100644
--- a/app/builders/messages/instagram/base_message_builder.rb
+++ b/app/builders/messages/instagram/base_message_builder.rb
@@ -94,11 +94,10 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
def build_message
# Duplicate webhook events may be sent for the same message
# when a user is connected to the Instagram account through both Messenger and Instagram login.
+ # There is chance for echo events to be sent for the same message.
# Therefore, we need to check if the message already exists before creating it.
return if message_already_exists?
- return if @outgoing_echo
-
return if message_content.blank? && all_unsupported_files?
@message = conversation.messages.create!(message_params)
diff --git a/app/javascript/dashboard/assets/scss/_next-colors.scss b/app/javascript/dashboard/assets/scss/_next-colors.scss
index 48cfce921..f23c01d42 100644
--- a/app/javascript/dashboard/assets/scss/_next-colors.scss
+++ b/app/javascript/dashboard/assets/scss/_next-colors.scss
@@ -29,6 +29,19 @@
--iris-11: 87 83 198;
--iris-12: 39 41 98;
+ --blue-1: 251 253 255;
+ --blue-2: 245 249 255;
+ --blue-3: 233 243 255;
+ --blue-4: 218 236 255;
+ --blue-5: 201 226 255;
+ --blue-6: 181 213 255;
+ --blue-7: 155 195 252;
+ --blue-8: 117 171 247;
+ --blue-9: 39 129 246;
+ --blue-10: 16 115 233;
+ --blue-11: 8 109 224;
+ --blue-12: 11 50 101;
+
--ruby-1: 255 252 253;
--ruby-2: 255 247 248;
--ruby-3: 254 234 237;
@@ -131,6 +144,19 @@
--iris-11: 158 177 255;
--iris-12: 224 223 254;
+ --blue-1: 10 17 28;
+ --blue-2: 15 24 38;
+ --blue-3: 15 39 72;
+ --blue-4: 10 49 99;
+ --blue-5: 18 61 117;
+ --blue-6: 29 84 134;
+ --blue-7: 40 89 156;
+ --blue-8: 48 106 186;
+ --blue-9: 39 129 246;
+ --blue-10: 21 116 231;
+ --blue-11: 126 182 255;
+ --blue-12: 205 227 255;
+
--ruby-1: 25 17 19;
--ruby-2: 30 21 23;
--ruby-3: 58 20 30;
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
index 14d944b3e..75400692e 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue
@@ -51,6 +51,7 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
index b263b882d..7125d8385 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactSortMenu.vue
@@ -31,10 +31,6 @@ const sortMenus = [
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.EMAIL'),
value: 'email',
},
- {
- label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.PHONE_NUMBER'),
- value: 'phone_number',
- },
{
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.COMPANY'),
value: 'company_name',
diff --git a/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue b/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
index ce9330076..bbb31d72c 100644
--- a/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
+++ b/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
@@ -218,14 +218,14 @@ const emitDateRange = () => {
/>
@@ -251,12 +251,12 @@ const emitDateRange = () => {
@validate="updateManualInput($event, calendar)"
@error="handleManualInputError($event)"
/>
-
+
import { CALENDAR_PERIODS } from '../helpers/DatePickerHelper';
+import NextButton from 'dashboard/components-next/button/Button.vue';
+
defineProps({
calendarType: {
type: String,
@@ -38,42 +40,38 @@ const onClickSetView = (type, mode) => {
-
+ />
-
+ />
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateInput.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateInput.vue
index b9fe88993..715bc8cb3 100644
--- a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateInput.vue
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarDateInput.vue
@@ -65,7 +65,7 @@ const validateDate = () => {
{
{{ $t('DATE_PICKER.DATE_RANGE_OPTIONS.TITLE') }}
@@ -26,11 +26,11 @@ const setDateRange = range => {
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarYear.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarYear.vue
index 725925f07..13a786e27 100644
--- a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarYear.vue
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarYear.vue
@@ -72,10 +72,10 @@ const selectYear = year => {
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/DatePickerButton.vue b/app/javascript/dashboard/components/ui/DatePicker/components/DatePickerButton.vue
index 9a1561eb5..d1fa42630 100644
--- a/app/javascript/dashboard/components/ui/DatePicker/components/DatePickerButton.vue
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/DatePickerButton.vue
@@ -48,7 +48,7 @@ const openDatePicker = () => {
{
trigger: '@',
showMenu: showUserMentions,
searchTerm: mentionSearchKey,
+ isAllowed: () => props.isPrivate,
}),
createSuggestionPlugin({
trigger: '/',
diff --git a/app/javascript/dashboard/components/widgets/conversation/TagAgents.vue b/app/javascript/dashboard/components/widgets/conversation/TagAgents.vue
index 793275e3d..2e055cae6 100644
--- a/app/javascript/dashboard/components/widgets/conversation/TagAgents.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/TagAgents.vue
@@ -69,10 +69,6 @@ const onAgentSelect = index => {
v-if="items.length"
ref="tagAgentsRef"
class="vertical dropdown menu mention--box bg-n-solid-1 p-1 rounded-xl text-sm overflow-auto absolute w-full z-20 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border border-solid border-n-strong"
- :class="{
- 'border-b-[0.5rem] border-solid border-white dark:!border-slate-700':
- items.length <= 4,
- }"
>
{
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
// If the sidebar order is not set, use the default order.
if (!itemsOrder) {
- return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
+ return [...DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER];
}
// Create a copy of itemsOrder to avoid mutating the original store object.
const itemsOrderCopy = [...itemsOrder];
diff --git a/app/javascript/dashboard/helper/permissionsHelper.js b/app/javascript/dashboard/helper/permissionsHelper.js
index 9200677bc..25f97c038 100644
--- a/app/javascript/dashboard/helper/permissionsHelper.js
+++ b/app/javascript/dashboard/helper/permissionsHelper.js
@@ -16,6 +16,15 @@ export const getUserPermissions = (user, accountId) => {
return currentAccount.permissions || [];
};
+export const getUserRole = (user, accountId) => {
+ const currentAccount = getCurrentAccount(user, accountId) || {};
+ if (currentAccount.custom_role_id) {
+ return 'custom_role';
+ }
+
+ return currentAccount.role || 'agent';
+};
+
const isPermissionsPresentInRoute = route =>
route.meta && route.meta.permissions;
diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js
index 33f140fad..f5b83e546 100644
--- a/app/javascript/dashboard/store/modules/conversations/getters.js
+++ b/app/javascript/dashboard/store/modules/conversations/getters.js
@@ -1,7 +1,11 @@
import { MESSAGE_TYPE } from 'shared/constants/messages';
-import { applyPageFilters, sortComparator } from './helpers';
+import { applyPageFilters, applyRoleFilter, sortComparator } from './helpers';
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
import { matchesFilters } from './helpers/filterHelpers';
+import {
+ getUserPermissions,
+ getUserRole,
+} from '../../../helper/permissionsHelper';
import camelcaseKeys from 'camelcase-keys';
export const getSelectedChatConversation = ({
@@ -77,10 +81,24 @@ const getters = {
return isUnAssigned && shouldFilter;
});
},
- getAllStatusChats: _state => activeFilters => {
+ getAllStatusChats: (_state, _, __, rootGetters) => activeFilters => {
+ const currentUser = rootGetters.getCurrentUser;
+ const currentUserId = rootGetters.getCurrentUser.id;
+ const currentAccountId = rootGetters.getCurrentAccountId;
+
+ const permissions = getUserPermissions(currentUser, currentAccountId);
+ const userRole = getUserRole(currentUser, currentAccountId);
+
return _state.allConversations.filter(conversation => {
const shouldFilter = applyPageFilters(conversation, activeFilters);
- return shouldFilter;
+ const allowedForRole = applyRoleFilter(
+ conversation,
+ userRole,
+ permissions,
+ currentUserId
+ );
+
+ return shouldFilter && allowedForRole;
});
},
getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus,
diff --git a/app/javascript/dashboard/store/modules/conversations/helpers.js b/app/javascript/dashboard/store/modules/conversations/helpers.js
index 0063c8cfc..ebbdcbe64 100644
--- a/app/javascript/dashboard/store/modules/conversations/helpers.js
+++ b/app/javascript/dashboard/store/modules/conversations/helpers.js
@@ -62,6 +62,51 @@ export const applyPageFilters = (conversation, filters) => {
return shouldFilter;
};
+/**
+ * Filters conversations based on user role and permissions
+ *
+ * @param {Object} conversation - The conversation object to check permissions for
+ * @param {string} role - The user's role (administrator, agent, etc.)
+ * @param {Array} permissions - List of permission strings the user has
+ * @param {number|string} currentUserId - The ID of the current user
+ * @returns {boolean} - Whether the user has permissions to access this conversation
+ */
+export const applyRoleFilter = (
+ conversation,
+ role,
+ permissions,
+ currentUserId
+) => {
+ // the role === "agent" check is typically not correct on it's own
+ // the backend handles this by checking the custom_role_id at the user model
+ // here however, the `getUserRole` returns "custom_role" if the id is present,
+ // so we can check the role === "agent" directly
+ if (['administrator', 'agent'].includes(role)) {
+ return true;
+ }
+
+ // Check for full conversation management permission
+ if (permissions.includes('conversation_manage')) {
+ return true;
+ }
+
+ const conversationAssignee = conversation.meta.assignee;
+ const isUnassigned = !conversationAssignee;
+ const isAssignedToUser = conversationAssignee?.id === currentUserId;
+
+ // Check unassigned management permission
+ if (permissions.includes('conversation_unassigned_manage')) {
+ return isUnassigned || isAssignedToUser;
+ }
+
+ // Check participating conversation management permission
+ if (permissions.includes('conversation_participating_manage')) {
+ return isAssignedToUser;
+ }
+
+ return false;
+};
+
const SORT_OPTIONS = {
last_activity_at_asc: ['sortOnLastActivityAt', 'asc'],
last_activity_at_desc: ['sortOnLastActivityAt', 'desc'],
diff --git a/app/javascript/dashboard/store/modules/conversations/specs/helpers.spec.js b/app/javascript/dashboard/store/modules/conversations/specs/helpers.spec.js
new file mode 100644
index 000000000..f9118281e
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/conversations/specs/helpers.spec.js
@@ -0,0 +1,276 @@
+import { describe, it, expect } from 'vitest';
+import { applyRoleFilter } from '../helpers';
+
+describe('Conversation Helpers', () => {
+ describe('#applyRoleFilter', () => {
+ // Test data for conversations
+ const conversationWithAssignee = {
+ meta: {
+ assignee: {
+ id: 1,
+ },
+ },
+ };
+
+ const conversationWithDifferentAssignee = {
+ meta: {
+ assignee: {
+ id: 2,
+ },
+ },
+ };
+
+ const conversationWithoutAssignee = {
+ meta: {
+ assignee: null,
+ },
+ };
+
+ // Test for administrator role
+ it('always returns true for administrator role regardless of permissions', () => {
+ const role = 'administrator';
+ const permissions = [];
+ const currentUserId = 1;
+
+ expect(
+ applyRoleFilter(
+ conversationWithAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ expect(
+ applyRoleFilter(
+ conversationWithDifferentAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ expect(
+ applyRoleFilter(
+ conversationWithoutAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ });
+
+ // Test for agent role
+ it('always returns true for agent role regardless of permissions', () => {
+ const role = 'agent';
+ const permissions = [];
+ const currentUserId = 1;
+
+ expect(
+ applyRoleFilter(
+ conversationWithAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ expect(
+ applyRoleFilter(
+ conversationWithDifferentAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ expect(
+ applyRoleFilter(
+ conversationWithoutAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ });
+
+ // Test for custom role with 'conversation_manage' permission
+ it('returns true for any user with conversation_manage permission', () => {
+ const role = 'custom_role';
+ const permissions = ['conversation_manage'];
+ const currentUserId = 1;
+
+ expect(
+ applyRoleFilter(
+ conversationWithAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ expect(
+ applyRoleFilter(
+ conversationWithDifferentAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ expect(
+ applyRoleFilter(
+ conversationWithoutAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ });
+
+ // Test for custom role with 'conversation_unassigned_manage' permission
+ describe('with conversation_unassigned_manage permission', () => {
+ const role = 'custom_role';
+ const permissions = ['conversation_unassigned_manage'];
+ const currentUserId = 1;
+
+ it('returns true for conversations assigned to the user', () => {
+ expect(
+ applyRoleFilter(
+ conversationWithAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ });
+
+ it('returns true for unassigned conversations', () => {
+ expect(
+ applyRoleFilter(
+ conversationWithoutAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ });
+
+ it('returns false for conversations assigned to other users', () => {
+ expect(
+ applyRoleFilter(
+ conversationWithDifferentAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(false);
+ });
+ });
+
+ // Test for custom role with 'conversation_participating_manage' permission
+ describe('with conversation_participating_manage permission', () => {
+ const role = 'custom_role';
+ const permissions = ['conversation_participating_manage'];
+ const currentUserId = 1;
+
+ it('returns true for conversations assigned to the user', () => {
+ expect(
+ applyRoleFilter(
+ conversationWithAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ });
+
+ it('returns false for unassigned conversations', () => {
+ expect(
+ applyRoleFilter(
+ conversationWithoutAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(false);
+ });
+
+ it('returns false for conversations assigned to other users', () => {
+ expect(
+ applyRoleFilter(
+ conversationWithDifferentAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(false);
+ });
+ });
+
+ // Test for user with no relevant permissions
+ it('returns false for custom role without any relevant permissions', () => {
+ const role = 'custom_role';
+ const permissions = ['some_other_permission'];
+ const currentUserId = 1;
+
+ expect(
+ applyRoleFilter(
+ conversationWithAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(false);
+ expect(
+ applyRoleFilter(
+ conversationWithDifferentAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(false);
+ expect(
+ applyRoleFilter(
+ conversationWithoutAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(false);
+ });
+
+ // Test edge cases for meta.assignee
+ describe('handles edge cases with meta.assignee', () => {
+ const role = 'custom_role';
+ const permissions = ['conversation_unassigned_manage'];
+ const currentUserId = 1;
+
+ it('treats undefined assignee as unassigned', () => {
+ const conversationWithUndefinedAssignee = {
+ meta: {
+ assignee: undefined,
+ },
+ };
+
+ expect(
+ applyRoleFilter(
+ conversationWithUndefinedAssignee,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ });
+
+ it('handles empty meta object', () => {
+ const conversationWithEmptyMeta = {
+ meta: {},
+ };
+
+ expect(
+ applyRoleFilter(
+ conversationWithEmptyMeta,
+ role,
+ permissions,
+ currentUserId
+ )
+ ).toBe(true);
+ });
+ });
+ });
+});
diff --git a/theme/colors.js b/theme/colors.js
index 919a9ecf3..67c20202a 100644
--- a/theme/colors.js
+++ b/theme/colors.js
@@ -315,6 +315,21 @@ export const colors = {
12: 'rgb(var(--iris-12) / )',
},
+ blue: {
+ 1: 'rgb(var(--blue-1) / )',
+ 2: 'rgb(var(--blue-2) / )',
+ 3: 'rgb(var(--blue-3) / )',
+ 4: 'rgb(var(--blue-4) / )',
+ 5: 'rgb(var(--blue-5) / )',
+ 6: 'rgb(var(--blue-6) / )',
+ 7: 'rgb(var(--blue-7) / )',
+ 8: 'rgb(var(--blue-8) / )',
+ 9: 'rgb(var(--blue-9) / )',
+ 10: 'rgb(var(--blue-10) / )',
+ 11: 'rgb(var(--blue-11) / )',
+ 12: 'rgb(var(--blue-12) / )',
+ },
+
ruby: {
1: 'rgb(var(--ruby-1) / )',
2: 'rgb(var(--ruby-2) / )',