fix: Trigger campaigns only during business hours (#3085)

Fixes #2433
This commit is contained in:
Muhsin Keloth 2021-10-12 17:58:33 +05:30 committed by GitHub
parent 7c21cf2255
commit 6bfa551c85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 142 additions and 38 deletions

View file

@ -28,7 +28,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
end end
def campaign_params def campaign_params
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id, params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
:scheduled_at, audience: [:type, :id], trigger_rules: {}) :scheduled_at, audience: [:type, :id], trigger_rules: {})
end end
end end

View file

@ -54,6 +54,7 @@
"ERROR": "Time on page is required" "ERROR": "Time on page is required"
}, },
"ENABLED": "Enable campaign", "ENABLED": "Enable campaign",
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
"SUBMIT": "Add Campaign" "SUBMIT": "Add Campaign"
}, },
"API": { "API": {

View file

@ -146,6 +146,15 @@
/> />
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }} {{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
</label> </label>
<label v-if="isOngoingType">
<input
v-model="triggerOnlyDuringBusinessHours"
type="checkbox"
value="triggerOnlyDuringBusinessHours"
name="triggerOnlyDuringBusinessHours"
/>
{{ $t('CAMPAIGN.ADD.FORM.TRIGGER_ONLY_BUSINESS_HOURS') }}
</label>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -185,6 +194,7 @@ export default {
timeOnPage: 10, timeOnPage: 10,
show: true, show: true,
enabled: true, enabled: true,
triggerOnlyDuringBusinessHours: false,
scheduledAt: null, scheduledAt: null,
selectedAudience: [], selectedAudience: [],
senderList: [], senderList: [],
@ -280,6 +290,9 @@ export default {
inbox_id: this.selectedInbox, inbox_id: this.selectedInbox,
sender_id: this.selectedSender || null, sender_id: this.selectedSender || null,
enabled: this.enabled, enabled: this.enabled,
trigger_only_during_business_hours:
// eslint-disable-next-line prettier/prettier
this.triggerOnlyDuringBusinessHours,
trigger_rules: { trigger_rules: {
url: this.endPoint, url: this.endPoint,
time_on_page: this.timeOnPage, time_on_page: this.timeOnPage,

View file

@ -87,6 +87,15 @@
/> />
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }} {{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
</label> </label>
<label v-if="isOngoingType">
<input
v-model="triggerOnlyDuringBusinessHours"
type="checkbox"
value="triggerOnlyDuringBusinessHours"
name="triggerOnlyDuringBusinessHours"
/>
{{ $t('CAMPAIGN.ADD.FORM.TRIGGER_ONLY_BUSINESS_HOURS') }}
</label>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<woot-button :is-loading="uiFlags.isCreating"> <woot-button :is-loading="uiFlags.isCreating">
@ -125,6 +134,7 @@ export default {
selectedInbox: null, selectedInbox: null,
endPoint: '', endPoint: '',
timeOnPage: 10, timeOnPage: 10,
triggerOnlyDuringBusinessHours: false,
show: true, show: true,
enabled: true, enabled: true,
senderList: [], senderList: [],
@ -209,6 +219,7 @@ export default {
title, title,
message, message,
enabled, enabled,
trigger_only_during_business_hours: triggerOnlyDuringBusinessHours,
inbox: { id: inboxId }, inbox: { id: inboxId },
trigger_rules: { url: endPoint, time_on_page: timeOnPage }, trigger_rules: { url: endPoint, time_on_page: timeOnPage },
sender, sender,
@ -218,6 +229,7 @@ export default {
this.endPoint = endPoint; this.endPoint = endPoint;
this.timeOnPage = timeOnPage; this.timeOnPage = timeOnPage;
this.selectedInbox = inboxId; this.selectedInbox = inboxId;
this.triggerOnlyDuringBusinessHours = triggerOnlyDuringBusinessHours;
this.selectedSender = (sender && sender.id) || 0; this.selectedSender = (sender && sender.id) || 0;
this.enabled = enabled; this.enabled = enabled;
this.loadInboxMembers(); this.loadInboxMembers();
@ -233,6 +245,9 @@ export default {
title: this.title, title: this.title,
message: this.message, message: this.message,
inbox_id: this.$route.params.inboxId, inbox_id: this.$route.params.inboxId,
trigger_only_during_business_hours:
// eslint-disable-next-line prettier/prettier
this.triggerOnlyDuringBusinessHours,
sender_id: this.selectedSender || null, sender_id: this.selectedSender || null,
enabled: this.enabled, enabled: this.enabled,
trigger_rules: { trigger_rules: {

View file

@ -19,12 +19,14 @@ 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],
data() { data() {
return { return {
showUnreadView: false, showUnreadView: false,
@ -219,7 +221,11 @@ export default {
this.scrollConversationToBottom(); this.scrollConversationToBottom();
} else if (message.event === 'change-url') { } else if (message.event === 'change-url') {
const { referrerURL, referrerHost } = message; const { referrerURL, referrerHost } = message;
this.initCampaigns({ currentURL: referrerURL, websiteToken }); this.initCampaigns({
currentURL: referrerURL,
websiteToken,
isInBusinessHours: this.isInBusinessHours,
});
window.referrerURL = referrerURL; window.referrerURL = referrerURL;
bus.$emit(BUS_EVENTS.SET_REFERRER_HOST, referrerHost); bus.$emit(BUS_EVENTS.SET_REFERRER_HOST, referrerHost);
} else if (message.event === 'toggle-close-button') { } else if (message.event === 'toggle-close-button') {

View file

@ -7,17 +7,24 @@ export const formatCampaigns = ({ campaigns }) => {
return campaigns.map(item => { return campaigns.map(item => {
return { return {
id: item.id, id: item.id,
triggerOnlyDuringBusinessHours:
item.trigger_only_during_business_hours || false,
timeOnPage: item?.trigger_rules?.time_on_page, timeOnPage: item?.trigger_rules?.time_on_page,
url: item?.trigger_rules?.url, url: item?.trigger_rules?.url,
}; };
}); });
}; };
// Find all campaigns that matches the current URL // Filter all campaigns based on current URL and business availability time
export const filterCampaigns = ({ campaigns, currentURL }) => { export const filterCampaigns = ({
return campaigns.filter( campaigns,
item => currentURL,
stripTrailingSlash({ URL: item.url }) === isInBusinessHours,
stripTrailingSlash({ URL: currentURL }) }) => {
return campaigns.filter(item =>
item.triggerOnlyDuringBusinessHours
? isInBusinessHours
: stripTrailingSlash({ URL: item.url }) ===
stripTrailingSlash({ URL: currentURL })
); );
}; };

View file

@ -1,6 +1,7 @@
export default [ export default [
{ {
id: 1, id: 1,
trigger_only_during_business_hours: false,
trigger_rules: { trigger_rules: {
time_on_page: 3, time_on_page: 3,
url: 'https://www.chatwoot.com/pricing', url: 'https://www.chatwoot.com/pricing',
@ -8,6 +9,7 @@ export default [
}, },
{ {
id: 2, id: 2,
trigger_only_during_business_hours: false,
trigger_rules: { trigger_rules: {
time_on_page: 6, time_on_page: 6,
url: 'https://www.chatwoot.com/about', url: 'https://www.chatwoot.com/about',

View file

@ -3,8 +3,12 @@ import {
formatCampaigns, formatCampaigns,
filterCampaigns, filterCampaigns,
} from '../campaignHelper'; } from '../campaignHelper';
import campaigns from './camapginFixtures'; import campaigns from './campaignFixtures';
describe('#Campagin Helper', () => {
global.chatwootWebChannel = {
workingHoursEnabled: false,
};
describe('#Campaigns Helper', () => {
describe('stripTrailingSlash', () => { describe('stripTrailingSlash', () => {
it('should return striped trailing slash if url with trailing slash is passed', () => { it('should return striped trailing slash if url with trailing slash is passed', () => {
expect( expect(
@ -14,15 +18,17 @@ describe('#Campagin Helper', () => {
}); });
describe('formatCampaigns', () => { describe('formatCampaigns', () => {
it('should return formated campaigns if camapgins are passed', () => { it('should return formatted campaigns if campaigns are passed', () => {
expect(formatCampaigns({ campaigns })).toStrictEqual([ expect(formatCampaigns({ campaigns })).toStrictEqual([
{ {
id: 1, id: 1,
timeOnPage: 3, timeOnPage: 3,
triggerOnlyDuringBusinessHours: false,
url: 'https://www.chatwoot.com/pricing', url: 'https://www.chatwoot.com/pricing',
}, },
{ {
id: 2, id: 2,
triggerOnlyDuringBusinessHours: false,
timeOnPage: 6, timeOnPage: 6,
url: 'https://www.chatwoot.com/about', url: 'https://www.chatwoot.com/about',
}, },
@ -30,7 +36,7 @@ describe('#Campagin Helper', () => {
}); });
}); });
describe('filterCampaigns', () => { describe('filterCampaigns', () => {
it('should return filtered campaigns if formatted camapgins are passed', () => { it('should return filtered campaigns if formatted campaigns are passed', () => {
expect( expect(
filterCampaigns({ filterCampaigns({
campaigns: [ campaigns: [

View file

@ -58,5 +58,9 @@ export default {
closeMinute: workingHourConfig.close_minutes, closeMinute: workingHourConfig.close_minutes,
}; };
}, },
isInBusinessHours() {
const { workingHoursEnabled } = window.chatwootWebChannel;
return workingHoursEnabled ? this.isInBetweenTheWorkingHours : true;
},
}, },
}; };

View file

@ -14,12 +14,18 @@ const state = {
activeCampaign: {}, activeCampaign: {},
}; };
const resetCampaignTimers = (campaigns, currentURL, websiteToken) => { const resetCampaignTimers = (
campaigns,
currentURL,
websiteToken,
isInBusinessHours
) => {
const formattedCampaigns = formatCampaigns({ campaigns }); const formattedCampaigns = formatCampaigns({ campaigns });
// Find all campaigns that matches the current URL // Find all campaigns that matches the current URL
const filteredCampaigns = filterCampaigns({ const filteredCampaigns = filterCampaigns({
campaigns: formattedCampaigns, campaigns: formattedCampaigns,
currentURL, currentURL,
isInBusinessHours,
}); });
campaignTimer.initTimers({ campaigns: filteredCampaigns }, websiteToken); campaignTimer.initTimers({ campaigns: filteredCampaigns }, websiteToken);
}; };
@ -31,13 +37,21 @@ export const getters = {
}; };
export const actions = { export const actions = {
fetchCampaigns: async ({ commit }, { websiteToken, currentURL }) => { fetchCampaigns: async (
{ commit },
{ websiteToken, currentURL, isInBusinessHours }
) => {
try { try {
const { data: campaigns } = await getCampaigns(websiteToken); const { data: campaigns } = await getCampaigns(websiteToken);
commit('setCampaigns', campaigns); commit('setCampaigns', campaigns);
commit('setError', false); commit('setError', false);
commit('setHasFetched', true); commit('setHasFetched', true);
resetCampaignTimers(campaigns, currentURL, websiteToken); resetCampaignTimers(
campaigns,
currentURL,
websiteToken,
isInBusinessHours
);
} catch (error) { } catch (error) {
commit('setError', true); commit('setError', true);
commit('setHasFetched', true); commit('setHasFetched', true);
@ -45,12 +59,21 @@ export const actions = {
}, },
initCampaigns: async ( initCampaigns: async (
{ getters: { getCampaigns: campaigns }, dispatch }, { getters: { getCampaigns: campaigns }, dispatch },
{ currentURL, websiteToken } { currentURL, websiteToken, isInBusinessHours }
) => { ) => {
if (!campaigns.length) { if (!campaigns.length) {
dispatch('fetchCampaigns', { websiteToken, currentURL }); dispatch('fetchCampaigns', {
websiteToken,
currentURL,
isInBusinessHours,
});
} else { } else {
resetCampaignTimers(campaigns, currentURL, websiteToken); resetCampaignTimers(
campaigns,
currentURL,
websiteToken,
isInBusinessHours
);
} }
}, },
startCampaign: async ({ commit }, { websiteToken, campaignId }) => { startCampaign: async ({ commit }, { websiteToken, campaignId }) => {

View file

@ -15,7 +15,11 @@ describe('#actions', () => {
API.get.mockResolvedValue({ data: campaigns }); API.get.mockResolvedValue({ data: campaigns });
await actions.fetchCampaigns( await actions.fetchCampaigns(
{ commit }, { commit },
{ websiteToken: 'XDsafmADasd', currentURL: 'https://chatwoot.com' } {
websiteToken: 'XDsafmADasd',
currentURL: 'https://chatwoot.com',
isInBusinessHours: true,
}
); );
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
['setCampaigns', campaigns], ['setCampaigns', campaigns],
@ -25,7 +29,12 @@ describe('#actions', () => {
expect(campaignTimer.initTimers).toHaveBeenCalledWith( expect(campaignTimer.initTimers).toHaveBeenCalledWith(
{ {
campaigns: [ campaigns: [
{ id: 11, timeOnPage: '20', url: 'https://chatwoot.com' }, {
id: 11,
timeOnPage: '20',
url: 'https://chatwoot.com',
triggerOnlyDuringBusinessHours: false,
},
], ],
}, },
'XDsafmADasd' 'XDsafmADasd'
@ -35,7 +44,11 @@ describe('#actions', () => {
API.get.mockRejectedValue({ message: 'Authentication required' }); API.get.mockRejectedValue({ message: 'Authentication required' });
await actions.fetchCampaigns( await actions.fetchCampaigns(
{ commit }, { commit },
{ websiteToken: 'XDsafmADasd', currentURL: 'https://www.chatwoot.com' } {
websiteToken: 'XDsafmADasd',
currentURL: 'https://www.chatwoot.com',
isInBusinessHours: true,
}
); );
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
['setError', true], ['setError', true],
@ -65,7 +78,12 @@ describe('#actions', () => {
expect(campaignTimer.initTimers).toHaveBeenCalledWith( expect(campaignTimer.initTimers).toHaveBeenCalledWith(
{ {
campaigns: [ campaigns: [
{ id: 11, timeOnPage: '20', url: 'https://chatwoot.com' }, {
id: 11,
timeOnPage: '20',
url: 'https://chatwoot.com',
triggerOnlyDuringBusinessHours: false,
},
], ],
}, },
'XDsafmADasd' 'XDsafmADasd'

View file

@ -2,22 +2,23 @@
# #
# Table name: campaigns # Table name: campaigns
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# audience :jsonb # audience :jsonb
# campaign_status :integer default("active"), not null # campaign_status :integer default("active"), not null
# campaign_type :integer default("ongoing"), not null # campaign_type :integer default("ongoing"), not null
# description :text # description :text
# enabled :boolean default(TRUE) # enabled :boolean default(TRUE)
# message :text not null # message :text not null
# scheduled_at :datetime # scheduled_at :datetime
# title :string not null # title :string not null
# trigger_rules :jsonb # trigger_only_during_business_hours :boolean default(FALSE)
# created_at :datetime not null # trigger_rules :jsonb
# updated_at :datetime not null # created_at :datetime not null
# account_id :bigint not null # updated_at :datetime not null
# display_id :integer not null # account_id :bigint not null
# inbox_id :bigint not null # display_id :integer not null
# sender_id :integer # inbox_id :bigint not null
# sender_id :integer
# #
# Indexes # Indexes
# #

View file

@ -17,5 +17,6 @@ if resource.campaign_type == 'one_off'
json.audience resource.audience json.audience resource.audience
end end
json.trigger_rules resource.trigger_rules json.trigger_rules resource.trigger_rules
json.trigger_only_during_business_hours resource.trigger_only_during_business_hours
json.created_at resource.created_at json.created_at resource.created_at
json.updated_at resource.updated_at json.updated_at resource.updated_at

View file

@ -1,6 +1,7 @@
json.array! @campaigns do |campaign| json.array! @campaigns do |campaign|
json.id campaign.display_id json.id campaign.display_id
json.trigger_rules campaign.trigger_rules json.trigger_rules campaign.trigger_rules
json.trigger_only_during_business_hours campaign.trigger_only_during_business_hours
json.message campaign.message json.message campaign.message
json.sender campaign.sender&.slice(:name, :avatar_url) json.sender campaign.sender&.slice(:name, :avatar_url)
end end

View file

@ -0,0 +1,5 @@
class AddTriggerOnlyDuringBusinessHoursCollectToCampaigns < ActiveRecord::Migration[6.1]
def change
add_column :campaigns, :trigger_only_during_business_hours, :boolean, default: false
end
end

View file

@ -147,6 +147,7 @@ ActiveRecord::Schema.define(version: 2021_09_29_150415) do
t.integer "campaign_status", default: 0, null: false t.integer "campaign_status", default: 0, null: false
t.jsonb "audience", default: [] t.jsonb "audience", default: []
t.datetime "scheduled_at" t.datetime "scheduled_at"
t.boolean "trigger_only_during_business_hours", default: false
t.index ["account_id"], name: "index_campaigns_on_account_id" t.index ["account_id"], name: "index_campaigns_on_account_id"
t.index ["campaign_status"], name: "index_campaigns_on_campaign_status" t.index ["campaign_status"], name: "index_campaigns_on_campaign_status"
t.index ["campaign_type"], name: "index_campaigns_on_campaign_type" t.index ["campaign_type"], name: "index_campaigns_on_campaign_type"