feat: One off campaign UI (#2621)

This commit is contained in:
Muhsin Keloth 2021-07-15 13:31:43 +05:30 committed by GitHub
parent aa7db90cd2
commit cf785123a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 473 additions and 107 deletions

View file

@ -0,0 +1,29 @@
@import '~vue2-datepicker/scss/index';
.mx-datepicker-popup {
z-index: 99999;
}
.date-picker {
.mx-datepicker {
width: 100%;
}
.mx-datepicker-range {
width: 320px;
}
.mx-input {
border: 1px solid var(--color-border);
border-radius: var(--border-radius-normal);
box-shadow: none;
display: flex;
height: 4.6rem;
}
.mx-input:disabled,
.mx-input[readonly] {
background-color: var(--white);
cursor: pointer;
}
}

View file

@ -12,6 +12,7 @@
@import 'foundation-settings';
@import 'helper-classes';
@import 'formulate';
@import 'date-picker';
@import 'foundation-sites/scss/foundation';
@import '~bourbon/core/bourbon';

View file

@ -1,5 +1,5 @@
<template>
<div class="woot-date-range-picker">
<div class="date-picker">
<date-picker
:range="true"
:confirm="true"
@ -15,7 +15,6 @@
<script>
import DatePicker from 'vue2-datepicker';
import 'vue2-datepicker/index.css';
export default {
components: { DatePicker },
props: {
@ -33,32 +32,9 @@ export default {
},
},
methods: {
updateValue(val) {
this.$emit('change', val);
},
handleChange(value) {
this.updateValue(value);
this.$emit('change', value);
},
},
};
</script>
<style lang="scss">
.woot-date-range-picker {
margin-left: var(--space-smaller);
.mx-input {
display: flex;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-normal);
box-shadow: none;
height: 4.6rem;
}
.mx-input:disabled,
.mx-input[readonly] {
background-color: var(--white);
cursor: pointer;
}
}
</style>

View file

@ -0,0 +1,47 @@
<template>
<div class="date-picker">
<date-picker
type="datetime"
:confirm="true"
:clearable="false"
:editable="false"
:confirm-text="confirmText"
:placeholder="placeholder"
:value="value"
:disabled-date="disableBeforeToday"
@change="handleChange"
/>
</div>
</template>
<script>
import addDays from 'date-fns/addDays';
import DatePicker from 'vue2-datepicker';
export default {
components: { DatePicker },
props: {
confirmText: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
value: {
type: Date,
default: () => [],
},
},
methods: {
handleChange(value) {
this.$emit('change', value);
},
disableBeforeToday(date) {
const yesterdayDate = addDays(new Date(), -1);
return date < yesterdayDate;
},
},
};
</script>

View file

