mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	feat: Show pre-chat form before triggering the campaign (#3215)
This commit is contained in:
		@@ -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') {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
      }
 | 
			
		||||
      // 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,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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": {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 }) => {
 | 
			
		||||
  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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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', {}]]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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.getCampaignHasExecuted &&
 | 
			
		||||
          ((this.preChatFormEnabled &&
 | 
			
		||||
            !isEmptyObject(this.activeCampaign) &&
 | 
			
		||||
            this.preChatFormOptions.requireEmail) ||
 | 
			
		||||
            this.isOnNewConversation ||
 | 
			
		||||
          (this.preChatFormEnabled && !currentUserEmail)
 | 
			
		||||
            (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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user