feat: Show pre-chat form before triggering the campaign (#3215)

This commit is contained in:
Muhsin Keloth 2021-11-11 19:02:16 +05:30 committed by GitHub
parent 76370267f3
commit c6326993df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 175 additions and 26 deletions

View file

@ -8,6 +8,7 @@
:is-left-aligned="isLeftAligned" :is-left-aligned="isLeftAligned"
:hide-message-bubble="hideMessageBubble" :hide-message-bubble="hideMessageBubble"
:show-popout-button="showPopoutButton" :show-popout-button="showPopoutButton"
:is-campaign-view-clicked="isCampaignViewClicked"
/> />
</template> </template>
@ -15,18 +16,19 @@
import { mapGetters, mapActions, mapMutations } from 'vuex'; import { mapGetters, mapActions, mapMutations } from 'vuex';
import { setHeader } from 'widget/helpers/axios'; import { setHeader } from 'widget/helpers/axios';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils'; import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import configMixin from './mixins/configMixin';
import availabilityMixin from 'widget/mixins/availability';
import Router from './views/Router'; import Router from './views/Router';
import { getLocale } from './helpers/urlParamsHelper'; import { getLocale } from './helpers/urlParamsHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { isEmptyObject } from 'widget/helpers/utils'; import { isEmptyObject } from 'widget/helpers/utils';
import availabilityMixin from 'widget/mixins/availability';
export default { export default {
name: 'App', name: 'App',
components: { components: {
Router, Router,
}, },
mixins: [availabilityMixin], mixins: [availabilityMixin, configMixin],
data() { data() {
return { return {
showUnreadView: false, showUnreadView: false,
@ -36,6 +38,7 @@ export default {
widgetPosition: 'right', widgetPosition: 'right',
showPopoutButton: false, showPopoutButton: false,
isWebWidgetTriggered: false, isWebWidgetTriggered: false,
isCampaignViewClicked: false,
isWidgetOpen: false, isWidgetOpen: false,
}; };
}, },
@ -98,7 +101,11 @@ export default {
methods: { methods: {
...mapActions('appConfig', ['setWidgetColor']), ...mapActions('appConfig', ['setWidgetColor']),
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']), ...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
...mapActions('campaign', ['initCampaigns', 'executeCampaign']), ...mapActions('campaign', [
'initCampaigns',
'executeCampaign',
'resetCampaign',
]),
...mapActions('agent', ['fetchAvailableAgents']), ...mapActions('agent', ['fetchAvailableAgents']),
...mapMutations('events', ['toggleOpen']), ...mapMutations('events', ['toggleOpen']),
scrollConversationToBottom() { scrollConversationToBottom() {
@ -147,15 +154,25 @@ export default {
}); });
}, },
registerCampaignEvents() { registerCampaignEvents() {
bus.$on('on-campaign-view-clicked', campaignId => { bus.$on('on-campaign-view-clicked', () => {
const { websiteToken } = window.chatwootWebChannel; this.isCampaignViewClicked = true;
this.showCampaignView = false; this.showCampaignView = false;
this.showUnreadView = false; this.showUnreadView = false;
this.unsetUnreadView(); this.unsetUnreadView();
this.setUserLastSeen(); this.setUserLastSeen();
// Execute campaign only if pre-chat form (and require email too) is not enabled
if (
!(this.preChatFormEnabled && this.preChatFormOptions.requireEmail)
) {
bus.$emit('execute-campaign', this.activeCampaign.id);
}
});
bus.$on('execute-campaign', campaignId => {
const { websiteToken } = window.chatwootWebChannel;
this.executeCampaign({ campaignId, websiteToken }); this.executeCampaign({ campaignId, websiteToken });
}); });
}, },
setPopoutDisplay(showPopoutButton) { setPopoutDisplay(showPopoutButton) {
this.showPopoutButton = showPopoutButton; this.showPopoutButton = showPopoutButton;
}, },
@ -255,6 +272,10 @@ export default {
this.showUnreadView = true; this.showUnreadView = true;
this.showCampaignView = false; this.showCampaignView = false;
} else if (message.event === 'unset-unread-view') { } else if (message.event === 'unset-unread-view') {
// Reset campaign, If widget opened via clciking on bubble button
if (!this.isCampaignViewClicked) {
this.resetCampaign();
}
this.showUnreadView = false; this.showUnreadView = false;
this.showCampaignView = false; this.showCampaignView = false;
} else if (message.event === 'toggle-open') { } else if (message.event === 'toggle-open') {

View file

@ -3,8 +3,11 @@
class="flex flex-1 flex-col p-6 overflow-y-auto" class="flex flex-1 flex-col p-6 overflow-y-auto"
@submit.prevent="onSubmit" @submit.prevent="onSubmit"
> >
<div v-if="options.preChatMessage" class="text-black-800 text-sm leading-5"> <div
{{ options.preChatMessage }} v-if="shouldShowHeaderMessage"
class="text-black-800 text-sm leading-5"
>
{{ headerMessage }}
</div> </div>
<form-input <form-input
v-if="options.requireEmail" v-if="options.requireEmail"
@ -31,6 +34,7 @@
" "
/> />
<form-text-area <form-text-area
v-if="!activeCampaignExist"
v-model="message" v-model="message"
class="my-5" class="my-5"
:label="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.LABEL')" :label="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.LABEL')"
@ -38,7 +42,7 @@
:error="$v.message.$error ? $t('PRE_CHAT_FORM.FIELDS.MESSAGE.ERROR') : ''" :error="$v.message.$error ? $t('PRE_CHAT_FORM.FIELDS.MESSAGE.ERROR') : ''"
/> />
<custom-button <custom-button
class="font-medium" class="font-medium my-5"
block block
:bg-color="widgetColor" :bg-color="widgetColor"
:text-color="textColor" :text-color="textColor"
@ -58,6 +62,8 @@ import Spinner from 'shared/components/Spinner';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils'; import { getContrastingTextColor } from '@chatwoot/utils';
import { required, minLength, email } from 'vuelidate/lib/validators'; import { required, minLength, email } from 'vuelidate/lib/validators';
import { isEmptyObject } from 'widget/helpers/utils';
export default { export default {
components: { components: {
FormInput, FormInput,
@ -88,6 +94,10 @@ export default {
minLength: minLength(1), minLength: minLength(1),
}, },
}; };
// For campaign, message field is not required
if (this.activeCampaignExist) {
return identityValidations;
}
if (this.options.requireEmail) { if (this.options.requireEmail) {
return { return {
...identityValidations, ...identityValidations,
@ -107,10 +117,23 @@ export default {
...mapGetters({ ...mapGetters({
widgetColor: 'appConfig/getWidgetColor', widgetColor: 'appConfig/getWidgetColor',
isCreating: 'conversation/getIsCreating', isCreating: 'conversation/getIsCreating',
activeCampaign: 'campaign/getActiveCampaign',
}), }),
textColor() { textColor() {
return getContrastingTextColor(this.widgetColor); return getContrastingTextColor(this.widgetColor);
}, },
activeCampaignExist() {
return !isEmptyObject(this.activeCampaign);
},
shouldShowHeaderMessage() {
return this.activeCampaignExist || this.options.preChatMessage;
},
headerMessage() {
if (this.activeCampaignExist) {
return this.$t('PRE_CHAT_FORM.CAMPAIGN_HEADER');
}
return this.options.preChatMessage;
},
}, },
methods: { methods: {
onSubmit() { onSubmit() {
@ -118,11 +141,22 @@ export default {
if (this.$v.$invalid) { if (this.$v.$invalid) {
return; return;
} }
this.$store.dispatch('conversation/createConversation', { // Check any active campaign exist or not
fullName: this.fullName, if (this.activeCampaignExist) {
emailAddress: this.emailAddress, bus.$emit('execute-campaign', this.activeCampaign.id);
message: this.message, this.$store.dispatch('contacts/update', {
}); user: {
email: this.emailAddress,
name: this.fullName,
},
});
} else {
this.$store.dispatch('conversation/createConversation', {
fullName: this.fullName,
emailAddress: this.emailAddress,
message: this.message,
});
}
}, },
}, },
}; };

View file

@ -50,7 +50,8 @@
"PLACEHOLDER": "Please enter your message", "PLACEHOLDER": "Please enter your message",
"ERROR": "Message too short" "ERROR": "Message too short"
} }
} },
"CAMPAIGN_HEADER": "Please provide your name and email before starting the conversation"
}, },
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
"CHAT_FORM": { "CHAT_FORM": {

View file

@ -12,6 +12,7 @@ const state = {
hasFetched: false, hasFetched: false,
}, },
activeCampaign: {}, activeCampaign: {},
campaignHasExecuted: false,
}; };
const resetCampaignTimers = ( const resetCampaignTimers = (
@ -34,6 +35,7 @@ export const getters = {
getHasFetched: $state => $state.uiFlags.hasFetched, getHasFetched: $state => $state.uiFlags.hasFetched,
getCampaigns: $state => $state.records, getCampaigns: $state => $state.records,
getActiveCampaign: $state => $state.activeCampaign, getActiveCampaign: $state => $state.activeCampaign,
getCampaignHasExecuted: $state => $state.campaignHasExecuted,
}; };
export const actions = { export const actions = {
@ -76,17 +78,37 @@ export const actions = {
); );
} }
}, },
startCampaign: async ({ commit }, { websiteToken, campaignId }) => { startCampaign: async (
const { data: campaigns } = await getCampaigns(websiteToken); {
const campaign = campaigns.find(item => item.id === campaignId); commit,
if (campaign) { rootState: {
commit('setActiveCampaign', campaign); events: { isOpen },
},
},
{ websiteToken, campaignId }
) => {
// Disable campaign execution if widget is opened
if (!isOpen) {
const { data: campaigns } = await getCampaigns(websiteToken);
// Check campaign is disabled or not
const campaign = campaigns.find(item => item.id === campaignId);
if (campaign) {
commit('setActiveCampaign', campaign);
}
} }
}, },
executeCampaign: async ({ commit }, { campaignId, websiteToken }) => { executeCampaign: async ({ commit }, { campaignId, websiteToken }) => {
try { try {
await triggerCampaign({ campaignId, websiteToken }); await triggerCampaign({ campaignId, websiteToken });
commit('setCampaignExecuted');
commit('setActiveCampaign', {});
} catch (error) {
commit('setError', true);
}
},
resetCampaign: async ({ commit }) => {
try {
commit('setActiveCampaign', {}); commit('setActiveCampaign', {});
} catch (error) { } catch (error) {
commit('setError', true); commit('setError', true);
@ -107,6 +129,9 @@ export const mutations = {
setHasFetched($state, value) { setHasFetched($state, value) {
Vue.set($state.uiFlags, 'hasFetched', value); Vue.set($state.uiFlags, 'hasFetched', value);
}, },
setCampaignExecuted($state) {
Vue.set($state, 'campaignHasExecuted', true);
},
}; };
export default { export default {

View file

@ -94,14 +94,28 @@ describe('#actions', () => {
it('reset campaign if campaign id is not present in the campaign list', async () => { it('reset campaign if campaign id is not present in the campaign list', async () => {
API.get.mockResolvedValue({ data: campaigns }); API.get.mockResolvedValue({ data: campaigns });
await actions.startCampaign( await actions.startCampaign(
{ dispatch, getters: { getCampaigns: campaigns }, commit }, {
dispatch,
getters: { getCampaigns: campaigns },
commit,
rootState: {
events: { isOpen: true },
},
},
{ campaignId: 32 } { campaignId: 32 }
); );
}); });
it('start campaign if campaign id passed', async () => { it('start campaign if campaign id passed', async () => {
API.get.mockResolvedValue({ data: campaigns }); API.get.mockResolvedValue({ data: campaigns });
await actions.startCampaign( await actions.startCampaign(
{ dispatch, getters: { getCampaigns: campaigns }, commit }, {
dispatch,
getters: { getCampaigns: campaigns },
commit,
rootState: {
events: { isOpen: false },
},
},
{ campaignId: 1 } { campaignId: 1 }
); );
expect(commit.mock.calls).toEqual([['setActiveCampaign', campaigns[0]]]); expect(commit.mock.calls).toEqual([['setActiveCampaign', campaigns[0]]]);
@ -112,7 +126,10 @@ describe('#actions', () => {
const params = { campaignId: 12, websiteToken: 'XDsafmADasd' }; const params = { campaignId: 12, websiteToken: 'XDsafmADasd' };
API.post.mockResolvedValue({}); API.post.mockResolvedValue({});
await actions.executeCampaign({ commit }, params); await actions.executeCampaign({ commit }, params);
expect(commit.mock.calls).toEqual([['setActiveCampaign', {}]]); expect(commit.mock.calls).toEqual([
['setCampaignExecuted'],
['setActiveCampaign', {}],
]);
}); });
it('sends correct actions if execute campaign API is failed', async () => { it('sends correct actions if execute campaign API is failed', async () => {
const params = { campaignId: 12, websiteToken: 'XDsafmADasd' }; const params = { campaignId: 12, websiteToken: 'XDsafmADasd' };
@ -121,4 +138,12 @@ describe('#actions', () => {
expect(commit.mock.calls).toEqual([['setError', true]]); expect(commit.mock.calls).toEqual([['setError', true]]);
}); });
}); });
describe('#resetCampaign', () => {
it('sends correct actions if execute campaign API is success', async () => {
API.post.mockResolvedValue({});
await actions.resetCampaign({ commit });
expect(commit.mock.calls).toEqual([['setActiveCampaign', {}]]);
});
});
}); });

View file

@ -129,4 +129,17 @@ describe('#getters', () => {
updated_at: '2021-05-03T04:53:36.354Z', updated_at: '2021-05-03T04:53:36.354Z',
}); });
}); });
it('getCampaignHasExecuted', () => {
const state = {
records: [],
uiFlags: {
isError: false,
hasFetched: false,
},
activeCampaign: {},
campaignHasExecuted: false,
};
expect(getters.getCampaignHasExecuted(state)).toEqual(false);
});
}); });

