feat: Add the ability to add new automation rule (#3459)

* Add automation modal

* Fix the v-model for automations

* Actions and Condition dropdowns for automations

* Fix merge conflicts

* Handle event change and confirmation

* Appends new action

* Removes actions

* Automation api integration

* Api integration for creating automations

* Registers vuex module to the global store

* Automations table

* Updarted labels and actions

* Integrate automation api

* Fixes the mutation error - removed the data key wrapper

* Fixed the automation condition models to work with respective event types

* Remove temporary fixes added to the api request

* Displa timestamp and automation status values

* Added the clone buton

* Removed uncessary helper method

* Specs for automations

* Handle WIP code

* Remove the payload wrap

* Fix the action query payload

* Removed unnecessary files

* Disabled Automations routes

* Ability to delete automations

* Fix specs

* Fixed merge conflicts

Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
Co-authored-by: fayazara <fayazara@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Muhsin Keloth
2022-02-02 18:46:07 +05:30
committed by GitHub
parent 1f3c5002b3
commit 91b9168fae
23 changed files with 1593 additions and 11 deletions

View File

@@ -0,0 +1,446 @@
<template>
<div class="column">
<woot-modal-header :header-title="$t('AUTOMATION.ADD.TITLE')" />
<div class="row modal-content">
<div class="medium-12 columns">
<woot-input
v-model="automation.name"
:label="$t('AUTOMATION.ADD.FORM.NAME.LABEL')"
type="text"
:class="{ error: $v.automation.name.$error }"
:error="
$v.automation.name.$error
? $t('AUTOMATION.ADD.FORM.NAME.ERROR')
: ''
"
:placeholder="$t('AUTOMATION.ADD.FORM.NAME.PLACEHOLDER')"
@blur="$v.automation.name.$touch"
/>
<woot-input
v-model="automation.description"
:label="$t('AUTOMATION.ADD.FORM.DESC.LABEL')"
type="text"
:class="{ error: $v.automation.description.$error }"
:error="
$v.automation.description.$error
? $t('AUTOMATION.ADD.FORM.DESC.ERROR')
: ''
"
:placeholder="$t('AUTOMATION.ADD.FORM.DESC.PLACEHOLDER')"
@blur="$v.automation.description.$touch"
/>
<div class="event_wrapper">
<label :class="{ error: $v.automation.event_name.$error }">
{{ $t('AUTOMATION.ADD.FORM.EVENT.LABEL') }}
<select v-model="automation.event_name" @change="onEventChange()">
<option
v-for="event in automationRuleEvents"
:key="event.key"
:value="event.key"
>
{{ event.value }}
</option>
</select>
<span v-if="$v.automation.event_name.$error" class="message">
{{ $t('AUTOMATION.ADD.FORM.EVENT.ERROR') }}
</span>
</label>
<p v-if="hasAutomationMutated" class="info-message">
{{ $t('AUTOMATION.FORM.RESET_MESSAGE') }}
</p>
</div>
<!-- // Conditions Start -->
<section>
<label>
{{ $t('AUTOMATION.ADD.FORM.CONDITIONS.LABEL') }}
</label>
<div class="medium-12 columns filters-wrap">
<filter-input-box
v-for="(condition, i) in automation.conditions"
:key="i"
v-model="automation.conditions[i]"
:filter-attributes="getAttributes(automation.event_name)"
:input-type="getInputType(automation.conditions[i].attribute_key)"
:operators="getOperators(automation.conditions[i].attribute_key)"
:dropdown-values="
getConditionDropdownValues(
automation.conditions[i].attribute_key
)
"
:show-query-operator="i !== automation.conditions.length - 1"
:v="$v.automation.conditions.$each[i]"
@resetFilter="resetFilter(i, automation.conditions[i])"
@removeFilter="removeFilter(i)"
/>
<div class="filter-actions">
<woot-button
icon="add"
color-scheme="success"
variant="smooth"
size="small"
@click="appendNewCondition"
>
{{ $t('AUTOMATION.ADD.CONDITION_BUTTON_LABEL') }}
</woot-button>
</div>
</div>
</section>
<!-- // Conditions End -->
<!-- // Actions Start -->
<section>
<label>
{{ $t('AUTOMATION.ADD.FORM.ACTIONS.LABEL') }}
</label>
<div class="medium-12 columns filters-wrap">
<automation-action-input
v-for="(action, i) in automation.actions"
:key="i"
v-model="automation.actions[i]"
:action-types="automationActionTypes"
:dropdown-values="
getActionDropdownValues(automation.actions[i].action_name)
"
:v="$v.automation.actions.$each[i]"
@removeAction="removeAction(i)"
/>
<div class="filter-actions">
<woot-button
icon="add"
color-scheme="success"
variant="smooth"
size="small"
@click="appendNewAction"
>
{{ $t('AUTOMATION.ADD.ACTION_BUTTON_LABEL') }}
</woot-button>
</div>
</div>
</section>
<!-- // Actions End -->
<div class="medium-12 columns">
<div class="modal-footer justify-content-end w-full">
<woot-button class="button clear" @click.prevent="onClose">
{{ $t('AUTOMATION.ADD.CANCEL_BUTTON_TEXT') }}
</woot-button>
<woot-button @click="submitAutomation">
{{ $t('AUTOMATION.ADD.SUBMIT') }}
</woot-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { required, requiredIf } from 'vuelidate/lib/validators';
import filterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
import automationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from '/app/javascript/shared/constants/countries.js';
import {
AUTOMATION_RULE_EVENTS,
AUTOMATION_ACTION_TYPES,
AUTOMATIONS,
} from './constants';
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator.js';
import actionQueryGenerator from 'dashboard/helper/actionQueryGenerator.js';
export default {
components: {
filterInputBox,
automationActionInput,
},
mixins: [alertMixin],
props: {
onClose: {
type: Function,
default: () => {},
},
},
validations: {
automation: {
name: {
required,
},
description: {
required,
},
event_name: {
required,
},
conditions: {
required,
$each: {
values: {
required: requiredIf(prop => {
return !(
prop.filter_operator === 'is_present' ||
prop.filter_operator === 'is_not_present'
);
}),
},
},
},
actions: {
required,
$each: {
action_params: {
required,
},
},
},
},
},
data() {
return {
automationTypes: AUTOMATIONS,
automationRuleEvent: AUTOMATION_RULE_EVENTS[0].key,
automationRuleEvents: AUTOMATION_RULE_EVENTS,
automationActionTypes: AUTOMATION_ACTION_TYPES,
automationMutated: false,
show: true,
automation: {
name: null,
description: null,
event_name: 'conversation_created',
conditions: [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
},
],
actions: [
{
action_name: 'assign_team',
action_params: [],
},
],
},
showDeleteConfirmationModal: false,
};
},
computed: {
conditions() {
return this.automationTypes[this.automation.event_name].conditions;
},
actions() {
return this.automationTypes[this.automation.event_name].actions;
},
filterAttributes() {
return this.filterTypes.map(type => {
return {
key: type.attributeKey,
name: type.attributeName,
attributeI18nKey: type.attributeI18nKey,
};
});
},
hasAutomationMutated() {
if (
this.automation.conditions[0].values ||
this.automation.actions[0].action_params.length
)
return true;
return false;
},
},
methods: {
onEventChange() {
if (this.automation.event_name === 'message_created') {
this.automation.conditions = [
{
attribute_key: 'message_type',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
},
];
} else {
this.automation.conditions = [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
},
];
}
this.automation.actions = [
{
action_name: 'assign_team',
action_params: [],
},
];
},
getAttributes(key) {
return this.automationTypes[key].conditions;
},
getInputType(key) {
const type = this.automationTypes[
this.automation.event_name
].conditions.find(condition => condition.key === key);
return type.inputType;
},
getOperators(key) {
const type = this.automationTypes[
this.automation.event_name
].conditions.find(condition => condition.key === key);
return type.filterOperators;
},
getConditionDropdownValues(type) {
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
switch (type) {
case 'status':
return [
...Object.keys(statusFilters).map(status => {
return {
id: status,
name: statusFilters[status].TEXT,
};
}),
{
id: 'all',
name: this.$t('CHAT_LIST.FILTER_ALL'),
},
];
case 'assignee_id':
return this.$store.getters['agents/getAgents'];
case 'contact':
return this.$store.getters['contacts/getContacts'];
case 'inbox_id':
return this.$store.getters['inboxes/getInboxes'];
case 'team_id':
return this.$store.getters['teams/getTeams'];
case 'campaign_id':
return this.$store.getters['campaigns/getAllCampaigns'].map(i => {
return {
id: i.id,
name: i.title,
};
});
case 'labels':
return this.$store.getters['labels/getLabels'].map(i => {
return {
id: i.id,
name: i.title,
};
});
case 'browser_language':
return languages;
case 'country_code':
return countries;
case 'message_type':
return [
{
id: 'incoming',
name: 'Incoming Message',
},
{
id: 'outgoing',
name: 'Outgoing Message',
},
];
default:
return undefined;
}
},
getActionDropdownValues(type) {
switch (type) {
case 'assign_team':
case 'send_message':
return this.$store.getters['teams/getTeams'];
case 'add_label':
return this.$store.getters['labels/getLabels'].map(i => {
return {
id: i.title,
name: i.title,
};
});
default:
return undefined;
}
},
appendNewCondition() {
this.automation.conditions.push({
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
});
},
appendNewAction() {
this.automation.actions.push({
action_name: 'assign_team',
action_params: [],
});
},
removeFilter(index) {
if (this.automation.conditions.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
} else {
this.automation.conditions.splice(index, 1);
}
},
removeAction(index) {
if (this.automation.actions.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
} else {
this.automation.actions.splice(index, 1);
}
},
submitAutomation() {
this.$v.$touch();
if (this.$v.$invalid) return;
this.automation.conditions[
this.automation.conditions.length - 1
].query_operator = null;
this.automation.conditions = filterQueryGenerator(
this.automation.conditions
).payload;
this.automation.actions = actionQueryGenerator(this.automation.actions);
this.$emit('saveAutomation', this.automation);
},
resetFilter(index, currentCondition) {
this.automation.conditions[index].filter_operator = this.automationTypes[
this.automation.event_name
].conditions.find(
condition => condition.key === currentCondition.attribute_key
).filterOperators[0].value;
this.automation.conditions[index].values = '';
},
showUserInput(operatorType) {
if (operatorType === 'is_present' || operatorType === 'is_not_present')
return false;
return true;
},
},
};
</script>
<style lang="scss" scoped>
.filters-wrap {
padding: var(--space-normal);
border-radius: var(--border-radius-large);
border: 1px solid var(--color-border);
background: var(--color-background-light);
margin-bottom: var(--space-normal);
}
.filter-actions {
margin-top: var(--space-normal);
}
.event_wrapper {
select {
margin: 0;
}
.info-message {
font-size: var(--font-size-mini);
color: #868686;
text-align: right;
}
margin-bottom: 1.6rem;
}
</style>

