feat: Create component to merge contacts (#2412)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Nithin David Thomas 2021-07-06 13:02:37 +05:30 committed by GitHub
parent e11b3c4f17
commit 104ae8de2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 369 additions and 6 deletions

View file

@ -31,7 +31,4 @@ exclude_patterns:
- "**/*.yml"
- "app/javascript/dashboard/i18n/locale"
- "**/*.stories.js"
- "stories/**/*"
- "**/*.stories.js"
- "**/stories/"
- "app/javascript/**/*.stories.js"
- "stories/"

View file

@ -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!"
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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