feat: Custom fields in pre-chat form (#4189)

This commit is contained in:
Muhsin Keloth
2022-04-19 12:47:29 +05:30
committed by GitHub
parent 1ccd29140d
commit 26f23a6e21
25 changed files with 824 additions and 160 deletions
+1 -4
View File
@@ -151,10 +151,7 @@ export default {
},
registerCampaignEvents() {
bus.$on(ON_CAMPAIGN_MESSAGE_CLICK, () => {
const showPreChatForm =
this.preChatFormEnabled && this.preChatFormOptions.requireEmail;
const isUserEmailAvailable = !!this.currentUser.email;
if (showPreChatForm && !isUserEmailAvailable) {
if (this.shouldShowPreChatForm) {
this.replaceRoute('prechat-form');
} else {
this.replaceRoute('messages');
+231 -77
View File
@@ -1,47 +1,44 @@
<template>
<form
<FormulateForm
v-model="formValues"
class="flex flex-1 flex-col p-6 overflow-y-auto"
@submit.prevent="onSubmit"
@submit="onSubmit"
>
<div
v-if="shouldShowHeaderMessage"
class="text-sm leading-5"
class="mb-4 text-sm leading-5"
:class="$dm('text-black-800', 'dark:text-slate-50')"
>
{{ headerMessage }}
</div>
<form-input
v-if="areContactFieldsVisible"
v-model="fullName"
class="mt-5"
:label="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.LABEL')"
:placeholder="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.PLACEHOLDER')"
type="text"
:error="
$v.fullName.$error ? $t('PRE_CHAT_FORM.FIELDS.FULL_NAME.ERROR') : ''
"
<FormulateInput
v-for="item in enabledPreChatFields"
:key="item.name"
:name="item.name"
:type="item.type"
:label="getLabel(item)"
:placeholder="getPlaceHolder(item)"
:validation="getValidation(item)"
:options="getOptions(item)"
:label-class="context => labelClass(context)"
:input-class="context => inputClass(context)"
:validation-messages="{
isPhoneE164OrEmpty: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'),
email: $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.VALID_ERROR'),
required: getRequiredErrorMessage(item),
}"
/>
<form-input
v-if="areContactFieldsVisible"
v-model="emailAddress"
class="mt-5"
:label="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.LABEL')"
:placeholder="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.PLACEHOLDER')"
type="email"
:error="
$v.emailAddress.$error
? $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.ERROR')
: ''
"
/>
<form-text-area
<FormulateInput
v-if="!hasActiveCampaign"
v-model="message"
class="my-5"
name="message"
type="textarea"
:label-class="context => labelClass(context)"
:input-class="context => inputClass(context)"
:label="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.LABEL')"
:placeholder="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.PLACEHOLDER')"
:error="$v.message.$error ? $t('PRE_CHAT_FORM.FIELDS.MESSAGE.ERROR') : ''"
validation="required"
/>
<custom-button
class="font-medium my-5"
block
@@ -52,24 +49,20 @@
<spinner v-if="isCreating" class="p-0" />
{{ $t('START_CONVERSATION') }}
</custom-button>
</form>
</FormulateForm>
</template>
<script>
import CustomButton from 'shared/components/Button';
import FormInput from '../Form/Input';
import FormTextArea from '../Form/TextArea';
import Spinner from 'shared/components/Spinner';
import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import { required, minLength, email } from 'vuelidate/lib/validators';
import { isEmptyObject } from 'widget/helpers/utils';
import routerMixin from 'widget/mixins/routerMixin';
import darkModeMixin from 'widget/mixins/darkModeMixin';
export default {
components: {
FormInput,
FormTextArea,
CustomButton,
Spinner,
},
@@ -77,47 +70,23 @@ export default {
props: {
options: {
type: Object,
default: () => ({}),
default: () => {},
},
disableContactFields: {
type: Boolean,
default: false,
},
},
validations() {
const identityValidations = {
fullName: {
required,
},
emailAddress: {
required,
email,
},
};
const messageValidation = {
message: {
required,
minLength: minLength(1),
},
};
// For campaign, message field is not required
if (this.hasActiveCampaign) {
return identityValidations;
}
if (this.areContactFieldsVisible) {
return {
...identityValidations,
...messageValidation,
};
}
return messageValidation;
},
data() {
return {
fullName: '',
emailAddress: '',
locale: this.$root.$i18n.locale,
message: '',
formValues: {},
labels: {
emailAddress: 'EMAIL_ADDRESS',
fullName: 'FULL_NAME',
phoneNumber: 'PHONE_NUMBER',
},
};
},
computed: {
@@ -125,6 +94,7 @@ export default {
widgetColor: 'appConfig/getWidgetColor',
isCreating: 'conversation/getIsCreating',
activeCampaign: 'campaign/getActiveCampaign',
currentUser: 'contacts/getCurrentUser',
}),
textColor() {
return getContrastingTextColor(this.widgetColor);
@@ -141,23 +111,207 @@ export default {
}
return this.options.preChatMessage;
},
areContactFieldsVisible() {
return this.options.requireEmail && !this.disableContactFields;
preChatFields() {
return this.disableContactFields ? [] : this.options.preChatFields;
},
filteredPreChatFields() {
const isUserEmailAvailable = !!this.currentUser.email;
const isUserPhoneNumberAvailable = !!this.currentUser.phone_number;
return this.preChatFields.filter(field => {
if (
(isUserEmailAvailable && field.name === 'emailAddress') ||
(isUserPhoneNumberAvailable && field.name === 'phoneNumber')
) {
return false;
}
return true;
});
},
enabledPreChatFields() {
return this.filteredPreChatFields
.filter(field => field.enabled)
.map(field => ({
...field,
type: this.findFieldType(field.type),
}));
},
conversationCustomAttributes() {
let conversationAttributes = {};
this.enabledPreChatFields.forEach(field => {
if (field.field_type === 'conversation_attribute') {
conversationAttributes = {
...conversationAttributes,
[field.name]: this.getValue(field),
};
}
});
return conversationAttributes;
},
contactCustomAttributes() {
let contactAttributes = {};
this.enabledPreChatFields.forEach(field => {
if (field.field_type === 'contact_attribute') {
contactAttributes = {
...contactAttributes,
[field.name]: this.getValue(field),
};
}
});
return contactAttributes;
},
inputStyles() {
return `mt-2 border rounded w-full py-2 px-3 text-slate-700 outline-none`;
},
isInputDarkOrLightMode() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
'text-slate-700',
'dark:text-slate-50'
)}`;
},
inputBorderColor() {
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
},
},
methods: {
onSubmit() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
labelClass(context) {
const { hasErrors } = context;
if (!hasErrors) {
return `text-xs font-medium ${this.$dm(
'text-black-800',
'dark:text-slate-50'
)}`;
}
return `text-xs font-medium ${this.$dm(
'text-red-400',
'dark:text-red-400'
)}`;
},
inputClass(context) {
const { hasErrors, classification, type } = context;
if (classification === 'box' && type === 'checkbox') {
return '';
}
if (!hasErrors) {
return `${this.inputStyles} hover:border-black-300 focus:border-black-300 ${this.isInputDarkOrLightMode} ${this.inputBorderColor}`;
}
return `${this.inputStyles} border-red-200 hover:border-red-300 focus:border-red-300 ${this.isInputDarkOrLightMode}`;
},
isContactFieldRequired(field) {
return this.preChatFields.find(option => option.name === field).required;
},
getLabel({ name, label }) {
if (this.labels[name])
return this.$t(`PRE_CHAT_FORM.FIELDS.${this.labels[name]}.LABEL`);
return label;
},
getPlaceHolder({ name, placeholder }) {
if (this.labels[name])
return this.$t(`PRE_CHAT_FORM.FIELDS.${this.labels[name]}.PLACEHOLDER`);
return placeholder;
},
getValue({ name, type }) {
if (type === 'select') {
return this.enabledPreChatFields.find(option => option.name === name)
.values[this.formValues[name]];
}
return this.formValues[name] || null;
},
getRequiredErrorMessage({ name, label }) {
if (this.labels[name])
return this.$t(
`PRE_CHAT_FORM.FIELDS.${this.labels[name]}.REQUIRED_ERROR`
);
return `${label} ${this.$t('PRE_CHAT_FORM.IS_REQUIRED')}`;
},
getValidation({ type, name }) {
if (!this.isContactFieldRequired(name)) {
return '';
}
const validations = {
emailAddress: 'email',
phoneNumber: 'isPhoneE164OrEmpty',
url: 'url',
date: 'date',
text: null,
select: null,
number: null,
};
const validationKeys = Object.keys(validations);
const validation = 'bail|required';
if (validationKeys.includes(name) || validationKeys.includes(type)) {
const validationType = validations[type] || validations[name];
return validationType ? `${validation}|${validationType}` : validation;
}
return '';
},
findFieldType(type) {
if (type === 'link') {
return 'url';
}
if (type === 'list') {
return 'select';
}
return type;
},
getOptions(item) {
if (item.type === 'select') {
let values = {};
item.values.forEach((value, index) => {
values = {
...values,
[index]: value,
};
});
return values;
}
return null;
},
onSubmit() {
const { emailAddress, fullName, phoneNumber, message } = this.formValues;
const { email } = this.currentUser;
this.$emit('submit', {
fullName: this.fullName,
emailAddress: this.emailAddress,
message: this.message,
fullName,
phoneNumber,
emailAddress: emailAddress || email,
message,
activeCampaignId: this.activeCampaign.id,
conversationCustomAttributes: this.conversationCustomAttributes,
contactCustomAttributes: this.contactCustomAttributes,
});
},
},
};
</script>
<style lang="scss" scoped>
::v-deep {
.wrapper[data-type='checkbox'] {
.formulate-input-wrapper {
display: flex;
align-items: center;
label {
margin-left: 0.2rem;
}
}
}
@media (prefers-color-scheme: dark) {
.wrapper {
.formulate-input-element--date,
.formulate-input-element--checkbox {
input {
color-scheme: dark;
}
}
}
}
.wrapper[data-type='textarea'] {
.formulate-input-element--textarea {
textarea {
min-height: 8rem;
}
}
}
}
</style>
+11 -3
View File
@@ -44,12 +44,19 @@
"FULL_NAME": {
"LABEL": "Full Name",
"PLACEHOLDER": "Please enter your full name",
"ERROR": "Full Name is required"
"REQUIRED_ERROR": "Full Name is required"
},
"EMAIL_ADDRESS": {
"LABEL": "Email Address",
"PLACEHOLDER": "Please enter your email address",
"ERROR": "Invalid email address"
"REQUIRED_ERROR": "Email Address is required",
"VALID_ERROR": "Please enter a valid email address"
},
"PHONE_NUMBER": {
"LABEL": "Phone Number",
"PLACEHOLDER": "Please enter your phone number",
"REQUIRED_ERROR": "Phone Number is required",
"VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555"
},
"MESSAGE": {
"LABEL": "Message",
@@ -57,7 +64,8 @@
"ERROR": "Message too short"
}
},
"CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation"
"CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation",
"IS_REQUIRED": "is required"
},
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
"CHAT_FORM": {
+9 -3
View File
@@ -25,15 +25,21 @@ export default {
return window.chatwootWebChannel.preChatFormEnabled;
},
preChatFormOptions() {
let requireEmail = false;
let preChatMessage = '';
const options = window.chatwootWebChannel.preChatFormOptions || {};
requireEmail = options.require_email;
preChatMessage = options.pre_chat_message;
const { pre_chat_fields: preChatFields = [] } = options;
return {
requireEmail,
preChatMessage,
preChatFields,
};
},
shouldShowPreChatForm() {
const { preChatFields } = this.preChatFormOptions;
// Check if at least one enabled field in pre-chat fields
const hasEnabledFields =
preChatFields.filter(field => field.enabled).length > 0;
return this.preChatFormEnabled && hasEnabledFields;
},
},
};
@@ -1,12 +1,30 @@
import { createWrapper } from '@vue/test-utils';
import configMixin from '../configMixin';
import Vue from 'vue';
const preChatFields = [
{
label: 'Email Id',
name: 'emailAddress',
type: 'email',
field_type: 'standard',
required: false,
enabled: false,
},
{
label: 'Full name',
name: 'fullName',
type: 'text',
field_type: 'standard',
required: true,
enabled: true,
},
];
global.chatwootWebChannel = {
avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: ['emoji_picker', 'attachments', 'end_conversation'],
preChatFormOptions: { require_email: false, pre_chat_message: '' },
preChatFormOptions: { pre_chat_fields: preChatFields, pre_chat_message: '' },
preChatFormEnabled: true,
};
global.chatwootWidgetDefaults = {
@@ -29,18 +47,22 @@ describe('configMixin', () => {
expect(wrapper.vm.hasAConnectedAgentBot).toBe(true);
expect(wrapper.vm.useInboxAvatarForBot).toBe(true);
expect(wrapper.vm.inboxAvatarUrl).toBe('https://test.url');
expect(wrapper.vm.channelConfig).toEqual({
avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: ['emoji_picker', 'attachments', 'end_conversation'],
preChatFormOptions: {
pre_chat_message: '',
require_email: false,
pre_chat_fields: preChatFields,
},
preChatFormEnabled: true,
});
expect(wrapper.vm.preChatFormOptions).toEqual({
requireEmail: false,
preChatMessage: '',
preChatFields: preChatFields,
});
expect(wrapper.vm.preChatFormEnabled).toEqual(true);
expect(wrapper.vm.shouldShowPreChatForm).toEqual(true);
});
});
+2 -6
View File
@@ -1,7 +1,7 @@
<template>
<div class="flex flex-1 flex-col justify-end">
<div class="flex flex-1 overflow-auto">
<!-- Load Converstion List Components Here -->
<!-- Load Conversation List Components Here -->
</div>
<team-availability
:available-agents="availableAgents"
@@ -40,16 +40,12 @@ export default {
availableAgents: 'agent/availableAgents',
activeCampaign: 'campaign/getActiveCampaign',
conversationSize: 'conversation/getConversationSize',
currentUser: 'contacts/getCurrentUser',
}),
},
methods: {
startConversation() {
const isUserEmailAvailable = !!this.currentUser.email;
if (this.preChatFormEnabled && !this.conversationSize) {
return this.replaceRoute('prechat-form', {
disableContactFields: isUserEmailAvailable,
});
return this.replaceRoute('prechat-form');
}
return this.replaceRoute('messages');
},
+19 -1
View File
@@ -12,6 +12,7 @@ import { mapGetters } from 'vuex';
import PreChatForm from '../components/PreChat/Form';
import configMixin from '../mixins/configMixin';
import routerMixin from '../mixins/routerMixin';
import { isEmptyObject } from 'widget/helpers/utils';
export default {
components: {
@@ -35,13 +36,22 @@ export default {
},
},
methods: {
onSubmit({ fullName, emailAddress, message, activeCampaignId }) {
onSubmit({
fullName,
emailAddress,
message,
activeCampaignId,
phoneNumber,
contactCustomAttributes,
conversationCustomAttributes,
}) {
if (activeCampaignId) {
bus.$emit('execute-campaign', activeCampaignId);
this.$store.dispatch('contacts/update', {
user: {
email: emailAddress,
name: fullName,
phone_number: phoneNumber,
},
});
} else {
@@ -49,8 +59,16 @@ export default {
fullName: fullName,
emailAddress: emailAddress,
message: message,
phoneNumber: phoneNumber,
customAttributes: conversationCustomAttributes,
});
}
if (!isEmptyObject(contactCustomAttributes)) {
this.$store.dispatch(
'contacts/setCustomAttributes',
contactCustomAttributes
);
}
},
},
};