feat: Add campaign (#2177)

This commit is contained in:
Muhsin Keloth 2021-05-04 15:08:41 +05:30 committed by GitHub
parent eaa61c3745
commit 7c7f91e70f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 792 additions and 19 deletions

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class CampaignsAPI extends ApiClient {
constructor() {
super('campaigns', { accountScoped: true });
}
}
export default new CampaignsAPI();

View file

@ -1,9 +1,49 @@
{
"CAMPAIGN": {
"HEADER": "Campaigns",
"HEADER_BTN_TXT": "Create Campaign",
"SIDEBAR_TXT": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations. Click on <b>Add Campaign</b> to create a new campaign. You can also edit or delete an existing campaign by clicking on the Edit or Delete button.",
"HEADER_BTN_TXT": "Create a campaign",
"LIST": {
"404": "There are no campaigns attached to this inbox"
"404": "There are no campaigns created for this inbox."
},
"ADD": {
"TITLE": "Create a campaign",
"DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.",
"CANCEL_BUTTON_TEXT": "Cancel",
"CREATE_BUTTON_TEXT": "Create",
"FORM": {
"TITLE": {
"LABEL": "Title",
"PLACEHOLDER": "Please enter the title of campaign",
"ERROR": "Title is required"
},
"MESSAGE": {
"LABEL": "Message",
"PLACEHOLDER": "Please enter the message of campaign",
"ERROR": "Message is required"
},
"SENT_BY": {
"LABEL": "Sent by",
"PLACEHOLDER": "Please select the the content of campaign",
"ERROR": "Sender is required"
},
"END_POINT": {
"LABEL": "URL",
"PLACEHOLDER": "Please enter the URL",
"ERROR": "Please enter a valid URL"
},
"TIME_ON_PAGE": {
"LABEL": "Time on page(Seconds)",
"PLACEHOLDER": "Please enter the time",
"ERROR": "Time on page is required"
},
"ENABLED": "Enable campaign",
"SUBMIT": "Add Campaign"
},
"API": {
"SUCCESS_MESSAGE": "Campaign created successfully",
"ERROR_MESSAGE": "There was an error. Please try again."
}
}
}
}

View file

@ -335,10 +335,10 @@ export default {
if (this.isAWebWidgetInbox) {
return [
...visibleToAllChannelTabs,
{
key: 'campaign',
name: this.$t('INBOX_MGMT.TABS.CAMPAIGN'),
},
// {
// key: 'campaign',
// name: this.$t('INBOX_MGMT.TABS.CAMPAIGN'),
// },
{
key: 'preChatForm',
name: this.$t('INBOX_MGMT.TABS.PRE_CHAT_FORM'),

View file

@ -0,0 +1,205 @@
<template>
<modal :show.sync="show" :on-close="onClose">
<div class="column content-box">
<woot-modal-header
:header-title="$t('CAMPAIGN.ADD.TITLE')"
:header-content="$t('CAMPAIGN.ADD.DESC')"
/>
<form class="row" @submit.prevent="addCampaign">
<div class="medium-12 columns">
<label :class="{ error: $v.title.$error }">
{{ $t('CAMPAIGN.ADD.FORM.TITLE.LABEL') }}
<input
v-model.trim="title"
type="text"
:placeholder="$t('CAMPAIGN.ADD.FORM.TITLE.PLACEHOLDER')"
@input="$v.title.$touch"
/>
</label>
</div>
<div class="medium-12 columns">
<label :class="{ error: $v.message.$error }">
{{ $t('CAMPAIGN.ADD.FORM.MESSAGE.LABEL') }}
<textarea
v-model.trim="message"
rows="5"
type="text"
:placeholder="$t('CAMPAIGN.ADD.FORM.MESSAGE.PLACEHOLDER')"
@input="$v.message.$touch"
/>
</label>
</div>
<div class="medium-12 columns">
<label :class="{ error: $v.selectedAgent.$error }">
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.LABEL') }}
<select v-model="selectedAgent">
<option
v-for="agent in agentsList"
:key="agent.name"
:value="agent.name"
>
{{ agent.name }}
</option>
</select>
<span v-if="$v.selectedAgent.$error" class="message">
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.ERROR') }}
</span>
</label>
</div>
<div class="medium-12 columns">
<label :class="{ error: $v.endPoint.$error }">
{{ $t('CAMPAIGN.ADD.FORM.END_POINT.LABEL') }}
<input
v-model.trim="endPoint"
type="text"
:placeholder="$t('CAMPAIGN.ADD.FORM.END_POINT.PLACEHOLDER')"
@input="$v.endPoint.$touch"
/>
<span v-if="$v.endPoint.$error" class="message">
{{ $t('CAMPAIGN.ADD.FORM.END_POINT.ERROR') }}
</span>
</label>
</div>
<div class="medium-12 columns">
<label :class="{ error: $v.timeOnPage.$error }">
{{ $t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.LABEL') }}
<input
v-model.trim="timeOnPage"
type="number"
:placeholder="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.PLACEHOLDER')"
@input="$v.timeOnPage.$touch"
/>
<span v-if="$v.timeOnPage.$error" class="message">
{{ $t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.ERROR') }}
</span>
</label>
</div>
<div class="medium-12 columns">
<label>
<input
v-model="enabled"
type="checkbox"
value="enabled"
name="enabled"
/>
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
</label>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-submit-button
:disabled="buttonDisabled"
:loading="uiFlags.isCreating"
:button-text="$t('CAMPAIGN.ADD.CREATE_BUTTON_TEXT')"
/>
<button class="button clear" @click.prevent="onClose">
{{ $t('CAMPAIGN.ADD.CANCEL_BUTTON_TEXT') }}
</button>
</div>
</div>
</form>
</div>
</modal>
</template>
<script>
import { mapGetters } from 'vuex';
import { required, url, minLength } from 'vuelidate/lib/validators';
import Modal from 'dashboard/components/Modal';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
Modal,
},
mixins: [alertMixin],
props: {
onClose: {
type: Function,
default: () => {},
},
},
data() {
return {
title: '',
message: '',
selectedAgent: '',
endPoint: '',
timeOnPage: 10,
show: true,
enabled: true,
};
},
validations: {
title: {
required,
},
message: {
required,
},
selectedAgent: {
required,
},
endPoint: {
required,
minLength: minLength(7),
url,
},
timeOnPage: {
required,
},
},
computed: {
...mapGetters({
agentList: 'agents/getAgents',
uiFlags: 'campaigns/getUIFlags',
}),
agentsList() {
return this.agentList;
},
buttonDisabled() {
return (
this.$v.message.$invalid ||
this.$v.title.$invalid ||
this.$v.selectedAgent.$invalid ||
this.$v.endPoint.$invalid ||
this.$v.timeOnPage.$invalid ||
this.uiFlags.isCreating
);
},
},
methods: {
async addCampaign() {
try {
await this.$store.dispatch('campaigns/create', {
title: this.title,
message: this.message,
inbox_id: this.$route.params.inboxId,
sender_id: this.selectedAgent,
enabled: this.enabled,
trigger_rules: {
url: this.endPoint,
time_on_page: this.timeOnPage,
},
});
this.showAlert(this.$t('CAMPAIGN.ADD.API.SUCCESS_MESSAGE'));
this.onClose();
} catch (error) {
this.showAlert(this.$t('CAMPAIGN.ADD.API.ERROR_MESSAGE'));
}
},
},
};
</script>
<style lang="scss" scoped>
.content-box .page-top-bar::v-deep {
padding: var(--space-large) var(--space-large) var(--space-zero);
}
</style>

View file

@ -1,17 +1,17 @@
<template>
<div class="column content-box">
<div class="row">
<a class="button icon success nice button--fixed-right-top">
<div v-if="campaigns.length" class="row button-wrapper">
<woot-button @click="openAddPopup">
<i class="icon ion-android-add-circle"></i>
{{ $t('CAMPAIGN.HEADER_BTN_TXT') }}
</a>
</woot-button>
</div>
<div class="row">
<div v-if="!campaigns.length" class="row">
<div class="small-8 columns">
<p class="no-items-error-message">
{{ $t('CAMPAIGN.LIST.404') }}
<a>
<a @click="openAddPopup">
{{ $t('CAMPAIGN.HEADER_BTN_TXT') }}
</a>
</p>
@ -22,16 +22,41 @@
<p>
<b> {{ $t('CAMPAIGN.HEADER') }}</b>
</p>
<p>
Proactive messages allows customer send outbound messages to their
contacts which would trigger more conversations. Campaigns are tied
to inbox. Click on
<b>Add Campaign</b>
to create a new campaign. You can also edit or delete an existing
campaigns Response by clicking on the Edit or Delete button.
</p>
<p v-html="$t('CAMPAIGN.SIDEBAR_TXT')" />
</span>
</div>
</div>
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<add-campaign :on-close="hideAddPopup" />
</woot-modal>
</div>
</template>
<script>
import AddCampaign from './AddCampaign';
export default {
components: {
AddCampaign,
},
data() {
return {
campaigns: [],
showAddPopup: false,
};
},
methods: {
openAddPopup() {
this.showAddPopup = true;
},
hideAddPopup() {
this.showAddPopup = false;
},
},
};
</script>
<style scoped lang="scss">
.button-wrapper {
display: flex;
justify-content: flex-end;
}
</style>

View file

@ -26,6 +26,7 @@ import userNotificationSettings from './modules/userNotificationSettings';
import webhooks from './modules/webhooks';
import teams from './modules/teams';
import teamMembers from './modules/teamMembers';
import campaigns from './modules/campaigns';
Vue.use(Vuex);
export default new Vuex.Store({
@ -55,5 +56,6 @@ export default new Vuex.Store({
webhooks,
teams,
teamMembers,
campaigns,
},
});

View file

@ -0,0 +1,65 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types';
import CampaignsAPI from '../../api/campaigns';
export const state = {
records: [],
uiFlags: {
isFetching: false,
isCreating: false,
},
};
export const getters = {
getUIFlags(_state) {
return _state.uiFlags;
},
getCampaigns(_state) {
return _state.records;
},
};
export const actions = {
get: async function getCampaigns({ commit }) {
commit(types.SET_CAMPAIGN_UI_FLAG, { isFetching: true });
try {
const response = await CampaignsAPI.get();
commit(types.SET_CAMPAIGNS, response.data);
} catch (error) {
// Ignore error
} finally {
commit(types.SET_CAMPAIGN_UI_FLAG, { isFetching: false });
}
},
create: async function createCampaign({ commit }, campaignObj) {
commit(types.SET_CAMPAIGN_UI_FLAG, { isCreating: true });
try {
const response = await CampaignsAPI.create(campaignObj);
commit(types.ADD_CAMPAIGN, response.data);
} catch (error) {
throw new Error(error);
} finally {
commit(types.SET_CAMPAIGN_UI_FLAG, { isCreating: false });
}
},
};
export const mutations = {
[types.SET_CAMPAIGN_UI_FLAG](_state, data) {
_state.uiFlags = {
..._state.uiFlags,
...data,
};
},
[types.ADD_CAMPAIGN]: MutationHelpers.create,
[types.SET_CAMPAIGNS]: MutationHelpers.set,
};
export default {
namespaced: true,
actions,
state,
getters,
mutations,
};

View file

@ -0,0 +1,30 @@
import axios from 'axios';
import { actions } from '../../campaigns';
import * as types from '../../../mutation-types';
import campaignList from './fixtures';
const commit = jest.fn();
global.axios = axios;
jest.mock('axios');
describe('#actions', () => {
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: campaignList[0] });
await actions.create({ commit }, campaignList[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_CAMPAIGN_UI_FLAG, { isCreating: true }],
[types.default.ADD_CAMPAIGN, campaignList[0]],
[types.default.SET_CAMPAIGN_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.create({ commit })).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_CAMPAIGN_UI_FLAG, { isCreating: true }],
[types.default.SET_CAMPAIGN_UI_FLAG, { isCreating: false }],
]);
});
});
});

