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"
:hide-message-bubble="hideMessageBubble"
:show-popout-button="showPopoutButton"
:is-campaign-view-clicked="isCampaignViewClicked"
/>
</template>
@ -15,18 +16,19 @@
import { mapGetters, mapActions, mapMutations } from 'vuex';
import { setHeader } from 'widget/helpers/axios';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import configMixin from './mixins/configMixin';
import availabilityMixin from 'widget/mixins/availability';
import Router from './views/Router';
import { getLocale } from './helpers/urlParamsHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { isEmptyObject } from 'widget/helpers/utils';
import availabilityMixin from 'widget/mixins/availability';
export default {
name: 'App',
components: {
Router,
},
mixins: [availabilityMixin],
mixins: [availabilityMixin, configMixin],
data() {
return {
showUnreadView: false,
@ -36,6 +38,7 @@ export default {
widgetPosition: 'right',
showPopoutButton: false,
isWebWidgetTriggered: false,
isCampaignViewClicked: false,
isWidgetOpen: false,
};
},
@ -98,7 +101,11 @@ export default {
methods: {
...mapActions('appConfig', ['setWidgetColor']),
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
...mapActions('campaign', ['initCampaigns', 'executeCampaign']),
...mapActions('campaign', [
'initCampaigns',
'executeCampaign',
'resetCampaign',
]),
...mapActions('agent', ['fetchAvailableAgents']),
...mapMutations('events', ['toggleOpen']),
scrollConversationToBottom() {
@ -147,15 +154,25 @@ export default {
});
},
registerCampaignEvents() {
bus.$on('on-campaign-view-clicked', campaignId => {
const { websiteToken } = window.chatwootWebChannel;
bus.$on('on-campaign-view-clicked', () => {
this.isCampaignViewClicked = true;
this.showCampaignView = false;
this.showUnreadView = false;
this.unsetUnreadView();
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 });
});
},
setPopoutDisplay(showPopoutButton) {
this.showPopoutButton = showPopoutButton;
},
@ -255,6 +272,10 @@ export default {
this.showUnreadView = true;
this.showCampaignView = false;
} 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.showCampaignView = false;
} else if (message.event === 'toggle-open') {

View file

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

View file

@ -12,6 +12,7 @@ const state = {
hasFetched: false,
},
activeCampaign: {},
campaignHasExecuted: false,
};
const resetCampaignTimers = (
@ -34,6 +35,7 @@ export const getters = {
getHasFetched: $state => $state.uiFlags.hasFetched,
getCampaigns: $state => $state.records,
getActiveCampaign: $state => $state.activeCampaign,
getCampaignHasExecuted: $state => $state.campaignHasExecuted,
};
export const actions = {
@ -76,17 +78,37 @@ export const actions = {
);
}
},
startCampaign: async ({ commit }, { websiteToken, campaignId }) => {
const { data: campaigns } = await getCampaigns(websiteToken);
const campaign = campaigns.find(item => item.id === campaignId);
if (campaign) {
commit('setActiveCampaign', campaign);
startCampaign: async (
{
commit,
rootState: {
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 }) => {
try {
await triggerCampaign({ campaignId, websiteToken });
commit('setCampaignExecuted');
commit('setActiveCampaign', {});
} catch (error) {
commit('setError', true);
}
},
resetCampaign: async ({ commit }) => {
try {
commit('setActiveCampaign', {});
} catch (error) {
commit('setError', true);
@ -107,6 +129,9 @@ export const mutations = {
setHasFetched($state, value) {
Vue.set($state.uiFlags, 'hasFetched', value);
},
setCampaignExecuted($state) {
Vue.set($state, 'campaignHasExecuted', true);
},
};
export default {

View file

@ -94,14 +94,28 @@ describe('#actions', () => {
it('reset campaign if campaign id is not present in the campaign list', async () => {
API.get.mockResolvedValue({ data: campaigns });
await actions.startCampaign(
{ dispatch, getters: { getCampaigns: campaigns }, commit },
{
dispatch,
getters: { getCampaigns: campaigns },
commit,
rootState: {
events: { isOpen: true },
},
},
{ campaignId: 32 }
);
});
it('start campaign if campaign id passed', async () => {
API.get.mockResolvedValue({ data: campaigns });
await actions.startCampaign(
{ dispatch, getters: { getCampaigns: campaigns }, commit },
{
dispatch,
getters: { getCampaigns: campaigns },
commit,
rootState: {
events: { isOpen: false },
},
},
{ campaignId: 1 }
);
expect(commit.mock.calls).toEqual([['setActiveCampaign', campaigns[0]]]);
@ -112,7 +126,10 @@ describe('#actions', () => {
const params = { campaignId: 12, websiteToken: 'XDsafmADasd' };
API.post.mockResolvedValue({});
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 () => {
const params = { campaignId: 12, websiteToken: 'XDsafmADasd' };
@ -121,4 +138,12 @@ describe('#actions', () => {
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',
});
});
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]);
});
});
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 { BUS_EVENTS } from 'shared/constants/busEvents';
import PreChatForm from '../components/PreChat/Form';
import { isEmptyObject } from 'widget/helpers/utils';
export default {
name: 'Home',
components: {
@ -106,6 +108,10 @@ export default {
type: Boolean,
default: false,
},
isCampaignViewClicked: {
type: Boolean,
default: false,
},
},
data() {
return {
@ -121,16 +127,24 @@ export default {
groupedMessages: 'conversation/getGroupedConversation',
isFetchingList: 'conversation/getIsFetchingList',
currentUser: 'contacts/getCurrentUser',
activeCampaign: 'campaign/getActiveCampaign',
getCampaignHasExecuted: 'campaign/getCampaignHasExecuted',
}),
currentView() {
const { email: currentUserEmail = '' } = this.currentUser;
if (this.isHeaderCollapsed) {
if (this.conversationSize) {
return 'messageView';
}
if (
this.isOnNewConversation ||
(this.preChatFormEnabled && !currentUserEmail)
!this.getCampaignHasExecuted &&
((this.preChatFormEnabled &&
!isEmptyObject(this.activeCampaign) &&
this.preChatFormOptions.requireEmail) ||
this.isOnNewConversation ||
(this.preChatFormEnabled && !currentUserEmail))
) {
return 'preChatFormView';
}
@ -145,10 +159,13 @@ export default {
return MAXIMUM_FILE_UPLOAD_SIZE;
},
isHeaderCollapsed() {
if (!this.hasIntroText || this.conversationSize) {
if (
!this.hasIntroText ||
this.conversationSize ||
this.isCampaignViewClicked
) {
return true;
}
return this.isOnCollapsedView;
},
hasIntroText() {

View file

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