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
This commit is contained in:
parent
aa7e3c2d38
commit
cfe3061b5d
@ -145,6 +145,7 @@ const {
|
|||||||
isConversationSelected,
|
isConversationSelected,
|
||||||
onAssignAgent,
|
onAssignAgent,
|
||||||
onAssignLabels,
|
onAssignLabels,
|
||||||
|
onRemoveLabels,
|
||||||
onAssignTeamsForBulk,
|
onAssignTeamsForBulk,
|
||||||
onUpdateConversations,
|
onUpdateConversations,
|
||||||
} = useBulkActions();
|
} = useBulkActions();
|
||||||
@ -859,6 +860,7 @@ provide('deSelectConversation', deSelectConversation);
|
|||||||
provide('assignAgent', onAssignAgent);
|
provide('assignAgent', onAssignAgent);
|
||||||
provide('assignTeam', onAssignTeam);
|
provide('assignTeam', onAssignTeam);
|
||||||
provide('assignLabels', onAssignLabels);
|
provide('assignLabels', onAssignLabels);
|
||||||
|
provide('removeLabels', onRemoveLabels);
|
||||||
provide('updateConversationStatus', handleResolveConversation);
|
provide('updateConversationStatus', handleResolveConversation);
|
||||||
provide('toggleContextMenu', onContextMenuToggle);
|
provide('toggleContextMenu', onContextMenuToggle);
|
||||||
provide('markAsUnread', markAsUnread);
|
provide('markAsUnread', markAsUnread);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export default {
|
|||||||
'assignAgent',
|
'assignAgent',
|
||||||
'assignTeam',
|
'assignTeam',
|
||||||
'assignLabels',
|
'assignLabels',
|
||||||
|
'removeLabels',
|
||||||
'updateConversationStatus',
|
'updateConversationStatus',
|
||||||
'toggleContextMenu',
|
'toggleContextMenu',
|
||||||
'markAsUnread',
|
'markAsUnread',
|
||||||
@ -63,6 +64,7 @@ export default {
|
|||||||
@assign-agent="assignAgent"
|
@assign-agent="assignAgent"
|
||||||
@assign-team="assignTeam"
|
@assign-team="assignTeam"
|
||||||
@assign-label="assignLabels"
|
@assign-label="assignLabels"
|
||||||
|
@remove-label="removeLabels"
|
||||||
@update-conversation-status="updateConversationStatus"
|
@update-conversation-status="updateConversationStatus"
|
||||||
@context-menu-toggle="toggleContextMenu"
|
@context-menu-toggle="toggleContextMenu"
|
||||||
@mark-as-unread="markAsUnread"
|
@mark-as-unread="markAsUnread"
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const emit = defineEmits([
|
|||||||
'contextMenuToggle',
|
'contextMenuToggle',
|
||||||
'assignAgent',
|
'assignAgent',
|
||||||
'assignLabel',
|
'assignLabel',
|
||||||
|
'removeLabel',
|
||||||
'assignTeam',
|
'assignTeam',
|
||||||
'markAsUnread',
|
'markAsUnread',
|
||||||
'markAsRead',
|
'markAsRead',
|
||||||
@ -203,7 +204,10 @@ const onAssignAgent = agent => {
|
|||||||
|
|
||||||
const onAssignLabel = label => {
|
const onAssignLabel = label => {
|
||||||
emit('assignLabel', [label.title], [props.chat.id]);
|
emit('assignLabel', [label.title], [props.chat.id]);
|
||||||
closeContextMenu();
|
};
|
||||||
|
|
||||||
|
const onRemoveLabel = label => {
|
||||||
|
emit('removeLabel', [label.title], [props.chat.id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAssignTeam = team => {
|
const onAssignTeam = team => {
|
||||||
@ -379,11 +383,13 @@ const deleteConversation = () => {
|
|||||||
:priority="chat.priority"
|
:priority="chat.priority"
|
||||||
:chat-id="chat.id"
|
:chat-id="chat.id"
|
||||||
:has-unread-messages="hasUnread"
|
:has-unread-messages="hasUnread"
|
||||||
|
:conversation-labels="chat.labels"
|
||||||
:conversation-url="conversationPath"
|
:conversation-url="conversationPath"
|
||||||
:allowed-options="allowedContextMenuOptions"
|
:allowed-options="allowedContextMenuOptions"
|
||||||
@update-conversation="onUpdateConversation"
|
@update-conversation="onUpdateConversation"
|
||||||
@assign-agent="onAssignAgent"
|
@assign-agent="onAssignAgent"
|
||||||
@assign-label="onAssignLabel"
|
@assign-label="onAssignLabel"
|
||||||
|
@remove-label="onRemoveLabel"
|
||||||
@assign-team="onAssignTeam"
|
@assign-team="onAssignTeam"
|
||||||
@mark-as-unread="markAsUnread"
|
@mark-as-unread="markAsUnread"
|
||||||
@mark-as-read="markAsRead"
|
@mark-as-read="markAsRead"
|
||||||
|
|||||||
@ -53,6 +53,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
conversationLabels: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
conversationUrl: {
|
conversationUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
@ -70,6 +74,7 @@ export default {
|
|||||||
'assignAgent',
|
'assignAgent',
|
||||||
'assignTeam',
|
'assignTeam',
|
||||||
'assignLabel',
|
'assignLabel',
|
||||||
|
'removeLabel',
|
||||||
'deleteConversation',
|
'deleteConversation',
|
||||||
'close',
|
'close',
|
||||||
],
|
],
|
||||||
@ -334,8 +339,16 @@ export default {
|
|||||||
v-for="label in labels"
|
v-for="label in labels"
|
||||||
:key="label.id"
|
:key="label.id"
|
||||||
:option="generateMenuLabelConfig(label, 'label')"
|
:option="generateMenuLabelConfig(label, 'label')"
|
||||||
variant="label"
|
:variant="
|
||||||
@click.stop="$emit('assignLabel', label)"
|
conversationLabels.includes(label.title)
|
||||||
|
? 'label-assigned'
|
||||||
|
: 'label'
|
||||||
|
"
|
||||||
|
@click.stop="
|
||||||
|
conversationLabels.includes(label.title)
|
||||||
|
? $emit('removeLabel', label)
|
||||||
|
: $emit('assignLabel', label)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</MenuItemWithSubmenu>
|
</MenuItemWithSubmenu>
|
||||||
<MenuItemWithSubmenu
|
<MenuItemWithSubmenu
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
option: {
|
option: {
|
||||||
@ -22,7 +23,9 @@ defineProps({
|
|||||||
class="flex-shrink-0"
|
class="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="variant === 'label' && option.color"
|
v-if="
|
||||||
|
(variant === 'label' || variant === 'label-assigned') && option.color
|
||||||
|
"
|
||||||
class="label-pill flex-shrink-0"
|
class="label-pill flex-shrink-0"
|
||||||
:style="{ backgroundColor: option.color }"
|
:style="{ backgroundColor: option.color }"
|
||||||
/>
|
/>
|
||||||
@ -37,6 +40,11 @@ defineProps({
|
|||||||
<p class="menu-label truncate min-w-0 flex-1">
|
<p class="menu-label truncate min-w-0 flex-1">
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</p>
|
</p>
|
||||||
|
<Icon
|
||||||
|
v-if="variant === 'label-assigned'"
|
||||||
|
icon="i-lucide-check"
|
||||||
|
class="flex-shrink-0 size-3.5 mr-1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
async function onAssignTeamsForBulk(team) {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('bulkActions/process', {
|
await store.dispatch('bulkActions/process', {
|
||||||
@ -189,6 +211,7 @@ export function useBulkActions() {
|
|||||||
isConversationSelected,
|
isConversationSelected,
|
||||||
onAssignAgent,
|
onAssignAgent,
|
||||||
onAssignLabels,
|
onAssignLabels,
|
||||||
|
onRemoveLabels,
|
||||||
onAssignTeamsForBulk,
|
onAssignTeamsForBulk,
|
||||||
onUpdateConversations,
|
onUpdateConversations,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -174,6 +174,10 @@
|
|||||||
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
|
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
|
||||||
"FAILED": "Couldn't assign label. Please try again."
|
"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": {
|
"TEAM_ASSIGNMENT": {
|
||||||
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
|
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
|
||||||
"FAILED": "Couldn't assign team. Please try again."
|
"FAILED": "Couldn't assign team. Please try again."
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user