View file

@ -0,0 +1,348 @@
export default [
{
id: 8,
title: 'Welcome',
description: null,
account_id: 1,
inbox: {
id: 37,
channel_id: 1,
name: 'Chatwoot',
channel_type: 'Channel::WebWidget',
greeting_enabled: true,
greeting_message: '',
working_hours_enabled: true,
out_of_office_message:
'We are unavailable at the moment. Leave a message we will respond once we are back.',
working_hours: [
{
day_of_week: 0,
closed_all_day: true,
open_hour: null,
open_minutes: null,
close_hour: null,
close_minutes: null,
},
{
day_of_week: 1,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 2,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 3,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 4,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 5,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 6,
closed_all_day: true,
open_hour: null,
open_minutes: null,
close_hour: null,
close_minutes: null,
},
],
timezone: 'Asia/Kolkata',
avatar_url: '',
page_id: null,
widget_color: '#1F93FF',
website_url: 'chatwoot.com',
welcome_title: 'Hi there ! 🙌🏼',
welcome_tagline:
'We make it simple to connect with us. Ask us anything, or share your feedback.',
enable_auto_assignment: true,
website_token: '',
forward_to_email: null,
phone_number: null,
selected_feature_flags: ['attachments', 'emoji_picker'],
reply_time: 'in_a_few_hours',
hmac_token: '',
pre_chat_form_enabled: true,
pre_chat_form_options: {
require_email: true,
pre_chat_message: 'Share your queries or comments here.',
},
},
sender: {
account_id: 1,
availability_status: 'offline',
confirmed: true,
email: 'sojan@chatwoot.com',
available_name: 'Sojan',
id: 10,
name: 'Sojan',
role: 'administrator',
thumbnail:
'http://0.0.0.0:3000/rails/active_storage/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--bfa5e8a4563aef73980771fc9b8007d380e586e5/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9MY21WemFYcGxTU0lNTWpVd2VESTFNQVk2QmtWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--c13bd5229b2a2a692444606e22f76ad61c634661/73185.jpeg',
},
message: 'Hey, What brings you today',
enabled: true,
trigger_rules: {
url: 'https://github.com',
time_on_page: 10,
},
created_at: '2021-05-03T04:53:36.354Z',
updated_at: '2021-05-03T04:53:36.354Z',
},
{
id: 11,
title: 'Onboarding Campaign',
description: null,
account_id: 1,
inbox: {
id: 37,
channel_id: 1,
name: 'Chatwoot',
channel_type: 'Channel::WebWidget',
greeting_enabled: true,
greeting_message: '',
working_hours_enabled: true,
out_of_office_message:
'We are unavailable at the moment. Leave a message we will respond once we are back.',
working_hours: [
{
day_of_week: 0,
closed_all_day: true,
open_hour: null,
open_minutes: null,
close_hour: null,
close_minutes: null,
},
{
day_of_week: 1,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 2,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 3,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 4,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 5,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 6,
closed_all_day: true,
open_hour: null,
open_minutes: null,
close_hour: null,
close_minutes: null,
},
],
timezone: 'Asia/Kolkata',
avatar_url: '',
page_id: null,
widget_color: '#1F93FF',
website_url: 'chatwoot.com',
welcome_title: 'Hi there ! 🙌🏼',
welcome_tagline:
'We make it simple to connect with us. Ask us anything, or share your feedback.',
enable_auto_assignment: true,
web_widget_script: '',
website_token: '',
forward_to_email: null,
phone_number: null,
selected_feature_flags: ['attachments', 'emoji_picker'],
reply_time: 'in_a_few_hours',
hmac_token: '',
pre_chat_form_enabled: true,
pre_chat_form_options: {
require_email: true,
pre_chat_message: 'Share your queries or comments here.',
},
},
sender: {
account_id: 1,
availability_status: 'offline',
confirmed: true,
email: 'sojan@chatwoot.com',
available_name: 'Sojan',
id: 10,
name: 'Sojan',
role: 'administrator',
thumbnail:
'http://0.0.0.0:3000/rails/active_storage/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--bfa5e8a4563aef73980771fc9b8007d380e586e5/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9MY21WemFYcGxTU0lNTWpVd2VESTFNQVk2QmtWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--c13bd5229b2a2a692444606e22f76ad61c634661/73185.jpeg',
},
message: 'Begin your onboarding campaign with a welcome message',
enabled: true,
trigger_rules: {
url: 'https://chatwoot.com',
time_on_page: '20',
},
created_at: '2021-05-03T08:15:35.828Z',
updated_at: '2021-05-03T08:15:35.828Z',
},
{
id: 12,
title: 'Thanks',
description: null,
account_id: 1,
inbox: {
id: 37,
channel_id: 1,
name: 'Chatwoot',
channel_type: 'Channel::WebWidget',
greeting_enabled: true,
greeting_message: '',
working_hours_enabled: true,
out_of_office_message:
'We are unavailable at the moment. Leave a message we will respond once we are back.',
working_hours: [
{
day_of_week: 0,
closed_all_day: true,
open_hour: null,
open_minutes: null,
close_hour: null,
close_minutes: null,
},
{
day_of_week: 1,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 2,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 3,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 4,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 5,
closed_all_day: false,
open_hour: 11,
open_minutes: 0,
close_hour: 23,
close_minutes: 30,
},
{
day_of_week: 6,
closed_all_day: true,
open_hour: null,
open_minutes: null,
close_hour: null,
close_minutes: null,
},
],
timezone: 'Asia/Kolkata',
avatar_url: '',
page_id: null,
widget_color: '#1F93FF',
website_url: 'chatwoot.com',
welcome_title: 'Hi there ! 🙌🏼',
welcome_tagline:
'We make it simple to connect with us. Ask us anything, or share your feedback.',
enable_auto_assignment: true,
web_widget_script: '',
website_token: '',
forward_to_email: null,
phone_number: null,
selected_feature_flags: ['attachments', 'emoji_picker'],
reply_time: 'in_a_few_hours',
hmac_token: '',
pre_chat_form_enabled: true,
pre_chat_form_options: {
require_email: true,
pre_chat_message: 'Share your queries or comments here.',
},
},
sender: {
account_id: 1,
availability_status: 'offline',
confirmed: true,
email: 'nithin@chatwoot.com',
available_name: 'Nithin',
id: 13,
name: 'Nithin',
role: 'administrator',
thumbnail: '',
},
message: 'Thanks for coming to the show. How may I help you?',
enabled: false,
trigger_rules: {
url: 'https://noshow.com',
time_on_page: 10,
},
created_at: '2021-05-03T10:22:51.025Z',
updated_at: '2021-05-03T10:22:51.025Z',
},
];

