feat: Create component to merge contacts (#2412)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
e11b3c4f17
commit
104ae8de2e
8 changed files with 369 additions and 6 deletions
|
@ -31,7 +31,4 @@ exclude_patterns:
|
|||
- "**/*.yml"
|
||||
- "app/javascript/dashboard/i18n/locale"
|
||||
- "**/*.stories.js"
|
||||
- "stories/**/*"
|
||||
- "**/*.stories.js"
|
||||
- "**/stories/"
|
||||
- "app/javascript/**/*.stories.js"
|
||||
- "stories/"
|
||||
|
|
|
@ -131,6 +131,7 @@
|
|||
},
|
||||
"CONTACTS_PAGE": {
|
||||
"HEADER": "Contacts",
|
||||
"FIELDS": "Contact fields",
|
||||
"SEARCH_BUTTON": "Search",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts",
|
||||
"LIST": {
|
||||
|
@ -204,5 +205,33 @@
|
|||
"PLACEHOLDER": "Eg: 11901 "
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
"ERROR": "ERROR_MESSAGE"
|
||||
},
|
||||
"FORM": {
|
||||
"SUBMIT": " Merge contacts",
|
||||
"CANCEL": "Cancel",
|
||||
"CHILD_CONTACT": {
|
||||
"ERROR": "Select a child contact to merge"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<div class="option-item--user">
|
||||
<thumbnail :src="thumbnail" size="24px" :username="name" />
|
||||
<span class="option__title">
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from '../../../components/widgets/Thumbnail';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
thumbnail: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.option-item--user {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,209 @@
|
|||
<template>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div class="merge-contacts">
|
||||
<div class="multiselect-wrap--small">
|
||||
<label class="multiselect__label">
|
||||
{{ $t('MERGE_CONTACTS.PRIMARY.TITLE') }}
|
||||
</label>
|
||||
<multiselect
|
||||
:value="primaryContact"
|
||||
disabled
|
||||
:options="[]"
|
||||
:show-labels="false"
|
||||
label="name"
|
||||
track-by="id"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="props">
|
||||
<contact-dropdown-item
|
||||
:thumbnail="props.option.thumbnail"
|
||||
:name="props.option.name"
|
||||
/>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
|
||||
<div class="child-contact-wrap">
|
||||
<div class="child-arrow">
|
||||
<i class="ion-ios-arrow-up up" />
|
||||
</div>
|
||||
<div
|
||||
class="child-contact multiselect-wrap--small"
|
||||
:class="{ error: $v.childContact.$error }"
|
||||
>
|
||||
<label class="multiselect__label">
|
||||
{{ $t('MERGE_CONTACTS.CHILD.TITLE') }}
|
||||
</label>
|
||||
<multiselect
|
||||
v-model="childContact"
|
||||
:options="searchResults"
|
||||
label="name"
|
||||
track-by="id"
|
||||
:internal-search="false"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:placeholder="$t('MERGE_CONTACTS.CHILD.PLACEHOLDER')"
|
||||
:allow-empty="true"
|
||||
:loading="isSearching"
|
||||
:max-height="150"
|
||||
open-direction="top"
|
||||
@search-change="searchChange"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="props">
|
||||
<contact-dropdown-item
|
||||
:thumbnail="props.option.thumbnail"
|
||||
:name="props.option.name"
|
||||
/>
|
||||
</template>
|
||||
<span slot="noResult">
|
||||
{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}
|
||||
</span>
|
||||
</multiselect>
|
||||
<span v-if="$v.childContact.$error" class="message">
|
||||
{{ $t('MERGE_CONTACTS.FORM.CHILD_CONTACT.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<merge-contact-summary
|
||||
:primary-contact-name="primaryContact.name"
|
||||
:child-contact-name="childContactName"
|
||||
/>
|
||||
<div class="footer">
|
||||
<woot-button variant="clear" @click.prevent="onCancel">
|
||||
{{ $t('MERGE_CONTACTS.FORM.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button type="submit" :is-loading="isMerging">
|
||||
{{ $t('MERGE_CONTACTS.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
|
||||
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary';
|
||||
import ContactDropdownItem from './ContactDropdownItem';
|
||||
|
||||
export default {
|
||||
components: { MergeContactSummary, ContactDropdownItem },
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
primaryContact: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isSearching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isMerging: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
searchResults: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
primaryContact: {
|
||||
required,
|
||||
},
|
||||
childContact: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
childContact: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
childContactName() {
|
||||
return this.childContact ? this.childContact.name : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
searchChange(query) {
|
||||
this.$emit('search', query);
|
||||
},
|
||||
onSubmit() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
this.$emit('submit', this.childContact.id);
|
||||
},
|
||||
onCancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.child-contact-wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.child-contact {
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.child-arrow {
|
||||
width: var(--space-larger);
|
||||
position: relative;
|
||||
font-size: var(--font-size-default);
|
||||
color: var(--color-border-dark);
|
||||
}
|
||||
.multiselect {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
.child-contact {
|
||||
margin-top: var(--space-smaller);
|
||||
}
|
||||
.child-arrow::after {
|
||||
content: '';
|
||||
height: var(--space-larger);
|
||||
width: 0;
|
||||
left: var(--space-two);
|
||||
position: absolute;
|
||||
border-left: 1px solid var(--color-border-dark);
|
||||
}
|
||||
|
||||
.child-arrow::before {
|
||||
content: '';
|
||||
height: 0;
|
||||
width: var(--space-normal);
|
||||
left: var(--space-two);
|
||||
top: var(--space-larger);
|
||||
position: absolute;
|
||||
border-bottom: 1px solid var(--color-border-dark);
|
||||
}
|
||||
|
||||
.up {
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
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;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* TDOD: Clean errors in forms style */
|
||||
.error .message {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<div v-if="childContactName" class="merge-summary callout">
|
||||
<h5 class="text-block-title">
|
||||
{{ $t('MERGE_CONTACTS.SUMMARY.TITLE') }}
|
||||
</h5>
|
||||
<ul class="summary-items">
|
||||
<li>
|
||||
<span>❌</span>
|
||||
<span
|
||||
v-html="
|
||||
$t('MERGE_CONTACTS.SUMMARY.DELETE_WARNING', {
|
||||
childContactName,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span>✅</span>
|
||||
<span
|
||||
v-html="
|
||||
$t('MERGE_CONTACTS.SUMMARY.ATTRIBUTE_WARNING', {
|
||||
childContactName,
|
||||
primaryContactName,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
primaryContactName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
childContactName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merge-summary {
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
|
||||
.summary-items {
|
||||
margin-left: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -10,7 +10,7 @@ const Template = (args, { argTypes }) => ({
|
|||
props: Object.keys(argTypes),
|
||||
components: { ContactIntro },
|
||||
template:
|
||||
'<contact-intro v-bind="$props" :user="user" @edit="onEdit" @message="onNewMessage" />',
|
||||
'<contact-intro v-bind="$props" @edit="onEdit" @message="onNewMessage" />',
|
||||
});
|
||||
|
||||
export const DefaultContactIntro = Template.bind({});
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import MergeContact from 'dashboard/modules/contact/components/MergeContact';
|
||||
|
||||
export default {
|
||||
title: 'Components/Contact/MergeContacts',
|
||||
component: MergeContact,
|
||||
argTypes: {
|
||||
'primary-contact': {
|
||||
defaultValue: '{}',
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { MergeContact },
|
||||
template:
|
||||
'<merge-contact v-bind="$props" @submit="onSearch" @search="onSubmit"></merge-contact>',
|
||||
});
|
||||
|
||||
export const List = Template.bind({});
|
||||
List.args = {
|
||||
primaryContact: {
|
||||
id: 12,
|
||||
name: 'Mason Mount',
|
||||
},
|
||||
onSearch: action('Search'),
|
||||
onSubmit: action('Submit'),
|
||||
};
|
|
@ -359,7 +359,7 @@ export default {
|
|||
margin-bottom: var(--space-normal);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
.multiselect-wrap--small {
|
||||
.conversation--actions .multiselect-wrap--small {
|
||||
.multiselect {
|
||||
padding-left: var(--space-medium);
|
||||
box-sizing: border-box;
|
||||
|
|
Loading…
Reference in a new issue