feat: Campaign table (#2212)

* code cleanup

* add campaign table

* update locale texts

* locale text cleanup

* Rename selectedAgent with selectedSender in add campaign form

* code cleanup

* remove timer mixin

* update avatar size to 20px

* add border for table

* add campaigns get action specs

* rename campaign table component

* fix style issues

* update sender list based on inbox permission

* style fixes

* review fixes

* add campaign sender component

* replace wootsubmit button with wootbutton

* update scroll width

* replace campaign status with woot label

* changes as per review

* style fixes

* remove unused code

* disable campaign in inbox settings page

* review fixes
This commit is contained in:
Muhsin Keloth
2021-05-06 10:46:59 +05:30
committed by GitHub
parent 381c358ffd
commit 6245a10a70
7 changed files with 344 additions and 41 deletions

View File

@@ -3,9 +3,6 @@
"HEADER": "Campaigns", "HEADER": "Campaigns",
"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.", "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", "HEADER_BTN_TXT": "Create a campaign",
"LIST": {
"404": "There are no campaigns created for this inbox."
},
"ADD": { "ADD": {
"TITLE": "Create a campaign", "TITLE": "Create a campaign",
"DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.", "DESC": "Proactive messages allow the customer to send outbound messages to their contacts which would trigger more conversations.",
@@ -44,6 +41,27 @@
"SUCCESS_MESSAGE": "Campaign created successfully", "SUCCESS_MESSAGE": "Campaign created successfully",
"ERROR_MESSAGE": "There was an error. Please try again." "ERROR_MESSAGE": "There was an error. Please try again."
} }
},
"LIST": {
"LOADING_MESSAGE": "Loading campaigns...",
"TABLE_HEADER": {
"TITLE": "Title",
"MESSAGE": "Message",
"STATUS": "Status",
"SENDER": "Sender",
"URL": "URL",
"TIME_ON_PAGE": "Time(Seconds)",
"CREATED_AT": "Created at"
},
"BUTTONS": {
"ADD": "Add",
"EDIT": "Edit",
"DELETE": "Delete"
},
"STATUS": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
}
} }
} }
} }

View File

