Files
leadchat/app/javascript/dashboard/composables/chatlist/useBulkActions.js
Sivin Varghese cfe3061b5d 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
2026-02-17 13:30:55 +05:30

219 lines
6.4 KiB
JavaScript

import { ref, unref } from 'vue';
import { useStore } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store.js';
import { useConversationRequiredAttributes } from 'dashboard/composables/useConversationRequiredAttributes';
import wootConstants from 'dashboard/constants/globals';
export function useBulkActions() {
const store = useStore();
const { t } = useI18n();
const { checkMissingAttributes } = useConversationRequiredAttributes();
const selectedConversations = useMapGetter(
'bulkActions/getSelectedConversationIds'
);
const selectedInboxes = ref([]);
function selectConversation(conversationId, inboxId) {
store.dispatch('bulkActions/setSelectedConversationIds', conversationId);
selectedInboxes.value = [...selectedInboxes.value, inboxId];
}
function deSelectConversation(conversationId, inboxId) {
store.dispatch('bulkActions/removeSelectedConversationIds', conversationId);
selectedInboxes.value = selectedInboxes.value.filter(
item => item !== inboxId
);
}
function resetBulkActions() {
store.dispatch('bulkActions/clearSelectedConversationIds');
selectedInboxes.value = [];
}
function selectAllConversations(check, conversationList) {
const availableConversations = unref(conversationList);
if (check) {
store.dispatch(
'bulkActions/setSelectedConversationIds',
availableConversations.map(item => item.id)
);
selectedInboxes.value = availableConversations.map(item => item.inbox_id);
} else {
resetBulkActions();
}
}
function isConversationSelected(id) {
return selectedConversations.value.includes(id);
}
// Same method used in context menu, conversationId being passed from there.
async function onAssignAgent(agent, conversationId = null) {
try {
await store.dispatch('bulkActions/process', {
type: 'Conversation',
ids: conversationId || selectedConversations.value,
fields: {
assignee_id: agent.id,
},
});
store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) {
useAlert(
t('CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL', {
agentName: agent.name,
conversationId,
})
);
} else {
useAlert(t('BULK_ACTION.ASSIGN_SUCCESFUL'));
}
} catch (err) {
useAlert(t('BULK_ACTION.ASSIGN_FAILED'));
}
}
// Same method used in context menu, conversationId being passed from there.
async function onAssignLabels(newLabels, conversationId = null) {
try {
await store.dispatch('bulkActions/process', {
type: 'Conversation',
ids: conversationId || selectedConversations.value,
labels: {
add: newLabels,
},
});
store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) {
useAlert(
t('CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL', {
labelName: newLabels[0],
conversationId,
})
);
} else {
useAlert(t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
}
} catch (err) {
useAlert(t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
}
}
// 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', {
type: 'Conversation',
ids: selectedConversations.value,
fields: {
team_id: team.id,
},
});
store.dispatch('bulkActions/clearSelectedConversationIds');
useAlert(t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
} catch (err) {
useAlert(t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
}
}
async function onUpdateConversations(status, snoozedUntil) {
let conversationIds = selectedConversations.value;
let skippedCount = 0;
// If resolving, check for required attributes
if (status === wootConstants.STATUS_TYPE.RESOLVED) {
const { validIds, skippedIds } = selectedConversations.value.reduce(
(acc, id) => {
const conversation = store.getters.getConversationById(id);
const currentCustomAttributes = conversation?.custom_attributes || {};
const { hasMissing } = checkMissingAttributes(
currentCustomAttributes
);
if (!hasMissing) {
acc.validIds.push(id);
} else {
acc.skippedIds.push(id);
}
return acc;
},
{ validIds: [], skippedIds: [] }
);
conversationIds = validIds;
skippedCount = skippedIds.length;
if (skippedCount > 0 && validIds.length === 0) {
// All conversations have missing attributes
useAlert(
t('BULK_ACTION.RESOLVE.ALL_MISSING_ATTRIBUTES') ||
'Cannot resolve conversations due to missing required attributes'
);
return;
}
}
try {
if (conversationIds.length > 0) {
await store.dispatch('bulkActions/process', {
type: 'Conversation',
ids: conversationIds,
fields: {
status,
},
snoozed_until: snoozedUntil,
});
}
store.dispatch('bulkActions/clearSelectedConversationIds');
if (skippedCount > 0) {
useAlert(t('BULK_ACTION.RESOLVE.PARTIAL_SUCCESS'));
} else {
useAlert(t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
}
} catch (err) {
useAlert(t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
}
}
return {
selectedConversations,
selectedInboxes,
selectConversation,
deSelectConversation,
selectAllConversations,
resetBulkActions,
isConversationSelected,
onAssignAgent,
onAssignLabels,
onRemoveLabels,
onAssignTeamsForBulk,
onUpdateConversations,
};
}