From cfe3061b5d7ca88738a5fa2df841dc4d3f61b9da Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:30:55 +0530 Subject: [PATCH] feat: Allow removing labels via conversation context menu (#13525) # Pull Request Template ## Description This PR adds support for removing labels from the conversation card context menu. Assigned labels now show a checkmark, and clicking an already-selected label will remove it. Fixes https://linear.app/chatwoot/issue/CW-6400/allow-removing-labels-directly-from-the-right-click-menu https://github.com/chatwoot/chatwoot/issues/13367 ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? **Screencast** https://github.com/user-attachments/assets/4e3a6080-a67d-4851-9d10-d8dbf3ceeb04 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] 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/ChatList.vue | 2 ++ .../dashboard/components/ConversationItem.vue | 2 ++ .../widgets/conversation/ConversationCard.vue | 8 ++++++- .../conversation/contextMenu/Index.vue | 17 ++++++++++++-- .../conversation/contextMenu/menuItem.vue | 10 +++++++- .../composables/chatlist/useBulkActions.js | 23 +++++++++++++++++++ .../i18n/locale/en/conversation.json | 4 ++++ 7 files changed, 62 insertions(+), 4 deletions(-) diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 08e2057e4..3b2a66929 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -145,6 +145,7 @@ const { isConversationSelected, onAssignAgent, onAssignLabels, + onRemoveLabels, onAssignTeamsForBulk, onUpdateConversations, } = useBulkActions(); @@ -859,6 +860,7 @@ provide('deSelectConversation', deSelectConversation); provide('assignAgent', onAssignAgent); provide('assignTeam', onAssignTeam); provide('assignLabels', onAssignLabels); +provide('removeLabels', onRemoveLabels); provide('updateConversationStatus', handleResolveConversation); provide('toggleContextMenu', onContextMenuToggle); provide('markAsUnread', markAsUnread); diff --git a/app/javascript/dashboard/components/ConversationItem.vue b/app/javascript/dashboard/components/ConversationItem.vue index a705dd067..fcd41ad45 100644 --- a/app/javascript/dashboard/components/ConversationItem.vue +++ b/app/javascript/dashboard/components/ConversationItem.vue @@ -10,6 +10,7 @@ export default { 'assignAgent', 'assignTeam', 'assignLabels', + 'removeLabels', 'updateConversationStatus', 'toggleContextMenu', 'markAsUnread', @@ -63,6 +64,7 @@ export default { @assign-agent="assignAgent" @assign-team="assignTeam" @assign-label="assignLabels" + @remove-label="removeLabels" @update-conversation-status="updateConversationStatus" @context-menu-toggle="toggleContextMenu" @mark-as-unread="markAsUnread" diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 5093e5c4b..9f7805b4e 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -34,6 +34,7 @@ const emit = defineEmits([ 'contextMenuToggle', 'assignAgent', 'assignLabel', + 'removeLabel', 'assignTeam', 'markAsUnread', 'markAsRead', @@ -203,7 +204,10 @@ const onAssignAgent = agent => { const onAssignLabel = label => { emit('assignLabel', [label.title], [props.chat.id]); - closeContextMenu(); +}; + +const onRemoveLabel = label => { + emit('removeLabel', [label.title], [props.chat.id]); }; const onAssignTeam = team => { @@ -379,11 +383,13 @@ const deleteConversation = () => { :priority="chat.priority" :chat-id="chat.id" :has-unread-messages="hasUnread" + :conversation-labels="chat.labels" :conversation-url="conversationPath" :allowed-options="allowedContextMenuOptions" @update-conversation="onUpdateConversation" @assign-agent="onAssignAgent" @assign-label="onAssignLabel" + @remove-label="onRemoveLabel" @assign-team="onAssignTeam" @mark-as-unread="markAsUnread" @mark-as-read="markAsRead" diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue index a6f79500a..34009935c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue @@ -53,6 +53,10 @@ export default { type: String, default: null, }, + conversationLabels: { + type: Array, + default: () => [], + }, conversationUrl: { type: String, default: '', @@ -70,6 +74,7 @@ export default { 'assignAgent', 'assignTeam', 'assignLabel', + 'removeLabel', 'deleteConversation', 'close', ], @@ -334,8 +339,16 @@ export default { v-for="label in labels" :key="label.id" :option="generateMenuLabelConfig(label, 'label')" - variant="label" - @click.stop="$emit('assignLabel', label)" + :variant=" + conversationLabels.includes(label.title) + ? 'label-assigned' + : 'label' + " + @click.stop=" + conversationLabels.includes(label.title) + ? $emit('removeLabel', label) + : $emit('assignLabel', label) + " /> import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; defineProps({ option: { @@ -22,7 +23,9 @@ defineProps({ class="flex-shrink-0" /> @@ -37,6 +40,11 @@ defineProps({ + diff --git a/app/javascript/dashboard/composables/chatlist/useBulkActions.js b/app/javascript/dashboard/composables/chatlist/useBulkActions.js index a32c4c512..45421b978 100644 --- a/app/javascript/dashboard/composables/chatlist/useBulkActions.js +++ b/app/javascript/dashboard/composables/chatlist/useBulkActions.js @@ -102,6 +102,28 @@ export function useBulkActions() { } } + // Only used in context menu + async function onRemoveLabels(labelsToRemove, conversationId = null) { + try { + await store.dispatch('bulkActions/process', { + type: 'Conversation', + ids: conversationId || selectedConversations.value, + labels: { + remove: labelsToRemove, + }, + }); + + useAlert( + t('CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_REMOVAL.SUCCESFUL', { + labelName: labelsToRemove[0], + conversationId, + }) + ); + } catch (err) { + useAlert(t('CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_REMOVAL.FAILED')); + } + } + async function onAssignTeamsForBulk(team) { try { await store.dispatch('bulkActions/process', { @@ -189,6 +211,7 @@ export function useBulkActions() { isConversationSelected, onAssignAgent, onAssignLabels, + onRemoveLabels, onAssignTeamsForBulk, onUpdateConversations, }; diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 99e0bb072..c1c87bc0c 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -174,6 +174,10 @@ "SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}", "FAILED": "Couldn't assign label. Please try again." }, + "LABEL_REMOVAL": { + "SUCCESFUL": "Removed label #{labelName} from conversation id {conversationId}", + "FAILED": "Couldn't remove label. Please try again." + }, "TEAM_ASSIGNMENT": { "SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}", "FAILED": "Couldn't assign team. Please try again."