mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Ability to delete a contact (#2984)
This change allows the administrator user to delete a contact and its related data like conversations, contact inboxes, and reports. Fixes #1929
This commit is contained in:
@@ -19,6 +19,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
'conversation.typing_off': this.onTypingOff,
|
||||
'conversation.contact_changed': this.onConversationContactChange,
|
||||
'presence.update': this.onPresenceUpdate,
|
||||
'contact.deleted': this.onContactDelete,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,6 +116,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
fetchConversationStats = () => {
|
||||
bus.$emit('fetch_conversation_stats');
|
||||
};
|
||||
|
||||
onContactDelete = data => {
|
||||
this.app.$store.dispatch(
|
||||
'contacts/deleteContactThroughConversations',
|
||||
data.id
|
||||
);
|
||||
this.fetchConversationStats();
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -54,6 +54,22 @@
|
||||
"TITLE": "Create new contact",
|
||||
"DESC": "Add basic information details about the contact."
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
"BUTTON_LABEL": "Delete Contact",
|
||||
"TITLE": "Delete contact",
|
||||
"DESC": "Delete contact details",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"PLACE_HOLDER": "Please type {contactName} to confirm",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
}
|
||||
},
|
||||
"CONTACT_FORM": {
|
||||
"FORM": {
|
||||
"SUBMIT": "Submit",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<span class="close-button" @click="onClose">
|
||||
<i class="ion-android-close close-icon" />
|
||||
</span>
|
||||
<contact-info show-new-message :contact="contact" />
|
||||
<contact-info show-new-message :contact="contact" @panel-close="onClose" />
|
||||
<accordion-item
|
||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
||||
|
||||
@@ -48,30 +48,59 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="!showNewMessage"
|
||||
class="edit-contact"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="toggleEditModal"
|
||||
>
|
||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<div v-else class="contact-actions">
|
||||
<woot-button
|
||||
class="new-message"
|
||||
size="small expanded"
|
||||
@click="toggleConversationModal"
|
||||
>
|
||||
{{ $t('CONTACT_PANEL.NEW_MESSAGE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small expanded"
|
||||
@click="toggleEditModal"
|
||||
>
|
||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<div v-if="!showNewMessage">
|
||||
<div>
|
||||
<woot-button
|
||||
class="edit-contact"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="toggleEditModal"
|
||||
>
|
||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="isAdmin">
|
||||
<woot-button
|
||||
class="delete-contact"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="alert"
|
||||
@click="toggleDeleteModal"
|
||||
:disabled="uiFlags.isDeleting"
|
||||
>
|
||||
{{ $t('DELETE_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="contact-actions">
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
class="new-message"
|
||||
icon="ion-chatboxes"
|
||||
size="small expanded"
|
||||
@click="toggleConversationModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
class="edit-contact"
|
||||
icon="ion-edit"
|
||||
variant="smooth"
|
||||
size="small expanded"
|
||||
@click="toggleEditModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
v-tooltip="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
class="delete-contact"
|
||||
icon="ion-trash-a"
|
||||
variant="hollow"
|
||||
size="small expanded"
|
||||
color-scheme="alert"
|
||||
@click="toggleDeleteModal"
|
||||
:disabled="uiFlags.isDeleting"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<edit-contact
|
||||
v-if="showEditModal"
|
||||
@@ -80,11 +109,24 @@
|
||||
@cancel="toggleEditModal"
|
||||
/>
|
||||
<new-conversation
|
||||
v-if="contact.id"
|
||||
:show="showConversationModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleConversationModal"
|
||||
/>
|
||||
</div>
|
||||
<woot-confirm-delete-modal
|
||||
v-if="showDeleteModal"
|
||||
:show.sync="showDeleteModal"
|
||||
:title="$t('DELETE_CONTACT.CONFIRM.TITLE')"
|
||||
:message="confirmDeleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
:confirm-value="contact.name"
|
||||
:confirm-place-holder-text="confirmPlaceHolderText"
|
||||
@on-confirm="confirmDeletion"
|
||||
@on-close="closeDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -93,6 +135,9 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import SocialIcons from './SocialIcons';
|
||||
import EditContact from './EditContact';
|
||||
import NewConversation from './NewConversation';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import adminMixin from '../../../../mixins/isAdmin';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -102,6 +147,7 @@ export default {
|
||||
SocialIcons,
|
||||
NewConversation,
|
||||
},
|
||||
mixins: [alertMixin, adminMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
@@ -120,9 +166,11 @@ export default {
|
||||
return {
|
||||
showEditModal: false,
|
||||
showConversationModal: false,
|
||||
showDeleteModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
@@ -134,6 +182,23 @@ export default {
|
||||
|
||||
return { twitter: twitterScreenName, ...(socialProfiles || {}) };
|
||||
},
|
||||
// Delete Modal
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.YES')} ${this.contact.name}`;
|
||||
},
|
||||
deleteRejectText() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.NO')} ${this.contact.name}`;
|
||||
},
|
||||
confirmDeleteMessage() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.MESSAGE')} ${
|
||||
this.contact.name
|
||||
} ?`;
|
||||
},
|
||||
confirmPlaceHolderText() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.PLACE_HOLDER', {
|
||||
contactName: this.contact.name,
|
||||
})}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleEditModal() {
|
||||
@@ -142,6 +207,31 @@ export default {
|
||||
toggleConversationModal() {
|
||||
this.showConversationModal = !this.showConversationModal;
|
||||
},
|
||||
toggleDeleteModal() {
|
||||
this.showDeleteModal = !this.showDeleteModal;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.deleteContact(this.contact);
|
||||
this.closeDelete();
|
||||
},
|
||||
closeDelete() {
|
||||
this.showDeleteModal = false;
|
||||
this.showConversationModal = false;
|
||||
this.showEditModal = false;
|
||||
},
|
||||
async deleteContact({ id }) {
|
||||
try {
|
||||
await this.$store.dispatch('contacts/delete', id);
|
||||
this.$emit('panel-close');
|
||||
this.showAlert(this.$t('DELETE_CONTACT.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message
|
||||
? error.message
|
||||
: this.$t('DELETE_CONTACT.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -179,17 +269,32 @@ export default {
|
||||
.contact-actions {
|
||||
margin-top: var(--space-small);
|
||||
}
|
||||
.button.edit-contact {
|
||||
|
||||
.edit-contact {
|
||||
margin-left: var(--space-medium);
|
||||
}
|
||||
|
||||
.button.new-message {
|
||||
margin-right: var(--space-small);
|
||||
.delete-contact {
|
||||
margin-left: var(--space-medium);
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.new-message {
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
|
||||
.edit-contact {
|
||||
margin-left: var(--space-small);
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
|
||||
.delete-contact {
|
||||
margin-left: var(--space-small);
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -82,6 +82,9 @@ export const mutations = {
|
||||
const conversations = $state.records[id] || [];
|
||||
Vue.set($state.records, id, [...conversations, data]);
|
||||
},
|
||||
[types.default.DELETE_CONTACT_CONVERSATION]: ($state, id) => {
|
||||
Vue.delete($state.records, id);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -83,6 +83,21 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ commit }, id) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await ContactAPI.delete(id);
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: false });
|
||||
if (error.response?.data?.message) {
|
||||
throw new Error(error.response.data.message);
|
||||
} else {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
fetchContactableInbox: async ({ commit }, id) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true });
|
||||
try {
|
||||
@@ -110,4 +125,12 @@ export const actions = {
|
||||
setContact({ commit }, data) {
|
||||
commit(types.SET_CONTACT_ITEM, data);
|
||||
},
|
||||
|
||||
deleteContactThroughConversations: ({ commit }, id) => {
|
||||
commit(types.DELETE_CONTACT, id);
|
||||
commit(types.CLEAR_CONTACT_CONVERSATIONS, id, { root: true });
|
||||
commit(`contactConversations/${types.DELETE_CONTACT_CONVERSATION}`, id, {
|
||||
root: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ const state = {
|
||||
isFetchingItem: false,
|
||||
isFetchingInboxes: false,
|
||||
isUpdating: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
sortOrder: [],
|
||||
};
|
||||
|
||||
@@ -46,6 +46,12 @@ export const mutations = {
|
||||
Vue.set($state.records, data.id, data);
|
||||
},
|
||||
|
||||
[types.DELETE_CONTACT]: ($state, id) => {
|
||||
const index = $state.sortOrder.findIndex(item => item === id);
|
||||
Vue.delete($state.sortOrder, index);
|
||||
Vue.delete($state.records, id);
|
||||
},
|
||||
|
||||
[types.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
|
||||
Object.values($state.records).forEach(element => {
|
||||
const availabilityStatus = data[element.id];
|
||||
|
||||
@@ -177,6 +177,13 @@ export const mutations = {
|
||||
Vue.set(chat, 'can_reply', canReply);
|
||||
}
|
||||
},
|
||||
|
||||
[types.default.CLEAR_CONTACT_CONVERSATIONS](_state, contactId) {
|
||||
const chats = _state.allConversations.filter(
|
||||
c => c.meta.sender.id !== contactId
|
||||
);
|
||||
Vue.set(_state, 'allConversations', chats);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -139,6 +139,27 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('sends correct mutations if API is success', async () => {
|
||||
axios.delete.mockResolvedValue();
|
||||
await actions.delete({ commit }, contactList[0].id);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONTACT_UI_FLAG, { isDeleting: true }],
|
||||
[types.SET_CONTACT_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.delete({ commit }, contactList[0].id)
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONTACT_UI_FLAG, { isDeleting: true }],
|
||||
[types.SET_CONTACT_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setContact', () => {
|
||||
it('returns correct mutations', () => {
|
||||
const data = { id: 1, name: 'john doe', availability_status: 'online' };
|
||||
@@ -146,4 +167,19 @@ describe('#actions', () => {
|
||||
expect(commit.mock.calls).toEqual([[types.SET_CONTACT_ITEM, data]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteContactThroughConversations', () => {
|
||||
it('returns correct mutations', () => {
|
||||
actions.deleteContactThroughConversations({ commit }, contactList[0].id);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.DELETE_CONTACT, contactList[0].id],
|
||||
[types.CLEAR_CONTACT_CONVERSATIONS, contactList[0].id, { root: true }],
|
||||
[
|
||||
`contactConversations/${types.DELETE_CONTACT_CONVERSATION}`,
|
||||
contactList[0].id,
|
||||
{ root: true },
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ export default {
|
||||
CHANGE_CHAT_STATUS_FILTER: 'CHANGE_CHAT_STATUS_FILTER',
|
||||
UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE',
|
||||
UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT',
|
||||
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
|
||||
|
||||
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
|
||||
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
|
||||
@@ -101,6 +102,7 @@ export default {
|
||||
SET_CONTACTS: 'SET_CONTACTS',
|
||||
CLEAR_CONTACTS: 'CLEAR_CONTACTS',
|
||||
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||
DELETE_CONTACT: 'DELETE_CONTACT',
|
||||
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
||||
|
||||
// Notifications
|
||||
@@ -119,6 +121,7 @@ export default {
|
||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
||||
ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION',
|
||||
DELETE_CONTACT_CONVERSATION: 'DELETE_CONTACT_CONVERSATION',
|
||||
|
||||
// Contact Label
|
||||
SET_CONTACT_LABELS_UI_FLAG: 'SET_CONTACT_LABELS_UI_FLAG',
|
||||
|
||||
Reference in New Issue
Block a user