View file

@ -33,4 +33,12 @@ describe('#mutations', () => {
expect(state.activeCampaign).toEqual(campaigns[0]); expect(state.activeCampaign).toEqual(campaigns[0]);
}); });
}); });
describe('#setCampaignExecuted', () => {
it('set campaign executed flag', () => {
const state = { records: [], uiFlags: {}, campaignHasExecuted: false };
mutations.setCampaignExecuted(state);
expect(state.campaignHasExecuted).toEqual(true);
});
});
}); });

View file

@ -83,6 +83,8 @@ import { mapGetters } from 'vuex';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages'; import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import PreChatForm from '../components/PreChat/Form'; import PreChatForm from '../components/PreChat/Form';
import { isEmptyObject } from 'widget/helpers/utils';
export default { export default {
name: 'Home', name: 'Home',
components: { components: {
@ -106,6 +108,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isCampaignViewClicked: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@ -121,16 +127,24 @@ export default {
groupedMessages: 'conversation/getGroupedConversation', groupedMessages: 'conversation/getGroupedConversation',
isFetchingList: 'conversation/getIsFetchingList', isFetchingList: 'conversation/getIsFetchingList',
currentUser: 'contacts/getCurrentUser', currentUser: 'contacts/getCurrentUser',
activeCampaign: 'campaign/getActiveCampaign',
getCampaignHasExecuted: 'campaign/getCampaignHasExecuted',
}), }),
currentView() { currentView() {
const { email: currentUserEmail = '' } = this.currentUser; const { email: currentUserEmail = '' } = this.currentUser;
if (this.isHeaderCollapsed) { if (this.isHeaderCollapsed) {
if (this.conversationSize) { if (this.conversationSize) {
return 'messageView'; return 'messageView';
} }
if ( if (
this.isOnNewConversation || !this.getCampaignHasExecuted &&
(this.preChatFormEnabled && !currentUserEmail) ((this.preChatFormEnabled &&
!isEmptyObject(this.activeCampaign) &&
this.preChatFormOptions.requireEmail) ||
this.isOnNewConversation ||
(this.preChatFormEnabled && !currentUserEmail))
) { ) {
return 'preChatFormView'; return 'preChatFormView';
} }
@ -145,10 +159,13 @@ export default {
return MAXIMUM_FILE_UPLOAD_SIZE; return MAXIMUM_FILE_UPLOAD_SIZE;
}, },
isHeaderCollapsed() { isHeaderCollapsed() {
if (!this.hasIntroText || this.conversationSize) { if (
!this.hasIntroText ||
this.conversationSize ||
this.isCampaignViewClicked
) {
return true; return true;
} }
return this.isOnCollapsedView; return this.isOnCollapsedView;
}, },
hasIntroText() { hasIntroText() {

View file

@ -13,6 +13,7 @@
:has-fetched="hasFetched" :has-fetched="hasFetched"
:unread-message-count="unreadMessageCount" :unread-message-count="unreadMessageCount"
:show-popout-button="showPopoutButton" :show-popout-button="showPopoutButton"
:is-campaign-view-clicked="isCampaignViewClicked"
/> />
<unread <unread
v-else v-else
@ -67,6 +68,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isCampaignViewClicked: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
showHomePage() { showHomePage() {