View file

@ -0,0 +1,22 @@
import { getters } from '../../campaigns';
import campaigns from './fixtures';
describe('#getters', () => {
it('getCampaigns', () => {
const state = { records: campaigns };
expect(getters.getCampaigns(state)).toEqual(campaigns);
});
it('getUIFlags', () => {
const state = {
uiFlags: {
isFetching: true,
isCreating: false,
},
};
expect(getters.getUIFlags(state)).toEqual({
isFetching: true,
isCreating: false,
});
});
});

View file

@ -0,0 +1,21 @@
import types from '../../../mutation-types';
import { mutations } from '../../campaigns';
import campaigns from './fixtures';
describe('#mutations', () => {
describe('#SET_CAMPAIGNS', () => {
it('set campaigns records', () => {
const state = { records: [] };
mutations[types.SET_CAMPAIGNS](state, campaigns);
expect(state.records).toEqual(campaigns);
});
});
describe('#ADD_CAMPAIGN', () => {
it('push newly created campaigns to the store', () => {
const state = { records: [campaigns[0]] };
mutations[types.ADD_CAMPAIGN](state, campaigns[1]);
expect(state.records).toEqual([campaigns[0], campaigns[1]]);
});
});
});

View file

@ -145,4 +145,9 @@ export default {
// Conversation Search
SEARCH_CONVERSATIONS_SET: 'SEARCH_CONVERSATIONS_SET',
SEARCH_CONVERSATIONS_SET_UI_FLAG: 'SEARCH_CONVERSATIONS_SET_UI_FLAG',
// Campaigns
SET_CAMPAIGN_UI_FLAG: 'SET_CAMPAIGN_UI_FLAG',
SET_CAMPAIGNS: 'SET_CAMPAIGNS',
ADD_CAMPAIGN: 'ADD_CAMPAIGN',
};

View file

@ -10,6 +10,7 @@
"test:coverage": "jest -w 1 --no-cache --collectCoverage",
"webpacker-start": "webpack-dev-server -d --config webpack.dev.config.js --content-base public/ --progress --colors",
"start:dev": "foreman start -f ./Procfile.dev",
"start:dev-overmind": "overmind start -f ./Procfile.dev",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},