@@ -258,7 +258,7 @@
<weekly-availability :inbox="inbox" /> <weekly-availability :inbox="inbox" />
</div> </div>
<div v-if="selectedTabKey === 'campaign'"> <div v-if="selectedTabKey === 'campaign'">
<campaign /> <campaign :selected-agents="selectedAgents" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -32,18 +32,18 @@
</div> </div>
<div class="medium-12 columns"> <div class="medium-12 columns">
<label :class="{ error: $v.selectedAgent.$error }"> <label :class="{ error: $v.selectedSender.$error }">
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.LABEL') }} {{ $t('CAMPAIGN.ADD.FORM.SENT_BY.LABEL') }}
<select v-model="selectedAgent"> <select v-model="selectedSender">
<option <option
v-for="agent in agentsList" v-for="sender in senderList"
:key="agent.name" :key="sender.name"
:value="agent.name" :value="sender.id"
> >
{{ agent.name }} {{ sender.name }}
</option> </option>
</select> </select>
<span v-if="$v.selectedAgent.$error" class="message"> <span v-if="$v.selectedSender.$error" class="message">
{{ $t('CAMPAIGN.ADD.FORM.SENT_BY.ERROR') }} {{ $t('CAMPAIGN.ADD.FORM.SENT_BY.ERROR') }}
</span> </span>
</label> </label>
@@ -119,6 +119,10 @@ export default {
}, },
mixins: [alertMixin], mixins: [alertMixin],
props: { props: {
senderList: {
type: Array,
default: () => [],
},
onClose: { onClose: {
type: Function, type: Function,
default: () => {}, default: () => {},
@@ -128,7 +132,7 @@ export default {
return { return {
title: '', title: '',
message: '', message: '',
selectedAgent: '', selectedSender: '',
endPoint: '', endPoint: '',
timeOnPage: 10, timeOnPage: 10,
show: true, show: true,
@@ -142,7 +146,7 @@ export default {
message: { message: {
required, required,
}, },
selectedAgent: { selectedSender: {
required, required,
}, },
endPoint: { endPoint: {
@@ -157,18 +161,13 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
agentList: 'agents/getAgents',
uiFlags: 'campaigns/getUIFlags', uiFlags: 'campaigns/getUIFlags',
}), }),
agentsList() {
return this.agentList;
},
buttonDisabled() { buttonDisabled() {
return ( return (
this.$v.message.$invalid || this.$v.message.$invalid ||
this.$v.title.$invalid || this.$v.title.$invalid ||
this.$v.selectedAgent.$invalid || this.$v.selectedSender.$invalid ||
this.$v.endPoint.$invalid || this.$v.endPoint.$invalid ||
this.$v.timeOnPage.$invalid || this.$v.timeOnPage.$invalid ||
this.uiFlags.isCreating this.uiFlags.isCreating
@@ -182,7 +181,7 @@ 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,
sender_id: this.selectedAgent, sender_id: this.selectedSender,
enabled: this.enabled, enabled: this.enabled,
trigger_rules: { trigger_rules: {
url: this.endPoint, url: this.endPoint,

View File

@@ -1,41 +1,37 @@
<template> <template>
<div class="column content-box"> <div class="column content-box">
<div v-if="campaigns.length" class="row button-wrapper"> <div class="row button-wrapper">
<woot-button @click="openAddPopup"> <woot-button @click="openAddPopup">
<i class="icon ion-android-add-circle"></i> <i class="icon ion-android-add-circle"></i>
{{ $t('CAMPAIGN.HEADER_BTN_TXT') }} {{ $t('CAMPAIGN.HEADER_BTN_TXT') }}
</woot-button> </woot-button>
</div> </div>
<campaigns-table
:campaigns="records"
:show-empty-state="showEmptyResult"
:is-loading="uiFlags.isFetching"
/>
<div v-if="!campaigns.length" class="row">
<div class="small-8 columns">
<p class="no-items-error-message">
{{ $t('CAMPAIGN.LIST.404') }}
<a @click="openAddPopup">
{{ $t('CAMPAIGN.HEADER_BTN_TXT') }}
</a>
</p>
</div>
<div class="small-4 columns">
<span>
<p>
<b> {{ $t('CAMPAIGN.HEADER') }}</b>
</p>
<p v-html="$t('CAMPAIGN.SIDEBAR_TXT')" />
</span>
</div>
</div>
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup"> <woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<add-campaign :on-close="hideAddPopup" /> <add-campaign :on-close="hideAddPopup" :sender-list="selectedAgents" />
</woot-modal> </woot-modal>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import AddCampaign from './AddCampaign'; import AddCampaign from './AddCampaign';
import CampaignsTable from './CampaignsTable';
export default { export default {
components: { components: {
AddCampaign, AddCampaign,
CampaignsTable,
},
props: {
selectedAgents: {
type: Array,
default: () => [],
},
}, },
data() { data() {
return { return {
@@ -43,6 +39,19 @@ export default {
showAddPopup: false, showAddPopup: false,
}; };
}, },
computed: {
...mapGetters({
records: 'campaigns/getCampaigns',
uiFlags: 'campaigns/getUIFlags',
}),
showEmptyResult() {
const hasEmptyResults = this.records.length === 0;
return hasEmptyResults;
},
},
mounted() {
this.$store.dispatch('campaigns/get');
},
methods: { methods: {
openAddPopup() { openAddPopup() {
this.showAddPopup = true; this.showAddPopup = true;
@@ -58,5 +67,6 @@ export default {
.button-wrapper { .button-wrapper {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding-bottom: var(--space-one);
} }
</style> </style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="row--user-block">
<Thumbnail
:src="sender.thumbnail"
size="20px"
:username="sender.name"
:status="sender.availability_status"
/>
<div>
<h6 class="text-block-title text-truncate">
{{ sender.name }}
</h6>
</div>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
export default {
components: {
Thumbnail,
},
props: {
sender: {
type: Object,
default: () => {},
},
},
};
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/mixins';
.row--user-block {
align-items: center;
display: flex;
text-align: left;
.user-name {
margin: 0;
text-transform: capitalize;
}
.user-thumbnail-box {
margin-right: var(--space-small);
}
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<section class="campaigns-table-wrap">
<ve-table
:columns="columns"
scroll-width="155rem"
:table-data="tableData"
:border-around="true"
/>
<empty-state v-if="showEmptyResult" :title="$t('CAMPAIGN.LIST.404')" />
<div v-if="isLoading" class="campaign--loader">
<spinner />
<span>{{ $t('CAMPAIGN.LIST.LOADING_MESSAGE') }}</span>
</div>
</section>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { VeTable } from 'vue-easytable';
import Spinner from 'shared/components/Spinner.vue';
import Label from 'dashboard/components/ui/Label';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import CampaignSender from './CampaignSender';
export default {
components: {
EmptyState,
Spinner,
VeTable,
},
mixins: [clickaway],
props: {
campaigns: {
type: Array,
default: () => [],
},
showEmptyResult: {
type: Boolean,
default: false,
},
isLoading: {
type: Boolean,
default: false,
},
},
data() {
return {
columns: [
{
field: 'title',
key: 'title',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.TITLE'),
fixed: 'left',
align: 'left',
renderBodyCell: ({ row }) => (
<div class="row--title-block">
<h6 class="sub-block-title title text-truncate">{row.title}</h6>
</div>
),
},
{
field: 'message',
key: 'message',
title: this.$t('CAMPAIGN.LIST.TABLE_HEADER.MESSAGE'),
align: 'left',
width: 350,
renderBodyCell: ({ row }) => {
return (
<div class="text-truncate">
<span title={row.message}>{row.message}</span>
</div>
);
},
},
{
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 <CampaignSender sender={row.sender} />;
return '---';
},
},
{
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: () => (
<div class="button-wrapper">
<WootButton
variant="clear"
icon="ion-edit"
color-scheme="secondary"
classNames="hollow grey-btn"
click="openEditPopup(label)"
>
{this.$t('CAMPAIGN.LIST.BUTTONS.EDIT')}
</WootButton>
</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,
};
});
},
},
};
</script>
<style lang="scss" scoped>
.campaigns-table-wrap::v-deep {
.ve-table {
padding-bottom: var(--space-large);
thead.ve-table-header .ve-table-header-tr .ve-table-header-th {
font-size: var(--font-size-mini);
padding: var(--space-small) var(--space-two);
}
tbody.ve-table-body .ve-table-body-tr .ve-table-body-td {
padding: var(--space-slab) var(--space-two);
}
}
.row--title-block {
align-items: center;
display: flex;
text-align: left;
.title {
font-size: var(--font-size-small);
margin: 0;
text-transform: capitalize;
}
}
.label {
padding: var(--space-smaller) var(--space-small);
}
}
.campaign--loader {
align-items: center;
display: flex;
font-size: var(--font-size-default);
justify-content: center;
padding: var(--space-big);
}
.button-wrapper {
justify-content: space-evenly;
display: flex;
flex-direction: row;
min-width: 20rem;
}
</style>

View File

@@ -8,6 +8,25 @@ global.axios = axios;
jest.mock('axios'); jest.mock('axios');
describe('#actions', () => { describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: campaignList });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_CAMPAIGN_UI_FLAG, { isFetching: true }],
[types.default.SET_CAMPAIGNS, campaignList],
[types.default.SET_CAMPAIGN_UI_FLAG, { isFetching: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_CAMPAIGN_UI_FLAG, { isFetching: true }],
[types.default.SET_CAMPAIGN_UI_FLAG, { isFetching: false }],
]);
});
});
describe('#create', () => { describe('#create', () => {
it('sends correct actions if API is success', async () => { it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: campaignList[0] }); axios.post.mockResolvedValue({ data: campaignList[0] });