@ -1,5 +1,5 @@
import { action } from '@storybook/addon-actions';
import WootDateRangePicker from '../DateRangePicker';
import WootDateRangePicker from '../DateRangePicker.vue';
export default {
title: 'Components/Date Picker/Date Range Picker',
@ -34,5 +34,5 @@ const Template = (args, { argTypes }) => ({
export const DateRangePicker = Template.bind({});
DateRangePicker.args = {
onChange: action('applied'),
value: [new Date(), new Date()],
value: new Date(),
};

View file

@ -0,0 +1,38 @@
import { action } from '@storybook/addon-actions';
import WootDateTimePicker from '../DateTimePicker.vue';
export default {
title: 'Components/Date Picker/Date Time Picker',
argTypes: {
confirmText: {
defaultValue: 'Apply',
control: {
type: 'text',
},
},
placeholder: {
defaultValue: 'Select date time',
control: {
type: 'text',
},
},
value: {
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { WootDateTimePicker },
template:
'<woot-date-time-picker v-bind="$props" @change="onChange"></woot-date-time-picker>',
});
export const DateTimePicker = Template.bind({});
DateTimePicker.args = {
onChange: action('applied'),
value: new Date(),
};

View file

@ -14,6 +14,17 @@
"PLACEHOLDER": "Please enter the title of campaign",
"ERROR": "Title is required"
},
"SCHEDULED_AT": {
"LABEL": "Scheduled time",
"PLACEHOLDER": "Please select the time",
"CONFIRM": "Confirm",
"ERROR": "Scheduled time is required"
},
"AUDIENCE": {
"LABEL": "Audience",
"PLACEHOLDER": "Select the customer labels",
"ERROR": "Audience is required"
},
"MESSAGE": {
"LABEL": "Message",
"PLACEHOLDER": "Please enter the message of campaign",
@ -72,6 +83,7 @@
"STATUS": "Status",
"SENDER": "Sender",
"URL": "URL",
"SCHEDULED_AT": "Scheduled time",
"TIME_ON_PAGE": "Time(Seconds)",
"CREATED_AT": "Created at"
},
@ -82,7 +94,9 @@
},
"STATUS": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
"DISABLED": "Disabled",
"COMPLETED": "Completed",
"ACTIVE": "Active"
},
"SENDER": {
"BOT": "Bot"

View file

@ -387,7 +387,21 @@ export default {
];
}
if (this.isATwilioChannel) {
if (this.isATwilioSMSChannel) {
return [
...visibleToAllChannelTabs,
{
key: 'campaign',
name: this.$t('INBOX_MGMT.TABS.CAMPAIGN'),
},
{
key: 'configuration',
name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'),
},
];
}
if (this.isATwilioWhatsappChannel) {
return [
...visibleToAllChannelTabs,
{
@ -459,6 +473,7 @@ export default {
this.selectedAgents = [];
this.$store.dispatch('agents/get');
this.$store.dispatch('teams/get');
this.$store.dispatch('labels/get');
this.$store.dispatch('inboxes/get').then(() => {
this.fetchAttachedAgents();
this.avatarUrl = this.inbox.avatar_url;

View file

@ -30,7 +30,46 @@
</span>
</label>
<label :class="{ error: $v.selectedSender.$error }">
<label v-if="isOnOffType">
{{ $t('CAMPAIGN.ADD.FORM.SCHEDULED_AT.LABEL') }}
<woot-date-time-picker
:value="scheduledAt"
:confirm-text="$t('CAMPAIGN.ADD.FORM.SCHEDULED_AT.CONFIRM')"
:placeholder="$t('CAMPAIGN.ADD.FORM.SCHEDULED_AT.PLACEHOLDER')"
@change="onChange"
/>
</label>
<label
v-if="isOnOffType"
:class="{ error: $v.selectedAudience.$error }"
>
{{ $t('CAMPAIGN.ADD.FORM.AUDIENCE.LABEL') }}
<multiselect
v-model="selectedAudience"
:options="audienceList"
track-by="id"
label="title"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:hide-selected="true"
:placeholder="$t('CAMPAIGN.ADD.FORM.AUDIENCE.PLACEHOLDER')"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
:deselect-label="$t('FORMS.MULTISELECT.ENTER_TO_REMOVE')"
@blur="$v.selectedAudience.$touch"
@select="$v.selectedAudience.$touch"
/>
<span v-if="$v.selectedAudience.$error" class="message">
{{ $t('CAMPAIGN.ADD.FORM.AUDIENCE.ERROR') }}
</span>
</label>
<label
v-if="isOngoingType"
:class="{ error: $v.selectedSender.$error }"
>
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.LABEL') }}
<select v-model="selectedSender">
<option
@ -47,6 +86,7 @@
</label>
<woot-input
v-if="isOngoingType"
v-model="endPoint"
:label="$t('CAMPAIGN.ADD.FORM.END_POINT.LABEL')"
type="text"
@ -58,6 +98,7 @@
@blur="$v.endPoint.$touch"
/>
<woot-input
v-if="isOngoingType"
v-model="timeOnPage"
:label="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.LABEL')"
type="text"
@ -70,7 +111,7 @@
:placeholder="$t('CAMPAIGN.ADD.FORM.TIME_ON_PAGE.PLACEHOLDER')"
@blur="$v.timeOnPage.$touch"
/>
<label>
<label v-if="isOngoingType">
<input
v-model="enabled"
type="checkbox"
@ -100,14 +141,23 @@
import { mapGetters } from 'vuex';
import { required, url, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import campaignMixin from 'shared/mixins/campaignMixin';
import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue';
export default {
mixins: [alertMixin],
components: { WootDateTimePicker },
mixins: [alertMixin, campaignMixin],
props: {
senderList: {
type: Array,
default: () => [],
},
audienceList: {
type: Array,
default: () => [],
},
},
data() {
return {
title: '',
@ -117,8 +167,11 @@ export default {
timeOnPage: 10,
show: true,
enabled: true,
scheduledAt: null,
selectedAudience: [],
};
},
validations: {
title: {
required,
@ -137,18 +190,37 @@ export default {
timeOnPage: {
required,
},
selectedAudience: {
isEmpty() {
return !!this.selectedAudience.length;
},
},
},
computed: {
...mapGetters({
uiFlags: 'campaigns/getUIFlags',
}),
currentInboxId() {
return this.$route.params.inboxId;
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
},
buttonDisabled() {
if (this.isOngoingType) {
return (
this.$v.message.$invalid ||
this.$v.title.$invalid ||
this.$v.selectedSender.$invalid ||
this.$v.endPoint.$invalid ||
this.$v.timeOnPage.$invalid ||
this.uiFlags.isCreating
);
}
return (
this.$v.message.$invalid ||
this.$v.title.$invalid ||
this.$v.selectedSender.$invalid ||
this.$v.endPoint.$invalid ||
this.$v.timeOnPage.$invalid ||
this.$v.selectedAudience.$invalid ||
this.uiFlags.isCreating
);
},
@ -166,9 +238,13 @@ export default {
onClose() {
this.$emit('on-close');
},
async addCampaign() {
try {
await this.$store.dispatch('campaigns/create', {
onChange(value) {
this.scheduledAt = value;
},
getCampaignDetails() {
let campaignDetails = null;
if (this.isOngoingType) {
campaignDetails = {
title: this.title,
message: this.message,
inbox_id: this.$route.params.inboxId,
@ -178,11 +254,34 @@ export default {
url: this.endPoint,
time_on_page: this.timeOnPage,
},
};
} else {
const audience = this.selectedAudience.map(item => {
return {
id: item.id,
type: 'Label',
};
});
campaignDetails = {
title: this.title,
message: this.message,
inbox_id: this.$route.params.inboxId,
scheduled_at: this.scheduledAt,
audience,
};
}
return campaignDetails;
},
async addCampaign() {
try {
const campaignDetails = this.getCampaignDetails();
await this.$store.dispatch('campaigns/create', campaignDetails);
this.showAlert(this.$t('CAMPAIGN.ADD.API.SUCCESS_MESSAGE'));
this.onClose();
} catch (error) {
this.showAlert(this.$t('CAMPAIGN.ADD.API.ERROR_MESSAGE'));
const errorMessage =
error?.response?.message || this.$t('CAMPAIGN.ADD.API.ERROR_MESSAGE');
this.showAlert(errorMessage);
}
},
},

View file

@ -9,12 +9,18 @@
:campaigns="records"
:show-empty-result="showEmptyResult"
:is-loading="uiFlags.isFetching"
:campaign-type="type"
@on-edit-click="openEditPopup"
@on-delete-click="openDeletePopup"
/>
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<add-campaign :sender-list="selectedAgents" @on-close="hideAddPopup" />
<add-campaign
:sender-list="selectedAgents"
:audience-list="labelList"
:campaign-type="type"
@on-close="hideAddPopup"
/>
</woot-modal>
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
<edit-campaign
@ -52,6 +58,10 @@ export default {
type: Array,
default: () => [],
},
type: {
type: String,
default: '',
},
},
data() {
return {
@ -66,6 +76,7 @@ export default {
...mapGetters({
records: 'campaigns/getCampaigns',
uiFlags: 'campaigns/getUIFlags',
labelList: 'labels/getLabels',
}),
showEmptyResult() {
const hasEmptyResults =

View file

@ -23,6 +23,8 @@ import Label from 'dashboard/components/ui/Label';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName';
import campaignMixin from 'shared/mixins/campaignMixin';
import timeMixin from 'dashboard/mixins/time';
export default {
components: {
@ -30,7 +32,7 @@ export default {
Spinner,
VeTable,
},
mixins: [clickaway],
mixins: [clickaway, timeMixin, campaignMixin],
props: {
campaigns: {
type: Array,
@ -46,9 +48,30 @@ export default {
},
},
data() {
return {
columns: [
computed: {
currentInboxId() {
return this.$route.params.inboxId;
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
},
tableData() {
if (this.isLoading) {
return [];
}
return this.campaigns.map(item => {
return {
...item,
url: item.trigger_rules.url,
timeOnPage: item.trigger_rules.time_on_page,
scheduledAt: item.scheduled_at
? this.messageStamp(new Date(item.scheduled_at), 'LLL d, h:mm a')
: '---',
};
});
},
columns() {
const visibleToAllTable = [
{
field: 'title',
key: 'title',
@ -76,54 +99,110 @@ export default {
);
},
},
];
if (this.isOngoingType) {
return [
...visibleToAllTable,
{
field: 'enabled',
key: 'enabled',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.STATUS'),
align: 'left',
renderBodyCell: ({ row }) => {
const labelText = row.enabled
? this.$t('CAMPAIGN.LIST.STATUS.ENABLED')
: this.$t('CAMPAIGN.LIST.STATUS.DISABLED');
const colorScheme = row.enabled ? 'success' : 'secondary';
return <Label title={labelText} colorScheme={colorScheme} />;
},
},
{
field: 'sender',
key: 'sender',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.SENDER'),
align: 'left',
renderBodyCell: ({ row }) => {
if (row.sender) return <UserAvatarWithName user={row.sender} />;
return this.$t('CAMPAIGN.LIST.SENDER.BOT');
},
},
{
field: 'url',
key: 'url',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.URL'),
align: 'left',
renderBodyCell: ({ row }) => (
<div class="text-truncate">
<a
target="_blank"
rel="noopener noreferrer nofollow"
href={row.url}
title={row.url}
>
{row.url}
</a>
</div>
),
},
{
field: 'timeOnPage',
key: 'timeOnPage',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.TIME_ON_PAGE'),
align: 'left',
},
{
field: 'buttons',
key: 'buttons',
title: '',
align: 'left',
renderBodyCell: row => (
<div class="button-wrapper">
<WootButton
variant="clear"
icon="ion-edit"
color-scheme="secondary"
classNames="grey-btn"
onClick={() => this.$emit('on-edit-click', row)}
>
{this.$t('CAMPAIGN.LIST.BUTTONS.EDIT')}
</WootButton>
<WootButton
variant="link"
icon="ion-close-circled"
color-scheme="secondary"
onClick={() => this.$emit('on-delete-click', row)}
>
{this.$t('CAMPAIGN.LIST.BUTTONS.DELETE')}
</WootButton>
</div>
),
},
];
}
return [
...visibleToAllTable,
{
field: 'enabled',
key: 'enabled',
field: 'campaign_status',
key: 'campaign_status',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.STATUS'),
align: 'left',
renderBodyCell: ({ row }) => {
const labelText = row.enabled
? this.$t('CAMPAIGN.LIST.STATUS.ENABLED')
: this.$t('CAMPAIGN.LIST.STATUS.DISABLED');
const colorScheme = row.enabled ? 'success' : 'secondary';
const labelText =
row.campaign_status === 'completed'
? this.$t('CAMPAIGN.LIST.STATUS.COMPLETED')
: this.$t('CAMPAIGN.LIST.STATUS.ACTIVE');
const colorScheme =
row.campaign_status === 'completed' ? 'secondary' : 'success';
return <Label title={labelText} colorScheme={colorScheme} />;
},
},
{
field: 'sender',
key: 'sender',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.SENDER'),
align: 'left',
renderBodyCell: ({ row }) => {
if (row.sender) return <UserAvatarWithName user={row.sender} />;
return this.$t('CAMPAIGN.LIST.SENDER.BOT');
},
},
{
field: 'url',
key: 'url',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.URL'),
align: 'left',
renderBodyCell: ({ row }) => (
<div class="text-truncate">
<a
target="_blank"
rel="noopener noreferrer nofollow"
href={row.url}
title={row.url}
>
{row.url}
</a>
</div>
),
},
{
field: 'timeOnPage',
key: 'timeOnPage',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.TIME_ON_PAGE'),
field: 'scheduledAt',
key: 'scheduledAt',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.SCHEDULED_AT'),
align: 'left',
},
{
field: 'buttons',
key: 'buttons',
@ -131,15 +210,6 @@ export default {
align: 'left',
renderBodyCell: row => (
<div class="button-wrapper">
<WootButton
variant="clear"
icon="ion-edit"
color-scheme="secondary"
classNames="grey-btn"
onClick={() => this.$emit('on-edit-click', row)}
>
{this.$t('CAMPAIGN.LIST.BUTTONS.EDIT')}
</WootButton>
<WootButton
variant="link"
icon="ion-close-circled"
@ -151,21 +221,7 @@ export default {
</div>
),
},
],
};
},
computed: {
tableData() {
if (this.isLoading) {
return [];
}
return this.campaigns.map(item => {
return {
...item,
url: item.trigger_rules.url,
timeOnPage: item.trigger_rules.time_on_page,
};
});
];
},
},
};

View file

@ -8,6 +8,7 @@
>
{{ $t('REPORT.DOWNLOAD_AGENT_REPORTS') }}
</woot-button>
<report-date-range-selector @date-range-change="onDateRangeChange" />
<div class="row">
<woot-report-stats-card

View file

@ -17,6 +17,7 @@
</div>
<woot-date-range-picker
v-if="isDateRangeSelected"
show-range
:value="customDateRange"
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
@ -89,3 +90,9 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.date-picker {
margin-left: var(--space-smaller);
}
</style>

View file

@ -0,0 +1,4 @@
export const CAMPAIGN_TYPES = {
ONGOING: 'ongoing',
ONE_OFF: 'one_off',
};

View file

@ -0,0 +1,19 @@
import { CAMPAIGN_TYPES } from '../constants/campaign';
import inboxMixin from './inboxMixin';
export default {
mixins: [inboxMixin],
computed: {
campaignType() {
if (this.isAWebWidgetInbox) {
return CAMPAIGN_TYPES.ONGOING;
}
return CAMPAIGN_TYPES.ONE_OFF;
},
isOngoingType() {
return this.campaignType === CAMPAIGN_TYPES.ONGOING;
},
isOnOffType() {
return this.campaignType === CAMPAIGN_TYPES.ONE_OFF;
},
},
};

View file

@ -0,0 +1,49 @@
import { shallowMount } from '@vue/test-utils';
import campaignMixin from '../campaignMixin';
import inboxMixin from '../inboxMixin';
describe('campaignMixin', () => {
it('returns the correct campaign type', () => {
const Component = {
render() {},
mixins: [campaignMixin, inboxMixin],
data() {
return {
inbox: {
channel_type: 'Channel::TwilioSms',
phone_number: '+91944444444',
},
};
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.campaignType).toBe('one_off');
});
it('isOnOffType returns true if campaign type is ongoing', () => {
const Component = {
render() {},
mixins: [campaignMixin, inboxMixin],
data() {
return { inbox: { channel_type: 'Channel::WebWidget' } };
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isOngoingType).toBe(true);
});
it('isOngoingType returns true if campaign type is one_off', () => {
const Component = {
render() {},
mixins: [campaignMixin, inboxMixin],
data() {
return {
inbox: {
channel_type: 'Channel::TwilioSms',
phone_number: '+91944444444',
},
};
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isOnOffType).toBe(true);
});
});