diff --git a/app/actions/contact_merge_action.rb b/app/actions/contact_merge_action.rb index f20e2ffb4..d31b6a367 100644 --- a/app/actions/contact_merge_action.rb +++ b/app/actions/contact_merge_action.rb @@ -42,7 +42,7 @@ class ContactMergeAction end def merge_and_remove_mergee_contact - mergable_attribute_keys = %w[identifier name email phone_number custom_attributes] + mergable_attribute_keys = %w[identifier name email phone_number additional_attributes custom_attributes] base_contact_attributes = base_contact.attributes.slice(*mergable_attribute_keys).compact_blank mergee_contact_attributes = mergee_contact.attributes.slice(*mergable_attribute_keys).compact_blank diff --git a/app/javascript/dashboard/api/accountActions.js b/app/javascript/dashboard/api/accountActions.js new file mode 100644 index 000000000..d8c46fe0a --- /dev/null +++ b/app/javascript/dashboard/api/accountActions.js @@ -0,0 +1,18 @@ +/* global axios */ + +import ApiClient from './ApiClient'; + +class AccountActions extends ApiClient { + constructor() { + super('actions', { accountScoped: true }); + } + + merge(parentId, childId) { + return axios.post(`${this.url}/contact_merge`, { + base_contact_id: parentId, + mergee_contact_id: childId, + }); + } +} + +export default new AccountActions(); diff --git a/app/javascript/dashboard/api/specs/accountActions.spec.js b/app/javascript/dashboard/api/specs/accountActions.spec.js new file mode 100644 index 000000000..45f9663f3 --- /dev/null +++ b/app/javascript/dashboard/api/specs/accountActions.spec.js @@ -0,0 +1,23 @@ +import accountActionsAPI from '../accountActions'; +import ApiClient from '../ApiClient'; +import describeWithAPIMock from './apiSpecHelper'; + +describe('#ContactsAPI', () => { + it('creates correct instance', () => { + expect(accountActionsAPI).toBeInstanceOf(ApiClient); + expect(accountActionsAPI).toHaveProperty('merge'); + }); + + describeWithAPIMock('API calls', context => { + it('#merge', () => { + accountActionsAPI.merge(1, 2); + expect(context.axiosMock.post).toHaveBeenCalledWith( + '/api/v1/actions/contact_merge', + { + base_contact_id: 1, + mergee_contact_id: 2, + } + ); + }); + }); +}); diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 7ae877045..e97792b97 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -15,6 +15,10 @@ .multiselect { margin-bottom: var(--space-normal); + &.multiselect--disabled { + opacity: .8; + } + .multiselect--active { >.multiselect__tags { border-color: $color-woot; @@ -209,3 +213,53 @@ flex-shrink: 0; } } + +.multiselect-wrap--medium { + $multiselect-height: 4.8rem; + + .multiselect__tags, + .multiselect__input { + align-items: center; + display: flex; + } + + .multiselect__tags, + .multiselect__input, + .multiselect { + background: var(--white); + font-size: var(--font-size-small); + height: $multiselect-height; + min-height: $multiselect-height; + } + + .multiselect__input { + height: $multiselect-height - $space-micro; + min-height: $multiselect-height - $space-micro; + } + + .multiselect__single { + align-items: center; + display: flex; + font-size: var(--font-size-small); + margin: 0; + padding: var(--space-smaller) var(--space-micro); + } + + .multiselect__placeholder { + margin: 0; + padding: var(--space-smaller) var(--space-micro); + } + + .multiselect__select { + min-height: $multiselect-height; + } + + .multiselect--disabled .multiselect__current, + .multiselect--disabled .multiselect__select { + background: transparent; + } + + .multiselect__tags-wrap { + flex-shrink: 0; + } +} diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 44f241e79..0f2096b14 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -32,6 +32,8 @@ "NO_RESULT": "No labels found" } }, + "MERGE_CONTACT": "Merge contact", + "CONTACT_ACTIONS": "Contact actions", "MUTE_CONTACT": "Mute Conversation", "UNMUTE_CONTACT": "Unmute Conversation", "MUTED_SUCCESS": "This conversation is muted for 6 hours", @@ -242,17 +244,19 @@ }, "MERGE_CONTACTS": { "TITLE": "Merge contacts", - "DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.", + "DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact’ s attributes will take precedence.", "PRIMARY": { - "TITLE": "Primary contact" + "TITLE": "Primary contact", + "HELP_LABEL": "To be kept" }, "CHILD": { "TITLE": "Contact to merge", - "PLACEHOLDER": "Choose a contact" + "PLACEHOLDER": "Search for a contact", + "HELP_LABEL": "To be deleted" }, "SUMMARY": { "TITLE": "Summary", - "DELETE_WARNING": "Contact of %{childContactName}will be deleted.", + "DELETE_WARNING": "Contact of %{childContactName} will be deleted.", "ATTRIBUTE_WARNING": "Contact details of %{childContactName} will be copied to %{primaryContactName}." }, "SEARCH": { @@ -265,7 +269,7 @@ "ERROR": "Select a child contact to merge" }, "SUCCESS_MESSAGE": "Contact merged successfully", - "ERROR_MESSAGE": "Could not merge contcts, try again!" + "ERROR_MESSAGE": "Could not merge contacts, try again!" } } -} \ No newline at end of file +} diff --git a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue new file mode 100644 index 000000000..bb318957d --- /dev/null +++ b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue @@ -0,0 +1,89 @@ + + + + diff --git a/app/javascript/dashboard/modules/contact/components/ContactDropdownItem.vue b/app/javascript/dashboard/modules/contact/components/ContactDropdownItem.vue index 3263f57b6..c4467c75f 100644 --- a/app/javascript/dashboard/modules/contact/components/ContactDropdownItem.vue +++ b/app/javascript/dashboard/modules/contact/components/ContactDropdownItem.vue @@ -1,9 +1,24 @@ @@ -23,6 +38,18 @@ export default { type: String, default: '', }, + email: { + type: String, + default: '', + }, + phoneNumber: { + type: String, + default: '', + }, + identifier: { + type: String, + default: '', + }, }, }; @@ -30,5 +57,49 @@ export default { diff --git a/app/javascript/dashboard/modules/contact/components/ContactFields.vue b/app/javascript/dashboard/modules/contact/components/ContactFields.vue index 3f3b75a7d..cf58b9a32 100644 --- a/app/javascript/dashboard/modules/contact/components/ContactFields.vue +++ b/app/javascript/dashboard/modules/contact/components/ContactFields.vue @@ -1,6 +1,6 @@ @@ -27,11 +36,17 @@
+ + @@ -190,12 +217,6 @@ export default { left: var(--space-normal); } -::v-deep .multiselect__tags .option__title { - display: inline-flex; - align-items: center; - margin-left: var(--space-small); -} - .footer { margin-top: var(--space-medium); display: flex; @@ -206,4 +227,8 @@ export default { .error .message { margin-top: 0; } + +.label--merge-warning { + margin-left: var(--space-small); +} diff --git a/app/javascript/dashboard/modules/contact/components/MergeContactSummary.vue b/app/javascript/dashboard/modules/contact/components/MergeContactSummary.vue index 5d028bd6a..1f238e509 100644 --- a/app/javascript/dashboard/modules/contact/components/MergeContactSummary.vue +++ b/app/javascript/dashboard/modules/contact/components/MergeContactSummary.vue @@ -5,7 +5,7 @@
  • - + -
    - - {{ $t('EDIT_CONTACT.BUTTON_LABEL') }} - -
    -
    - - {{ $t('DELETE_CONTACT.BUTTON_LABEL') }} - -
    -
-
-
- - - -
+
+ + + +
+
diff --git a/app/javascript/dashboard/store/modules/contacts/actions.js b/app/javascript/dashboard/store/modules/contacts/actions.js index 96a7c811a..801157bb4 100644 --- a/app/javascript/dashboard/store/modules/contacts/actions.js +++ b/app/javascript/dashboard/store/modules/contacts/actions.js @@ -4,6 +4,7 @@ import { } from 'shared/helpers/CustomErrors'; import types from '../../mutation-types'; import ContactAPI from '../../../api/contacts'; +import AccountActionsAPI from '../../../api/accountActions'; export const actions = { search: async ({ commit }, { search, page, sortAttr, label }) => { @@ -137,6 +138,18 @@ export const actions = { commit(types.SET_CONTACT_ITEM, data); }, + merge: async ({ commit }, { childId, parentId }) => { + commit(types.SET_CONTACT_UI_FLAG, { isMerging: true }); + try { + const response = await AccountActionsAPI.merge(parentId, childId); + commit(types.SET_CONTACT_ITEM, response.data); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_CONTACT_UI_FLAG, { isMerging: false }); + } + }, + deleteContactThroughConversations: ({ commit }, id) => { commit(types.DELETE_CONTACT, id); commit(types.CLEAR_CONTACT_CONVERSATIONS, id, { root: true }); diff --git a/app/javascript/dashboard/store/modules/contacts/index.js b/app/javascript/dashboard/store/modules/contacts/index.js index f1982e4be..b2657ca78 100644 --- a/app/javascript/dashboard/store/modules/contacts/index.js +++ b/app/javascript/dashboard/store/modules/contacts/index.js @@ -13,6 +13,7 @@ const state = { isFetchingItem: false, isFetchingInboxes: false, isUpdating: false, + isMerging: false, isDeleting: false, }, sortOrder: [], diff --git a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js index 03d49d8d0..dc49f4731 100644 --- a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js @@ -168,6 +168,30 @@ describe('#actions', () => { }); }); + describe('#merge', () => { + it('sends correct mutations if API is success', async () => { + axios.post.mockResolvedValue({ + data: contactList[0], + }); + await actions.merge({ commit }, { childId: 0, parentId: 1 }); + expect(commit.mock.calls).toEqual([ + [types.SET_CONTACT_UI_FLAG, { isMerging: true }], + [types.SET_CONTACT_ITEM, contactList[0]], + [types.SET_CONTACT_UI_FLAG, { isMerging: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.merge({ commit }, { childId: 0, parentId: 1 }) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.SET_CONTACT_UI_FLAG, { isMerging: true }], + [types.SET_CONTACT_UI_FLAG, { isMerging: false }], + ]); + }); + }); + describe('#deleteContactThroughConversations', () => { it('returns correct mutations', () => { actions.deleteContactThroughConversations({ commit }, contactList[0].id);