mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 12:37:56 +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:
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index, :active, :search]
|
before_action :set_current_page, only: [:index, :active, :search]
|
||||||
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes]
|
||||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@@ -73,6 +73,18 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
}, status: :unprocessable_entity
|
}, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
if ::OnlineStatusTracker.get_presence(
|
||||||
|
@contact.account.id, 'Contact', @contact.id
|
||||||
|
)
|
||||||
|
return render_error({ message: I18n.t('contacts.online.delete', contact_name: @contact.name.capitalize) },
|
||||||
|
:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
@contact.destroy!
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# TODO: Move this to a finder class
|
# TODO: Move this to a finder class
|
||||||
@@ -137,4 +149,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
def fetch_contact
|
def fetch_contact
|
||||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_error(error, error_status)
|
||||||
|
render json: error, status: error_status
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
'conversation.typing_off': this.onTypingOff,
|
'conversation.typing_off': this.onTypingOff,
|
||||||
'conversation.contact_changed': this.onConversationContactChange,
|
'conversation.contact_changed': this.onConversationContactChange,
|
||||||
'presence.update': this.onPresenceUpdate,
|
'presence.update': this.onPresenceUpdate,
|
||||||
|
'contact.deleted': this.onContactDelete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +116,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
fetchConversationStats = () => {
|
fetchConversationStats = () => {
|
||||||
bus.$emit('fetch_conversation_stats');
|
bus.$emit('fetch_conversation_stats');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onContactDelete = data => {
|
||||||
|
this.app.$store.dispatch(
|
||||||
|
'contacts/deleteContactThroughConversations',
|
||||||
|
data.id
|
||||||
|
);
|
||||||
|
this.fetchConversationStats();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -54,6 +54,22 @@
|
|||||||
"TITLE": "Create new contact",
|
"TITLE": "Create new contact",
|
||||||
"DESC": "Add basic information details about the 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": {
|
"CONTACT_FORM": {
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"SUBMIT": "Submit",
|
"SUBMIT": "Submit",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<span class="close-button" @click="onClose">
|
<span class="close-button" @click="onClose">
|
||||||
<i class="ion-android-close close-icon" />
|
<i class="ion-android-close close-icon" />
|
||||||
</span>
|
</span>
|
||||||
<contact-info show-new-message :contact="contact" />
|
<contact-info show-new-message :contact="contact" @panel-close="onClose" />
|
||||||
<accordion-item
|
<accordion-item
|
||||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')"
|
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')"
|
||||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
||||||
|
|||||||
@@ -48,8 +48,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!showNewMessage">
|
||||||
|
<div>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="!showNewMessage"
|
|
||||||
class="edit-contact"
|
class="edit-contact"
|
||||||
variant="link"
|
variant="link"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -57,21 +58,49 @@
|
|||||||
>
|
>
|
||||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<div v-else class="contact-actions">
|
</div>
|
||||||
|
<div v-if="isAdmin">
|
||||||
<woot-button
|
<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"
|
class="new-message"
|
||||||
|
icon="ion-chatboxes"
|
||||||
size="small expanded"
|
size="small expanded"
|
||||||
@click="toggleConversationModal"
|
@click="toggleConversationModal"
|
||||||
>
|
/>
|
||||||
{{ $t('CONTACT_PANEL.NEW_MESSAGE') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
<woot-button
|
||||||
|
v-tooltip="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||||
|
class="edit-contact"
|
||||||
|
icon="ion-edit"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="small expanded"
|
size="small expanded"
|
||||||
@click="toggleEditModal"
|
@click="toggleEditModal"
|
||||||
>
|
/>
|
||||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
<woot-button
|
||||||
</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>
|
</div>
|
||||||
<edit-contact
|
<edit-contact
|
||||||
v-if="showEditModal"
|
v-if="showEditModal"
|
||||||
@@ -80,11 +109,24 @@
|
|||||||
@cancel="toggleEditModal"
|
@cancel="toggleEditModal"
|
||||||
/>
|
/>
|
||||||
<new-conversation
|
<new-conversation
|
||||||
|
v-if="contact.id"
|
||||||
:show="showConversationModal"
|
:show="showConversationModal"
|
||||||
:contact="contact"
|
:contact="contact"
|
||||||
@cancel="toggleConversationModal"
|
@cancel="toggleConversationModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@@ -93,6 +135,9 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
|||||||
import SocialIcons from './SocialIcons';
|
import SocialIcons from './SocialIcons';
|
||||||
import EditContact from './EditContact';
|
import EditContact from './EditContact';
|
||||||
import NewConversation from './NewConversation';
|
import NewConversation from './NewConversation';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import adminMixin from '../../../../mixins/isAdmin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -102,6 +147,7 @@ export default {
|
|||||||
SocialIcons,
|
SocialIcons,
|
||||||
NewConversation,
|
NewConversation,
|
||||||
},
|
},
|
||||||
|
mixins: [alertMixin, adminMixin],
|
||||||
props: {
|
props: {
|
||||||
contact: {
|
contact: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -120,9 +166,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
showConversationModal: false,
|
showConversationModal: false,
|
||||||
|
showDeleteModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
|
||||||
additionalAttributes() {
|
additionalAttributes() {
|
||||||
return this.contact.additional_attributes || {};
|
return this.contact.additional_attributes || {};
|
||||||
},
|
},
|
||||||
@@ -134,6 +182,23 @@ export default {
|
|||||||
|
|
||||||
return { twitter: twitterScreenName, ...(socialProfiles || {}) };
|
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: {
|
methods: {
|
||||||
toggleEditModal() {
|
toggleEditModal() {
|
||||||
@@ -142,6 +207,31 @@ export default {
|
|||||||
toggleConversationModal() {
|
toggleConversationModal() {
|
||||||
this.showConversationModal = !this.showConversationModal;
|
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>
|
</script>
|
||||||
@@ -179,17 +269,32 @@ export default {
|
|||||||
.contact-actions {
|
.contact-actions {
|
||||||
margin-top: var(--space-small);
|
margin-top: var(--space-small);
|
||||||
}
|
}
|
||||||
.button.edit-contact {
|
|
||||||
|
.edit-contact {
|
||||||
margin-left: var(--space-medium);
|
margin-left: var(--space-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.new-message {
|
.delete-contact {
|
||||||
margin-right: var(--space-small);
|
margin-left: var(--space-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-actions {
|
.contact-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export const mutations = {
|
|||||||
const conversations = $state.records[id] || [];
|
const conversations = $state.records[id] || [];
|
||||||
Vue.set($state.records, id, [...conversations, data]);
|
Vue.set($state.records, id, [...conversations, data]);
|
||||||
},
|
},
|
||||||
|
[types.default.DELETE_CONTACT_CONVERSATION]: ($state, id) => {
|
||||||
|
Vue.delete($state.records, id);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
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) => {
|
fetchContactableInbox: async ({ commit }, id) => {
|
||||||
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true });
|
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true });
|
||||||
try {
|
try {
|
||||||
@@ -110,4 +125,12 @@ export const actions = {
|
|||||||
setContact({ commit }, data) {
|
setContact({ commit }, data) {
|
||||||
commit(types.SET_CONTACT_ITEM, 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,
|
isFetchingItem: false,
|
||||||
isFetchingInboxes: false,
|
isFetchingInboxes: false,
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
|
isDeleting: false,
|
||||||
},
|
},
|
||||||
sortOrder: [],
|
sortOrder: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export const mutations = {
|
|||||||
Vue.set($state.records, data.id, data);
|
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) => {
|
[types.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
|
||||||
Object.values($state.records).forEach(element => {
|
Object.values($state.records).forEach(element => {
|
||||||
const availabilityStatus = data[element.id];
|
const availabilityStatus = data[element.id];
|
||||||
|
|||||||
@@ -177,6 +177,13 @@ export const mutations = {
|
|||||||
Vue.set(chat, 'can_reply', canReply);
|
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 {
|
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', () => {
|
describe('#setContact', () => {
|
||||||
it('returns correct mutations', () => {
|
it('returns correct mutations', () => {
|
||||||
const data = { id: 1, name: 'john doe', availability_status: 'online' };
|
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]]);
|
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',
|
CHANGE_CHAT_STATUS_FILTER: 'CHANGE_CHAT_STATUS_FILTER',
|
||||||
UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE',
|
UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE',
|
||||||
UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT',
|
UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT',
|
||||||
|
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
|
||||||
|
|
||||||
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
|
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
|
||||||
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
|
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
|
||||||
@@ -101,6 +102,7 @@ export default {
|
|||||||
SET_CONTACTS: 'SET_CONTACTS',
|
SET_CONTACTS: 'SET_CONTACTS',
|
||||||
CLEAR_CONTACTS: 'CLEAR_CONTACTS',
|
CLEAR_CONTACTS: 'CLEAR_CONTACTS',
|
||||||
EDIT_CONTACT: 'EDIT_CONTACT',
|
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||||
|
DELETE_CONTACT: 'DELETE_CONTACT',
|
||||||
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
@@ -119,6 +121,7 @@ export default {
|
|||||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||||
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
||||||
ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION',
|
ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION',
|
||||||
|
DELETE_CONTACT_CONVERSATION: 'DELETE_CONTACT_CONVERSATION',
|
||||||
|
|
||||||
// Contact Label
|
// Contact Label
|
||||||
SET_CONTACT_LABELS_UI_FLAG: 'SET_CONTACT_LABELS_UI_FLAG',
|
SET_CONTACT_LABELS_UI_FLAG: 'SET_CONTACT_LABELS_UI_FLAG',
|
||||||
|
|||||||
@@ -111,6 +111,13 @@ class ActionCableListener < BaseListener
|
|||||||
broadcast(account, tokens, CONTACT_MERGED, contact.push_event_data)
|
broadcast(account, tokens, CONTACT_MERGED, contact.push_event_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def contact_deleted(event)
|
||||||
|
contact, account = extract_contact_and_account(event)
|
||||||
|
tokens = user_tokens(account, account.agents)
|
||||||
|
|
||||||
|
broadcast(account, tokens, CONTACT_DELETED, contact.push_event_data)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def typing_event_listener_tokens(account, conversation, user)
|
def typing_event_listener_tokens(account, conversation, user)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class Contact < ApplicationRecord
|
|||||||
before_validation :prepare_email_attribute
|
before_validation :prepare_email_attribute
|
||||||
after_create_commit :dispatch_create_event, :ip_lookup
|
after_create_commit :dispatch_create_event, :ip_lookup
|
||||||
after_update_commit :dispatch_update_event
|
after_update_commit :dispatch_update_event
|
||||||
|
after_destroy_commit :dispatch_destroy_event
|
||||||
|
|
||||||
def get_source_id(inbox_id)
|
def get_source_id(inbox_id)
|
||||||
contact_inboxes.find_by!(inbox_id: inbox_id).source_id
|
contact_inboxes.find_by!(inbox_id: inbox_id).source_id
|
||||||
@@ -73,7 +74,8 @@ class Contact < ApplicationRecord
|
|||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
avatar: avatar_url,
|
avatar: avatar_url,
|
||||||
type: 'contact'
|
type: 'contact',
|
||||||
|
account: account.webhook_data
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -98,4 +100,8 @@ class Contact < ApplicationRecord
|
|||||||
def dispatch_update_event
|
def dispatch_update_event
|
||||||
Rails.configuration.dispatcher.dispatch(CONTACT_UPDATED, Time.zone.now, contact: self)
|
Rails.configuration.dispatcher.dispatch(CONTACT_UPDATED, Time.zone.now, contact: self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dispatch_destroy_event
|
||||||
|
Rails.configuration.dispatcher.dispatch(CONTACT_DELETED, Time.zone.now, contact: self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,4 +30,8 @@ class ContactPolicy < ApplicationPolicy
|
|||||||
def create?
|
def create?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ en:
|
|||||||
transcript_subject: "Conversation Transcript"
|
transcript_subject: "Conversation Transcript"
|
||||||
survey:
|
survey:
|
||||||
response: "Please rate this conversation, %{link}"
|
response: "Please rate this conversation, %{link}"
|
||||||
|
contacts:
|
||||||
|
online:
|
||||||
|
delete: "%{contact_name} is Online, please try again later"
|
||||||
integration_apps:
|
integration_apps:
|
||||||
slack:
|
slack:
|
||||||
name: "Slack"
|
name: "Slack"
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :contacts, only: [:index, :show, :update, :create] do
|
resources :contacts, only: [:index, :show, :update, :create, :destroy] do
|
||||||
collection do
|
collection do
|
||||||
get :active
|
get :active
|
||||||
get :search
|
get :search
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ module Events::Types
|
|||||||
CONTACT_CREATED = 'contact.created'
|
CONTACT_CREATED = 'contact.created'
|
||||||
CONTACT_UPDATED = 'contact.updated'
|
CONTACT_UPDATED = 'contact.updated'
|
||||||
CONTACT_MERGED = 'contact.merged'
|
CONTACT_MERGED = 'contact.merged'
|
||||||
|
CONTACT_DELETED = 'contact.deleted'
|
||||||
|
|
||||||
# agent events
|
# agent events
|
||||||
AGENT_ADDED = 'agent.added'
|
AGENT_ADDED = 'agent.added'
|
||||||
|
|||||||
@@ -376,4 +376,53 @@ RSpec.describe 'Contacts API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /api/v1/accounts/{account.id}/contacts/:id', :contact_delete do
|
||||||
|
let(:inbox) { create(:inbox, account: account) }
|
||||||
|
let(:contact) { create(:contact, account: account) }
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
|
||||||
|
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, contact_inbox: contact_inbox) }
|
||||||
|
|
||||||
|
context 'when it is an unauthenticated user' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated user' do
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
|
||||||
|
it 'deletes the contact for administrator user' do
|
||||||
|
allow(::OnlineStatusTracker).to receive(:get_presence).and_return(false)
|
||||||
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
||||||
|
headers: admin.create_new_auth_token
|
||||||
|
|
||||||
|
expect(contact.conversations).to be_empty
|
||||||
|
expect(contact.inboxes).to be_empty
|
||||||
|
expect(contact.contact_inboxes).to be_empty
|
||||||
|
expect(contact.csat_survey_responses).to be_empty
|
||||||
|
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not delete the contact if online' do
|
||||||
|
allow(::OnlineStatusTracker).to receive(:get_presence).and_return(true)
|
||||||
|
|
||||||
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
||||||
|
headers: admin.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unauthorized for agent user' do
|
||||||
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,4 +65,19 @@ describe ActionCableListener do
|
|||||||
listener.conversation_typing_off(event)
|
listener.conversation_typing_off(event)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#contact_deleted' do
|
||||||
|
let(:event_name) { :'contact.deleted' }
|
||||||
|
let!(:contact) { create(:contact, account: account) }
|
||||||
|
let!(:event) { Events::Base.new(event_name, Time.zone.now, contact: contact) }
|
||||||
|
|
||||||
|
it 'sends message to account admins, inbox agents' do
|
||||||
|
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||||
|
[agent.pubsub_token, admin.pubsub_token],
|
||||||
|
'contact.deleted',
|
||||||
|
contact.push_event_data.merge(account_id: account.id)
|
||||||
|
)
|
||||||
|
listener.contact_deleted(event)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,3 +48,22 @@ put:
|
|||||||
description: Contact not found
|
description: Contact not found
|
||||||
403:
|
403:
|
||||||
description: Access denied
|
description: Access denied
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Contact
|
||||||
|
operationId: contactDelete
|
||||||
|
summary: Delete Contact
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
type: number
|
||||||
|
description: ID of the contact
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Success
|
||||||
|
401:
|
||||||
|
description: Unauthorized
|
||||||
|
404:
|
||||||
|
description: Contact not found
|
||||||
@@ -1249,6 +1249,33 @@
|
|||||||
"description": "Access denied"
|
"description": "Access denied"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Contact"
|
||||||
|
],
|
||||||
|
"operationId": "contactDelete",
|
||||||
|
"summary": "Delete Contact",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"type": "number",
|
||||||
|
"description": "ID of the contact",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Contact not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/accounts/{account_id}/contacts/{id}/conversations": {
|
"/api/v1/accounts/{account_id}/contacts/{id}/conversations": {
|
||||||
|
|||||||
Reference in New Issue
Block a user