+
+
+
diff --git a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue
index 46c64fc53..b3dd67dc3 100644
--- a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue
+++ b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue
@@ -32,6 +32,7 @@
v-for="attribute in filterAttributes"
:key="attribute.key"
:value="attribute.key"
+ :disabled="attribute.disabled"
>
{{ attribute.name }}
@@ -173,6 +174,10 @@ export default {
type: Array,
default: () => [],
},
+ customAttributeType: {
+ type: String,
+ default: '',
+ },
},
computed: {
attributeKey: {
diff --git a/app/javascript/dashboard/helper/actionQueryGenerator.js b/app/javascript/dashboard/helper/actionQueryGenerator.js
index 965c5b238..23cbdee68 100644
--- a/app/javascript/dashboard/helper/actionQueryGenerator.js
+++ b/app/javascript/dashboard/helper/actionQueryGenerator.js
@@ -22,7 +22,7 @@ const generatePayload = data => {
let payload = actions.map(item => {
if (Array.isArray(item.action_params)) {
item.action_params = formatArray(item.action_params);
- } else if (typeof item.values === 'object') {
+ } else if (typeof item.action_params === 'object') {
item.action_params = [item.action_params.id];
} else if (!item.action_params) {
item.action_params = [];
diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js
new file mode 100644
index 000000000..715a11294
--- /dev/null
+++ b/app/javascript/dashboard/helper/automationHelper.js
@@ -0,0 +1,242 @@
+import {
+ OPERATOR_TYPES_1,
+ OPERATOR_TYPES_3,
+ OPERATOR_TYPES_4,
+} from 'dashboard/routes/dashboard/settings/automation/operators';
+import filterQueryGenerator from './filterQueryGenerator';
+import actionQueryGenerator from './actionQueryGenerator';
+const MESSAGE_CONDITION_VALUES = [
+ {
+ id: 'incoming',
+ name: 'Incoming Message',
+ },
+ {
+ id: 'outgoing',
+ name: 'Outgoing Message',
+ },
+];
+
+export const getCustomAttributeInputType = key => {
+ const customAttributeMap = {
+ date: 'date',
+ text: 'plain_text',
+ list: 'search_select',
+ checkbox: 'search_select',
+ };
+
+ return customAttributeMap[key] || 'plain_text';
+};
+
+export const isACustomAttribute = (customAttributes, key) => {
+ return customAttributes.find(attr => {
+ return attr.attribute_key === key;
+ });
+};
+
+export const getCustomAttributeListDropdownValues = (
+ customAttributes,
+ type
+) => {
+ return customAttributes
+ .find(attr => attr.attribute_key === type)
+ .attribute_values.map(item => {
+ return {
+ id: item,
+ name: item,
+ };
+ });
+};
+
+export const isCustomAttributeCheckbox = (customAttributes, key) => {
+ return customAttributes.find(attr => {
+ return (
+ attr.attribute_key === key && attr.attribute_display_type === 'checkbox'
+ );
+ });
+};
+
+export const isCustomAttributeList = (customAttributes, type) => {
+ return customAttributes.find(attr => {
+ return (
+ attr.attribute_key === type && attr.attribute_display_type === 'list'
+ );
+ });
+};
+
+export const getOperatorTypes = key => {
+ const operatorMap = {
+ list: OPERATOR_TYPES_1,
+ text: OPERATOR_TYPES_3,
+ number: OPERATOR_TYPES_1,
+ link: OPERATOR_TYPES_1,
+ date: OPERATOR_TYPES_4,
+ checkbox: OPERATOR_TYPES_1,
+ };
+
+ return operatorMap[key] || OPERATOR_TYPES_1;
+};
+
+export const generateCustomAttributeTypes = (customAttributes, type) => {
+ return customAttributes.map(attr => {
+ return {
+ key: attr.attribute_key,
+ name: attr.attribute_display_name,
+ inputType: getCustomAttributeInputType(attr.attribute_display_type),
+ filterOperators: getOperatorTypes(attr.attribute_display_type),
+ customAttributeType: type,
+ };
+ });
+};
+
+export const generateConditionOptions = (options, key = 'id') => {
+ return options.map(i => {
+ return {
+ id: i[key],
+ name: i.title,
+ };
+ });
+};
+
+export const getActionOptions = ({ teams, labels, type }) => {
+ const actionsMap = {
+ assign_team: teams,
+ send_email_to_team: teams,
+ add_label: generateConditionOptions(labels, 'title'),
+ };
+ return actionsMap[type];
+};
+
+export const getConditionOptions = ({
+ agents,
+ booleanFilterOptions,
+ campaigns,
+ contacts,
+ countries,
+ customAttributes,
+ inboxes,
+ languages,
+ statusFilterOptions,
+ teams,
+ type,
+}) => {
+ if (isCustomAttributeCheckbox(customAttributes, type)) {
+ return booleanFilterOptions;
+ }
+
+ if (isCustomAttributeList(customAttributes, type)) {
+ return getCustomAttributeListDropdownValues(customAttributes, type);
+ }
+
+ const conditionFilterMaps = {
+ status: statusFilterOptions,
+ assignee_id: agents,
+ contact: contacts,
+ inbox_id: inboxes,
+ team_id: teams,
+ campaigns: generateConditionOptions(campaigns),
+ browser_language: languages,
+ country_code: countries,
+ message_type: MESSAGE_CONDITION_VALUES,
+ };
+
+ return conditionFilterMaps[type];
+};
+
+export const getFileName = (action, files = []) => {
+ const blobId = action.action_params[0];
+ if (!blobId) return '';
+ if (action.action_name === 'send_attachment') {
+ const file = files.find(item => item.blob_id === blobId);
+ if (file) return file.filename.toString();
+ }
+ return '';
+};
+
+export const getDefaultConditions = eventName => {
+ if (eventName === 'message_created') {
+ return [
+ {
+ attribute_key: 'message_type',
+ filter_operator: 'equal_to',
+ values: '',
+ query_operator: 'and',
+ custom_attribute_type: '',
+ },
+ ];
+ }
+ return [
+ {
+ attribute_key: 'status',
+ filter_operator: 'equal_to',
+ values: '',
+ query_operator: 'and',
+ custom_attribute_type: '',
+ },
+ ];
+};
+
+export const getDefaultActions = () => {
+ return [
+ {
+ action_name: 'assign_team',
+ action_params: [],
+ },
+ ];
+};
+
+export const filterCustomAttributes = customAttributes => {
+ return customAttributes.map(attr => {
+ return {
+ key: attr.attribute_key,
+ name: attr.attribute_display_name,
+ type: attr.attribute_display_type,
+ };
+ });
+};
+
+export const getStandardAttributeInputType = (automationTypes, event, key) => {
+ return automationTypes[event].conditions.find(item => item.key === key)
+ .inputType;
+};
+
+export const generateAutomationPayload = payload => {
+ const automation = JSON.parse(JSON.stringify(payload));
+ automation.conditions[automation.conditions.length - 1].query_operator = null;
+ automation.conditions = filterQueryGenerator(automation.conditions).payload;
+ automation.actions = actionQueryGenerator(automation.actions);
+ return automation;
+};
+
+export const isCustomAttribute = (attrs, key) => {
+ return attrs.find(attr => attr.key === key);
+};
+
+export const generateCustomAttributes = (
+ conversationAttributes = [],
+ contactAttribtues = [],
+ conversationlabel,
+ contactlabel
+) => {
+ const customAttributes = [];
+ if (conversationAttributes.length) {
+ customAttributes.push(
+ {
+ key: `conversation_custom_attribute`,
+ name: conversationlabel,
+ disabled: true,
+ },
+ ...conversationAttributes
+ );
+ }
+ if (contactAttribtues.length) {
+ customAttributes.push(
+ {
+ key: `contact_custom_attribute`,
+ name: contactlabel,
+ disabled: true,
+ },
+ ...contactAttribtues
+ );
+ }
+ return customAttributes;
+};
diff --git a/app/javascript/dashboard/helper/filterQueryGenerator.js b/app/javascript/dashboard/helper/filterQueryGenerator.js
index d549c490c..d519806bf 100644
--- a/app/javascript/dashboard/helper/filterQueryGenerator.js
+++ b/app/javascript/dashboard/helper/filterQueryGenerator.js
@@ -1,15 +1,3 @@
-const lowerCaseValues = (operator, values) => {
- if (operator === 'equal_to' || operator === 'not_equal_to') {
- values = values.map(val => {
- if (typeof val === 'string') {
- return val.toLowerCase();
- }
- return val;
- });
- }
- return values;
-};
-
const generatePayload = data => {
// Make a copy of data to avoid vue data reactivity issues
const filters = JSON.parse(JSON.stringify(data));
@@ -23,8 +11,6 @@ const generatePayload = data => {
} else {
item.values = [item.values];
}
- // Convert all values to lowerCase if operator_type is 'equal_to' or 'not_equal_to'
- item.values = lowerCaseValues(item.filter_operator, item.values);
return item;
});
// For every query added, the query_operator is set default to and so the
diff --git a/app/javascript/dashboard/helper/specs/filterQueryGenerator.spec.js b/app/javascript/dashboard/helper/specs/filterQueryGenerator.spec.js
index 6b38b094a..723ab8ef4 100644
--- a/app/javascript/dashboard/helper/specs/filterQueryGenerator.spec.js
+++ b/app/javascript/dashboard/helper/specs/filterQueryGenerator.spec.js
@@ -5,7 +5,7 @@ const testData = [
attribute_key: 'status',
filter_operator: 'equal_to',
values: [
- { id: 'PENDING', name: 'Pending' },
+ { id: 'pending', name: 'Pending' },
{ id: 'resolved', name: 'Resolved' },
],
query_operator: 'and',
@@ -18,7 +18,7 @@ const testData = [
account_id: 1,
auto_offline: true,
confirmed: true,
- email: 'fayazara@gmail.com',
+ email: 'fayaz@test.com',
available_name: 'Fayaz',
name: 'Fayaz',
role: 'agent',
@@ -52,7 +52,7 @@ const finalResult = {
{
attribute_key: 'id',
filter_operator: 'equal_to',
- values: ['this is a test'],
+ values: ['This is a test'],
},
],
};
diff --git a/app/javascript/dashboard/i18n/locale/en/automation.json b/app/javascript/dashboard/i18n/locale/en/automation.json
index e05ecd9ec..469df1c24 100644
--- a/app/javascript/dashboard/i18n/locale/en/automation.json
+++ b/app/javascript/dashboard/i18n/locale/en/automation.json
@@ -86,7 +86,9 @@
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
},
"CONDITION": {
- "DELETE_MESSAGE": "You need to have atleast one condition to save"
+ "DELETE_MESSAGE": "You need to have atleast one condition to save",
+ "CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
+ "CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
},
"ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save",
diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
index 164a6b94e..f9e763808 100644
--- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json
+++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
@@ -54,7 +54,8 @@
"MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select",
"ENTER_TO_REMOVE": "Press enter to remove",
- "SELECT_ONE": "Select one"
+ "SELECT_ONE": "Select one",
+ "SELECT": "Select"
}
},
"NOTIFICATIONS_PAGE": {
diff --git a/app/javascript/dashboard/mixins/automations/methodsMixin.js b/app/javascript/dashboard/mixins/automations/methodsMixin.js
new file mode 100644
index 000000000..ac9dddd06
--- /dev/null
+++ b/app/javascript/dashboard/mixins/automations/methodsMixin.js
@@ -0,0 +1,289 @@
+import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
+import countries from 'shared/constants/countries';
+import {
+ generateCustomAttributeTypes,
+ getActionOptions,
+ getConditionOptions,
+ getCustomAttributeInputType,
+ getOperatorTypes,
+ isACustomAttribute,
+ getFileName,
+ getDefaultConditions,
+ getDefaultActions,
+ filterCustomAttributes,
+ generateAutomationPayload,
+ getStandardAttributeInputType,
+ isCustomAttribute,
+ generateCustomAttributes,
+} from 'dashboard/helper/automationHelper';
+import { mapGetters } from 'vuex';
+
+export default {
+ computed: {
+ ...mapGetters({
+ agents: 'agents/getAgents',
+ campaigns: 'campaigns/getAllCampaigns',
+ contacts: 'contacts/getContacts',
+ inboxes: 'inboxes/getInboxes',
+ labels: 'labels/getLabels',
+ teams: 'teams/getTeams',
+ }),
+ booleanFilterOptions() {
+ return [
+ {
+ id: true,
+ name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
+ },
+ {
+ id: false,
+ name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
+ },
+ ];
+ },
+
+ statusFilterOptions() {
+ const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
+ return [
+ ...Object.keys(statusFilters).map(status => {
+ return {
+ id: status,
+ name: statusFilters[status].TEXT,
+ };
+ }),
+ {
+ id: 'all',
+ name: this.$t('CHAT_LIST.FILTER_ALL'),
+ },
+ ];
+ },
+ },
+ methods: {
+ getFileName,
+ onEventChange() {
+ this.automation.conditions = getDefaultConditions(
+ this.automation.event_name
+ );
+ this.automation.actions = getDefaultActions();
+ },
+ getAttributes(key) {
+ return this.automationTypes[key].conditions;
+ },
+ getInputType(key) {
+ const customAttribute = isACustomAttribute(this.allCustomAttributes, key);
+ if (customAttribute) {
+ return getCustomAttributeInputType(
+ customAttribute.attribute_display_type
+ );
+ }
+ const type = this.getAutomationType(key);
+ return type.inputType;
+ },
+ getOperators(key) {
+ if (this.mode === 'edit') {
+ const customAttribute = isACustomAttribute(
+ this.allCustomAttributes,
+ key
+ );
+ if (customAttribute) {
+ return getOperatorTypes(customAttribute.attribute_display_type);
+ }
+ }
+ const type = this.getAutomationType(key);
+ return type.filterOperators;
+ },
+ getAutomationType(key) {
+ return this.automationTypes[this.automation.event_name].conditions.find(
+ condition => condition.key === key
+ );
+ },
+ getCustomAttributeType(key) {
+ const type = this.automationTypes[
+ this.automation.event_name
+ ].conditions.find(i => i.key === key).customAttributeType;
+ return type;
+ },
+ getConditionDropdownValues(type) {
+ const {
+ agents,
+ allCustomAttributes: customAttributes,
+ booleanFilterOptions,
+ campaigns,
+ contacts,
+ inboxes,
+ statusFilterOptions,
+ teams,
+ } = this;
+ return getConditionOptions({
+ agents,
+ booleanFilterOptions,
+ campaigns,
+ contacts,
+ customAttributes,
+ inboxes,
+ statusFilterOptions,
+ teams,
+ languages,
+ countries,
+ type,
+ });
+ },
+ appendNewCondition() {
+ this.automation.conditions.push(
+ ...getDefaultConditions(this.automation.event_name)
+ );
+ },
+ appendNewAction() {
+ this.automation.actions.push(...getDefaultActions());
+ },
+ removeFilter(index) {
+ if (this.automation.conditions.length <= 1) {
+ this.showAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
+ } else {
+ this.automation.conditions.splice(index, 1);
+ }
+ },
+ removeAction(index) {
+ if (this.automation.actions.length <= 1) {
+ this.showAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
+ } else {
+ this.automation.actions.splice(index, 1);
+ }
+ },
+ submitAutomation() {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ const automation = generateAutomationPayload(this.automation);
+ this.$emit('saveAutomation', automation, this.mode);
+ },
+ 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(type) {
+ return !(type === 'is_present' || type === 'is_not_present');
+ },
+ showActionInput(action) {
+ if (action === 'send_email_to_team' || action === 'send_message')
+ return false;
+ const type = this.automationActionTypes.find(i => i.key === action)
+ .inputType;
+ return !!type;
+ },
+ resetAction(index) {
+ this.automation.actions[index].action_params = [];
+ },
+ manifestConditions(automation) {
+ const customAttributes = filterCustomAttributes(this.allCustomAttributes);
+ const conditions = automation.conditions.map(condition => {
+ const customAttr = isCustomAttribute(
+ customAttributes,
+ condition.attribute_key
+ );
+ let inputType = 'plain_text';
+ if (customAttr) {
+ inputType = getCustomAttributeInputType(customAttr.type);
+ } else {
+ inputType = getStandardAttributeInputType(
+ this.automationTypes,
+ automation.event_name,
+ condition.attribute_key
+ );
+ }
+ if (inputType === 'plain_text' || inputType === 'date') {
+ return {
+ ...condition,
+ values: condition.values[0],
+ };
+ }
+ return {
+ ...condition,
+ query_operator: condition.query_operator || 'and',
+ values: [
+ ...this.getConditionDropdownValues(condition.attribute_key),
+ ].filter(item => [...condition.values].includes(item.id)),
+ };
+ });
+ return conditions;
+ },
+ generateActionsArray(action) {
+ const params = action.action_params;
+ let actionParams = [];
+ const inputType = this.automationActionTypes.find(
+ item => item.key === action.action_name
+ ).inputType;
+ if (inputType === 'multi_select' || inputType === 'search_select') {
+ actionParams = [
+ ...this.getActionDropdownValues(action.action_name),
+ ].filter(item => [...params].includes(item.id));
+ } else if (inputType === 'team_message') {
+ actionParams = {
+ team_ids: [
+ ...this.getActionDropdownValues(action.action_name),
+ ].filter(item => [...params[0].team_ids].includes(item.id)),
+ message: params[0].message,
+ };
+ } else actionParams = [...params];
+ return actionParams;
+ },
+ manifestActions(automation) {
+ let actionParams = [];
+ const actions = automation.actions.map(action => {
+ if (action.action_params.length) {
+ actionParams = this.generateActionsArray(action);
+ }
+ return {
+ ...action,
+ action_params: actionParams,
+ };
+ });
+ return actions;
+ },
+ formatAutomation(automation) {
+ this.automation = {
+ ...automation,
+ conditions: this.manifestConditions(automation),
+ actions: this.manifestActions(automation),
+ };
+ },
+ getActionDropdownValues(type) {
+ const { labels, teams } = this;
+ return getActionOptions({ labels, teams, type });
+ },
+ manifestCustomAttributes() {
+ const conversationCustomAttributesRaw = this.$store.getters[
+ 'attributes/getAttributesByModel'
+ ]('conversation_attribute');
+
+ const contactCustomAttributesRaw = this.$store.getters[
+ 'attributes/getAttributesByModel'
+ ]('contact_attribute');
+ const conversationCustomAttributeTypes = generateCustomAttributeTypes(
+ conversationCustomAttributesRaw,
+ 'conversation_attribute'
+ );
+ const contactCustomAttributeTypes = generateCustomAttributeTypes(
+ contactCustomAttributesRaw,
+ 'contact_attribute'
+ );
+ let manifestedCustomAttributes = generateCustomAttributes(
+ conversationCustomAttributeTypes,
+ contactCustomAttributeTypes,
+ this.$t('AUTOMATION.CONDITION.CONVERSATION_CUSTOM_ATTR_LABEL'),
+ this.$t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
+ );
+ this.automationTypes.message_created.conditions.push(
+ ...manifestedCustomAttributes
+ );
+ this.automationTypes.conversation_created.conditions.push(
+ ...manifestedCustomAttributes
+ );
+ this.automationTypes.conversation_updated.conditions.push(
+ ...manifestedCustomAttributes
+ );
+ },
+ },
+};
diff --git a/app/javascript/dashboard/mixins/automations/validationsMixin.js b/app/javascript/dashboard/mixins/automations/validationsMixin.js
new file mode 100644
index 000000000..68705d2fd
--- /dev/null
+++ b/app/javascript/dashboard/mixins/automations/validationsMixin.js
@@ -0,0 +1,44 @@
+import { required, requiredIf } from 'vuelidate/lib/validators';
+
+export 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: requiredIf(prop => {
+ return !(
+ prop.action_name === 'mute_conversation' ||
+ prop.action_name === 'snooze_conversation' ||
+ prop.action_name === 'resolve_conversation'
+ );
+ }),
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue
index 66239b623..8b5a13585 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/automation/AddAutomationRule.vue
@@ -68,6 +68,9 @@
)
"
:show-query-operator="i !== automation.conditions.length - 1"
+ :custom-attribute-type="
+ getCustomAttributeType(automation.conditions[i].attribute_key)
+ "
:v="$v.automation.conditions.$each[i]"
@resetFilter="resetFilter(i, automation.conditions[i])"
@removeFilter="removeFilter(i)"
@@ -138,75 +141,32 @@
diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue
index 668b44d51..7230c6a27 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/automation/EditAutomationRule.vue
@@ -2,7 +2,7 @@