feat: Add contact merge form component (#10478)
Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
|||||||
|
<script setup>
|
||||||
|
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||||
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
selectedContact: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
primaryContactId: {
|
||||||
|
type: [Number, null],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
primaryContactList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
isSearching: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
hasError: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:primaryContactId', 'search']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between h-5 gap-2">
|
||||||
|
<label class="text-sm text-n-slate-12">
|
||||||
|
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PRIMARY') }}
|
||||||
|
</label>
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center w-24 h-5 text-xs rounded-md text-n-teal-11 bg-n-alpha-2"
|
||||||
|
>
|
||||||
|
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PRIMARY_HELP_LABEL') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ComboBox
|
||||||
|
id="inbox"
|
||||||
|
:model-value="primaryContactId"
|
||||||
|
:options="primaryContactList"
|
||||||
|
:empty-state="
|
||||||
|
isSearching
|
||||||
|
? t('CONTACTS_LAYOUT.SIDEBAR.MERGE.IS_SEARCHING')
|
||||||
|
: t('CONTACTS_LAYOUT.SIDEBAR.MERGE.EMPTY_STATE')
|
||||||
|
"
|
||||||
|
:search-placeholder="
|
||||||
|
t('CONTACTS_LAYOUT.SIDEBAR.MERGE.SEARCH_PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:placeholder="t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PLACEHOLDER')"
|
||||||
|
:has-error="hasError"
|
||||||
|
:message="errorMessage"
|
||||||
|
class="[&>div>button]:bg-n-alpha-black2"
|
||||||
|
@update:model-value="value => emit('update:primaryContactId', value)"
|
||||||
|
@search="query => emit('search', query)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center gap-2 top-4">
|
||||||
|
<div v-for="i in 3" :key="i" class="relative w-4 h-8">
|
||||||
|
<div
|
||||||
|
class="absolute w-0 h-0 border-l-[4px] border-r-[4px] border-b-[6px] border-l-transparent border-r-transparent border-n-strong ltr:translate-x-[4px] rtl:-translate-x-[4px] -translate-y-[4px]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute w-[1px] h-full bg-n-strong left-1/2 transform -translate-x-1/2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between h-5 gap-2">
|
||||||
|
<label class="text-sm text-n-slate-12">
|
||||||
|
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PARENT') }}
|
||||||
|
</label>
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center w-24 h-5 text-xs rounded-md text-n-ruby-11 bg-n-alpha-2"
|
||||||
|
>
|
||||||
|
{{ t('CONTACTS_LAYOUT.SIDEBAR.MERGE.PARENT_HELP_LABEL') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="border border-n-strong h-[60px] gap-2 flex items-center rounded-xl p-3"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
:name="selectedContact.name || ''"
|
||||||
|
:src="selectedContact.thumbnail || ''"
|
||||||
|
:size="32"
|
||||||
|
rounded-full
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-sm leading-4 truncate text-n-slate-11">
|
||||||
|
{{ selectedContact.name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm leading-4 truncate text-n-slate-11">
|
||||||
|
{{ selectedContact.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup>
|
||||||
|
import ContactMergeForm from '../ContactMergeForm.vue';
|
||||||
|
import { contactData, primaryContactList } from './fixtures';
|
||||||
|
|
||||||
|
const handleSearch = query => {
|
||||||
|
console.log('Searching for:', query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = value => {
|
||||||
|
console.log('Primary contact updated:', value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/Contacts/ContactMergeForm"
|
||||||
|
:layout="{ type: 'grid', width: '600px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Default">
|
||||||
|
<div class="p-6 border rounded-lg border-n-strong">
|
||||||
|
<ContactMergeForm
|
||||||
|
:selected-contact="contactData"
|
||||||
|
:primary-contact-list="primaryContactList"
|
||||||
|
:primary-contact-id="null"
|
||||||
|
:is-searching="false"
|
||||||
|
@update:primary-contact-id="handleUpdate"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="With Selected Primary Contact">
|
||||||
|
<div class="p-6 border rounded-lg border-n-strong">
|
||||||
|
<ContactMergeForm
|
||||||
|
:selected-contact="contactData"
|
||||||
|
:primary-contact-list="primaryContactList"
|
||||||
|
:primary-contact-id="1"
|
||||||
|
:is-searching="false"
|
||||||
|
@update:primary-contact-id="handleUpdate"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Error State">
|
||||||
|
<div class="p-6 border rounded-lg border-n-strong">
|
||||||
|
<ContactMergeForm
|
||||||
|
:selected-contact="contactData"
|
||||||
|
:primary-contact-list="primaryContactList"
|
||||||
|
:primary-contact-id="null"
|
||||||
|
:is-searching="false"
|
||||||
|
has-error
|
||||||
|
error-message="Please select a primary contact"
|
||||||
|
@update:primary-contact-id="handleUpdate"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Empty Primary Contact List">
|
||||||
|
<div class="p-6 border rounded-lg border-n-strong">
|
||||||
|
<ContactMergeForm
|
||||||
|
:selected-contact="contactData"
|
||||||
|
:primary-contact-list="[]"
|
||||||
|
:primary-contact-id="null"
|
||||||
|
:is-searching="false"
|
||||||
|
@update:primary-contact-id="handleUpdate"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ContactsForm from '../ContactsForm.vue';
|
import ContactsForm from '../ContactsForm.vue';
|
||||||
import contactData from './fixtures';
|
import { contactData } from './fixtures';
|
||||||
|
|
||||||
const handleUpdate = updatedData => {
|
const handleUpdate = updatedData => {
|
||||||
console.log('Form updated:', updatedData);
|
console.log('Form updated:', updatedData);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default {
|
export const contactData = {
|
||||||
id: 370,
|
id: 370,
|
||||||
name: 'John Doe',
|
name: 'John Doe',
|
||||||
email: 'johndoe@chatwoot.com',
|
email: 'johndoe@chatwoot.com',
|
||||||
@@ -18,3 +18,30 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const primaryContactList = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Jane Smith',
|
||||||
|
email: 'jane@chatwoot.com',
|
||||||
|
thumbnail: '',
|
||||||
|
label: '(ID: 1) Jane Smith',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Mike Johnson',
|
||||||
|
email: 'mike@chatwoot.com',
|
||||||
|
thumbnail: '',
|
||||||
|
label: '(ID: 2) Mike Johnson',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Sarah Wilson',
|
||||||
|
email: 'sarah@chatwoot.com',
|
||||||
|
thumbnail: '',
|
||||||
|
label: '(ID: 3) Sarah Wilson',
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -442,6 +442,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SIDEBAR": {
|
"SIDEBAR": {
|
||||||
|
"MERGE": {
|
||||||
|
"TITLE": "Merge contact",
|
||||||
|
"DESCRIPTION": "Combine two profiles into one, including all attributes and conversations. In case of conflict, the primary contact’s attributes will take precedence.",
|
||||||
|
"PRIMARY": "Primary contact",
|
||||||
|
"PRIMARY_HELP_LABEL": "To be saved",
|
||||||
|
"PRIMARY_REQUIRED_ERROR": "Please select a contact to merge with before proceeding",
|
||||||
|
"PARENT": "To be merged",
|
||||||
|
"PARENT_HELP_LABEL": "To be deleted",
|
||||||
|
"EMPTY_STATE": "No contacts found",
|
||||||
|
"PLACEHOLDER": "Search for primary contact",
|
||||||
|
"SEARCH_PLACEHOLDER": "Search for a contact",
|
||||||
|
"SEARCH_ERROR_MESSAGE": "Could not search for contacts. Please try again later.",
|
||||||
|
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||||
|
"ERROR_MESSAGE": "Could not merge contacts, try again!",
|
||||||
|
"IS_SEARCHING": "Searching...",
|
||||||
|
"BUTTONS": {
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"CONFIRM": "Merge contact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"NOTES": {
|
"NOTES": {
|
||||||
"PLACEHOLDER": "Add a note",
|
"PLACEHOLDER": "Add a note",
|
||||||
"WROTE": "wrote",
|
"WROTE": "wrote",
|
||||||
|
|||||||
Reference in New Issue
Block a user