View File

@@ -4,9 +4,209 @@
color-scheme="success"
class-names="button--fixed-right-top"
icon="add-circle"
@click="openAddPopup()"
>
{{ $t('AUTOMATION.HEADER_BTN_TXT') }}
</woot-button>
<div class="row">
<div class="small-8 columns with-right-space">
<p
v-if="!uiFlags.isFetching && !records.length"
class="no-items-error-message"
>
{{ $t('AUTOMATION.LIST.404') }}
</p>
<woot-loading-state
v-if="uiFlags.isFetching"
:message="$t('AUTOMATION.LOADING')"
/>
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
<thead>
<th
v-for="thHeader in $t('AUTOMATION.LIST.TABLE_HEADER')"
:key="thHeader"
>
{{ thHeader }}
</th>
</thead>
<tbody>
<tr v-for="(automation, index) in records" :key="index">
<td>{{ automation.name }}</td>
<td>{{ automation.description }}</td>
<td>
<fluent-icon
v-if="automation.active"
icon="checkmark-square"
type="solid"
/>
<fluent-icon v-else icon="square" />
</td>
<td>{{ readableTime(automation.created_on) }}</td>
<td class="button-wrapper">
<!-- <woot-button
v-tooltip.top="$t('AUTOMATION.FORM.EDIT')"
variant="smooth"
size="tiny"
color-scheme="secondary"
class-names="grey-btn"
:is-loading="loading[automation.id]"
icon="edit"
@click="openEditPopup(automation)"
>
</woot-button>
<woot-button
v-tooltip.top="'Clone'"
variant="smooth"
size="tiny"
color-scheme="primary"
class-names="grey-btn"
:is-loading="loading[automation.id]"
icon="copy"
@click="openEditPopup(automation)"
>
</woot-button> -->
<woot-button
v-tooltip.top="$t('AUTOMATION.FORM.DELETE')"
variant="smooth"
color-scheme="alert"
size="tiny"
icon="dismiss-circle"
class-names="grey-btn"
:is-loading="loading[automation.id]"
@click="openDeletePopup(automation, index)"
>
</woot-button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="small-4 columns">
<span v-html="$t('AUTOMATION.SIDEBAR_TXT')"></span>
</div>
</div>
<woot-modal
:show.sync="showAddPopup"
size="medium"
:on-close="hideAddPopup"
>
<add-automation-rule
v-if="showAddPopup"
:on-close="hideAddPopup"
@saveAutomation="onCreateAutomation"
/>
</woot-modal>
<woot-delete-modal
:show.sync="showDeleteConfirmationPopup"
:on-close="closeDeletePopup"
:on-confirm="confirmDeletion"
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
:message="deleteMessage"
:confirm-text="deleteConfirmText"
:reject-text="deleteRejectText"
/>
</div>
</template>
<script></script>
<script>
import { mapGetters } from 'vuex';
import AddAutomationRule from './AddAutomationRule.vue';
import alertMixin from 'shared/mixins/alertMixin';
import timeMixin from 'dashboard/mixins/time';
export default {
components: {
AddAutomationRule,
},
mixins: [alertMixin, timeMixin],
data() {
return {
loading: {},
showAddPopup: false,
showEditPopup: false,
showDeleteConfirmationPopup: false,
selectedResponse: {},
};
},
computed: {
...mapGetters({
records: ['automations/getAutomations'],
uiFlags: 'automations/getUIFlags',
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('AUTOMATION.DELETE.CONFIRM.YES')} ${
this.selectedResponse.name
}`;
},
deleteRejectText() {
return `${this.$t('AUTOMATION.DELETE.CONFIRM.NO')} ${
this.selectedResponse.name
}`;
},
deleteMessage() {
return `${this.$t('AUTOMATION.DELETE.CONFIRM.MESSAGE')} ${
this.selectedResponse.name
} ?`;
},
},
mounted() {
this.$store.dispatch('automations/get');
},
methods: {
openAddPopup() {
this.showAddPopup = true;
},
hideAddPopup() {
this.showAddPopup = false;
},
openEditPopup(response) {
this.showEditPopup = true;
this.selectedResponse = response;
},
hideEditPopup() {
this.showEditPopup = false;
},
openDeletePopup(response) {
this.showDeleteConfirmationPopup = true;
this.selectedResponse = response;
},
closeDeletePopup() {
this.showDeleteConfirmationPopup = false;
},
confirmDeletion() {
this.loading[this.selectedResponse.id] = true;
this.closeDeletePopup();
this.deleteAutomation(this.selectedResponse.id);
},
async deleteAutomation(id) {
try {
await this.$store.dispatch('automations/delete', id);
this.showAlert(this.$t('AUTOMATION.DELETE.API.SUCCESS_MESSAGE'));
this.loading[this.selectedResponse.id] = false;
} catch (error) {
this.showAlert(this.$t('AUTOMATION.DELETE.API.ERROR_MESSAGE'));
}
},
async onCreateAutomation(payload) {
try {
await await this.$store.dispatch('automations/create', payload);
this.showAlert(this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE'));
this.hideAddPopup();
} catch (error) {
this.showAlert(this.$t('AUTOMATION.ADD.API.ERROR_MESSAGE'));
}
},
readableTime(date) {
return this.messageStamp(new Date(date), 'LLL d, h:mm a');
},
},
};
</script>
<style scoped>
.automation__status-checkbox {
margin: 0;
}
</style>

View File

@@ -9,7 +9,7 @@ export default {
component: SettingsContent,
props: {
headerTitle: 'AUTOMATION.HEADER',
icon: 'autocorrect',
icon: 'automation',
showNewButton: false,
},
children: [

View File

@@ -0,0 +1,229 @@
const OPERATOR_TYPES_1 = [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
];
const OPERATOR_TYPES_2 = [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
];
const OPERATOR_TYPES_3 = [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'is_present',
label: 'Is present',
},
{
value: 'is_not_present',
label: 'Is not present',
},
];
export const AUTOMATIONS = {
message_created: {
conditions: [
{
key: 'message_type',
name: 'Message Type',
attributeI18nKey: 'MESSAGE_TYPE',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'message_contains',
name: 'Message Contains',
attributeI18nKey: 'MESSAGE_CONTAINS',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
],
actions: [
{
key: 'assign_team',
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
{
key: 'add_label',
name: 'Add a label',
attributeI18nKey: 'ADD_LABEL',
},
{
key: 'send_message',
name: 'Send an email to team',
attributeI18nKey: 'SEND_MESSAGE',
},
],
},
conversation_created: {
conditions: [
{
key: 'status',
name: 'Status',
attributeI18nKey: 'STATUS',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'browser_language',
name: 'Browser Language',
attributeI18nKey: 'BROWSER_LANGUAGE',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'country_code',
name: 'Country',
attributeI18nKey: 'COUNTRY_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'referrer',
name: 'Referrer Link',
attributeI18nKey: 'REFERER_LINK',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
],
actions: [
{
key: 'assign_team',
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
{
key: 'send_message',
name: 'Send an email to team',
attributeI18nKey: 'SEND_MESSAGE',
},
{
key: 'assign_agent',
name: 'Assign an agent',
attributeI18nKey: 'ASSIGN_AGENT',
},
],
},
conversation_updated: {
conditions: [
{
key: 'status',
name: 'Status',
attributeI18nKey: 'STATUS',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'browser_language',
name: 'Browser Language',
attributeI18nKey: 'BROWSER_LANGUAGE',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'country_code',
name: 'Country',
attributeI18nKey: 'COUNTRY_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'referer',
name: 'Referrer Link',
attributeI18nKey: 'REFERER_LINK',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'assignee_id',
name: 'Assignee',
attributeI18nKey: 'ASSIGNEE_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_3,
},
{
key: 'team_id',
name: 'Team',
attributeI18nKey: 'TEAM_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_3,
},
],
actions: [
{
key: 'assign_team',
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
{
key: 'send_message',
name: 'Send an email to team',
attributeI18nKey: 'SEND_MESSAGE',
},
{
key: 'assign_agent',
name: 'Assign an agent',
attributeI18nKey: 'ASSIGN_AGENT',
attributeKey: 'assignee_id',
},
],
},
};
export const AUTOMATION_RULE_EVENTS = [
{
key: 'conversation_created',
value: 'Conversation Created',
},
{
key: 'conversation_updated',
value: 'Conversation Updated',
},
{
key: 'message_created',
value: 'Message Created',
},
];
export const AUTOMATION_ACTION_TYPES = [
{
key: 'assign_team',
label: 'Assign a team',
},
{
key: 'add_label',
label: 'Add a label',
},
{
key: 'send_message',
label: 'Send an email to team',
},
];

View File

@@ -11,6 +11,7 @@ import reports from './reports/reports.routes';
import campaigns from './campaigns/campaigns.routes';
import teams from './teams/teams.routes';
import attributes from './attributes/attributes.routes';
import automation from './automation/automation.routes';
import store from '../../../store';
export default {
@@ -38,5 +39,6 @@ export default {
...campaigns.routes,
...integrationapps.routes,
...attributes.routes,
...automation.routes,
],
};