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:
Aswin Dev P.S
2021-09-23 12:52:49 +05:30
committed by GitHub
parent 0c24df96a8
commit 4f51a46c2b
22 changed files with 387 additions and 32 deletions

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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')"

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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,
});
},
};

View File

@@ -13,6 +13,7 @@ const state = {
isFetchingItem: false,
isFetchingInboxes: false,
isUpdating: false,
isDeleting: false,
},
sortOrder: [],
};

View File

@@ -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];

View File

@@ -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 {

View File

@@ -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 },
],
]);
});
});
});

View File

@@ -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',