From 70a65e2c342a6c08bf0667734507820a84f5f835 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Wed, 9 Apr 2025 15:25:46 +0530
Subject: [PATCH 1/6] fix: Prevent mentions menu from triggering on reply mode
change (#11264)
# Pull Request Template
## Description
**Issue:** When typing "@" in reply mode and then switching to private
note mode, the user mentions menu remains visible and cannot be
dismissed.
**Cause:** The Prose Mirror suggestion plugin for "@" was active in both
reply modes (normal and private). When triggered in normal reply mode
and then switching to private note mode, the menu would remain open and
couldn't be dismissed.
**Solution**
```
createSuggestionPlugin({
trigger: '@',
showMenu: showUserMentions,
searchTerm: mentionSearchKey,
isAllowed: () => props.isPrivate, // Only allow @ mentions in private note mode
}),
```
1. By setting `isAllowed: () => props.isPrivate`, the @ mention trigger
will only activate when in private note mode
2. In normal reply mode, the plugin won't activate at all since
`isAllowed` returns false
3. This prevents the menu from being triggered in the wrong context and
fix the scenario where the menu gets stuck.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Loom video
**Before**
https://www.loom.com/share/5333f0c6498d4a9ea4d220b1de1b608c?sid=1425f24f-2c6f-4ff5-aab3-23c3203d2e05
**After**
https://www.loom.com/share/9f183c76d6a94b618a7c2aaed280b780?sid=19ef08b8-2b70-434b-ad5a-267410212e11
## 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
- [ ] 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
---
.../dashboard/components/widgets/WootWriter/Editor.vue | 1 +
.../dashboard/components/widgets/conversation/TagAgents.vue | 4 ----
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
index d9a0341b4..628c88ad3 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
@@ -220,6 +220,7 @@ const plugins = computed(() => {
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,
- }"
>
Date: Wed, 9 Apr 2025 16:03:53 +0530
Subject: [PATCH 2/6] chore: Update date range picker with new theme colors
(#11267)
---
.../dashboard/assets/scss/_next-colors.scss | 26 ++++++++++++
.../components/ui/DatePicker/DatePicker.vue | 8 ++--
.../DatePicker/components/CalendarAction.vue | 40 +++++++++----------
.../components/CalendarDateInput.vue | 2 +-
.../components/CalendarDateRange.vue | 8 ++--
.../DatePicker/components/CalendarFooter.vue | 25 +++++++-----
.../DatePicker/components/CalendarMonth.vue | 6 ++-
.../ui/DatePicker/components/CalendarWeek.vue | 13 +++---
.../ui/DatePicker/components/CalendarYear.vue | 6 +--
.../components/DatePickerButton.vue | 2 +-
theme/colors.js | 15 +++++++
11 files changed, 97 insertions(+), 54 deletions(-)
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/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) => {
-
-
-
+ />
{{ firstButtonLabel }}
{{ buttonLabel }}
-
-
-
+ />
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/CalendarFooter.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarFooter.vue
index beb2861ac..830f17ead 100644
--- a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarFooter.vue
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarFooter.vue
@@ -1,4 +1,6 @@
-
-
+
- {{ $t('DATE_PICKER.CLEAR_BUTTON') }}
-
-
+
- {{ $t('DATE_PICKER.APPLY_BUTTON') }}
-
+ />
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarMonth.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarMonth.vue
index ec8b8789d..c3ddb5bfd 100644
--- a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarMonth.vue
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarMonth.vue
@@ -71,10 +71,12 @@ const selectMonth = index => {
diff --git a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeek.vue b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeek.vue
index f78f88465..49aaf9e57 100644
--- a/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeek.vue
+++ b/app/javascript/dashboard/components/ui/DatePicker/components/CalendarWeek.vue
@@ -107,17 +107,16 @@ const isNextDayInRange = day => {
};
const dayClasses = day => ({
- 'text-slate-500 dark:text-slate-400 pointer-events-none':
- !isInCurrentMonth(day),
- 'text-slate-800 dark:text-slate-50 hover:text-slate-800 dark:hover:text-white hover:bg-woot-100 dark:hover:bg-woot-700':
+ 'text-n-slate-10 pointer-events-none': !isInCurrentMonth(day),
+ 'text-n-slate-12 hover:text-n-slate-12 hover:bg-n-blue-6 dark:hover:bg-n-blue-7':
isInCurrentMonth(day),
- 'bg-woot-600 dark:bg-woot-600 text-white dark:text-white':
+ 'bg-n-brand text-white':
isSelectedStartOrEndDate(day) && isInCurrentMonth(day),
- 'bg-woot-50 dark:bg-woot-800':
+ 'bg-n-blue-4 dark:bg-n-blue-5':
(isInRange(day) || isHoveringInRange(day)) &&
!isSelectedStartOrEndDate(day) &&
isInCurrentMonth(day),
- 'outline outline-1 outline-woot-200 -outline-offset-1 dark:outline-woot-700 text-woot-600 dark:text-woot-400':
+ 'outline outline-1 outline-n-blue-8 -outline-offset-1 !text-n-blue-text':
isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day),
});
@@ -164,7 +163,7 @@ const dayClasses = day => ({
!isLastDayOfMonth(day) &&
isInCurrentMonth(day)
"
- class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-woot-50 dark:bg-woot-800 -z-10"
+ class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-n-blue-4 dark:bg-n-blue-5 -z-10"
/>
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 = () => {
)',
},
+ 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) / )',
From 35d9dc925f30e141a17998b40660fc2948c41b41 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Wed, 9 Apr 2025 18:41:18 +0530
Subject: [PATCH 3/6] chore: Remove sorting by `phone_number` from contact list
(#11271)
---
.../Contacts/ContactsForm/CreateNewContactDialog.vue | 1 +
.../Contacts/ContactsHeader/components/ContactSortMenu.vue | 4 ----
2 files changed, 1 insertion(+), 4 deletions(-)
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',
From 73dcf539ed632a9f96cf82caf7642e9e34c65f6c Mon Sep 17 00:00:00 2001
From: Shivam Mishra
Date: Thu, 10 Apr 2025 00:16:20 +0530
Subject: [PATCH 4/6] feat: allow role based filtering on the frontend (#11246)
This pull request introduces frontend role filtering to allStatusChat
getter. The key changes include the addition of a new helper function to
get the user's role, updates to the conversation filtering logic to
incorporate role and permissions, and the addition of unit tests for the
new filtering logic.
---------
Co-authored-by: Muhsin Keloth
---
.../dashboard/helper/permissionsHelper.js | 9 +
.../store/modules/conversations/getters.js | 24 +-
.../store/modules/conversations/helpers.js | 45 +++
.../conversations/specs/helpers.spec.js | 276 ++++++++++++++++++
4 files changed, 351 insertions(+), 3 deletions(-)
create mode 100644 app/javascript/dashboard/store/modules/conversations/specs/helpers.spec.js
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);
+ });
+ });
+ });
+});
From 65cd0717e6807239daee8af4c283c4ed966c1cc0 Mon Sep 17 00:00:00 2001
From: Muhsin Keloth
Date: Thu, 10 Apr 2025 12:58:38 +0530
Subject: [PATCH 5/6] fix: Handle Instagram echo events (#11275)
This PR fixes the issue with message creation when someone sends
messages from the Instagram app instead of the Chatwoot dashboard.
---
app/builders/messages/instagram/base_message_builder.rb | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
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)
From 50344a282be5026770cdf5d3659f7a87d662c949 Mon Sep 17 00:00:00 2001
From: Pranav
Date: Fri, 11 Apr 2025 13:02:16 +0530
Subject: [PATCH 6/6] fix: Return new Array instead of freezed object (#11283)
Users who have not changed the order of the sidebar items were not able
to change it as the object returned was created using Object.freeze
To reproduce:
- Create a new user account, open a conversation, try changing the order
of the sidebar items.
---
app/javascript/dashboard/composables/useUISettings.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/javascript/dashboard/composables/useUISettings.js b/app/javascript/dashboard/composables/useUISettings.js
index f67f58ebc..418011e10 100644
--- a/app/javascript/dashboard/composables/useUISettings.js
+++ b/app/javascript/dashboard/composables/useUISettings.js
@@ -36,7 +36,7 @@ const useConversationSidebarItemsOrder = uiSettings => {
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];