fix: Automation Bugs and minor enhancements (#3936)

This commit is contained in:
Fayaz Ahmed 2022-02-15 23:36:29 +05:30 committed by GitHub
parent e345a4486d
commit 5ad6db07b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 203 additions and 129 deletions

View file

@ -69,13 +69,13 @@ const settings = accountId => ({
), ),
toStateName: 'attributes_list', toStateName: 'attributes_list',
}, },
// { {
// icon: 'automation', icon: 'automation',
// label: 'AUTOMATION', label: 'AUTOMATION',
// hasSubMenu: false, hasSubMenu: false,
// toState: frontendURL(`accounts/${accountId}/settings/automation/list`), toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
// toStateName: 'automation_list', toStateName: 'automation_list',
// }, },
{ {
icon: 'chat-multiple', icon: 'chat-multiple',
label: 'CANNED_RESPONSES', label: 'CANNED_RESPONSES',

View file

@ -15,6 +15,13 @@
size="14" size="14"
/> />
{{ $t(`SIDEBAR.${menuItem.label}`) }} {{ $t(`SIDEBAR.${menuItem.label}`) }}
<span
v-if="menuItem.label === 'AUTOMATION'"
data-view-component="true"
label="Beta"
class="beta"
>Beta
</span>
</router-link> </router-link>
<ul v-if="hasSubMenu" class="nested vertical menu"> <ul v-if="hasSubMenu" class="nested vertical menu">
@ -221,4 +228,17 @@ export default {
color: var(--w-500); color: var(--w-500);
} }
} }
.beta {
padding-right: var(--space-smaller) !important;
padding-left: var(--space-smaller) !important;
margin-left: var(--space-half) !important;
display: inline-block;
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
line-height: 18px;
border: 1px solid transparent;
border-radius: 2em;
color: var(--g-800);
border-color: var(--g-700);
}
</style> </style>

View file

@ -1,6 +1,6 @@
{ {
"AUTOMATION": { "AUTOMATION": {
"HEADER": "Automation", "HEADER": "Automations",
"HEADER_BTN_TXT": "Add Automation Rule", "HEADER_BTN_TXT": "Add Automation Rule",
"LOADING": "Fetching automation rules", "LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>", "SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
@ -64,7 +64,7 @@
}, },
"EDIT": { "EDIT": {
"TITLE": "Edit Automation Rule", "TITLE": "Edit Automation Rule",
"SUBMIT": "Edit", "SUBMIT": "Update",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"API": { "API": {
"SUCCESS_MESSAGE": "Automation rule updated successfully", "SUCCESS_MESSAGE": "Automation rule updated successfully",
@ -84,6 +84,12 @@
"DELETE": "Delete", "DELETE": "Delete",
"CANCEL": "Cancel", "CANCEL": "Cancel",
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below" "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"
},
"ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save"
} }
} }
} }

View file

@ -351,7 +351,7 @@ export default {
getActionDropdownValues(type) { getActionDropdownValues(type) {
switch (type) { switch (type) {
case 'assign_team': case 'assign_team':
case 'send_message': case 'send_email_to_team':
return this.$store.getters['teams/getTeams']; return this.$store.getters['teams/getTeams'];
case 'add_label': case 'add_label':
return this.$store.getters['labels/getLabels'].map(i => { return this.$store.getters['labels/getLabels'].map(i => {
@ -365,12 +365,24 @@ export default {
} }
}, },
appendNewCondition() { appendNewCondition() {
this.automation.conditions.push({ switch (this.automation.event_name) {
attribute_key: 'status', case 'message_created':
filter_operator: 'equal_to', this.automation.conditions.push({
values: '', attribute_key: 'message_type',
query_operator: 'and', filter_operator: 'equal_to',
}); values: '',
query_operator: 'and',
});
break;
default:
this.automation.conditions.push({
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
});
break;
}
}, },
appendNewAction() { appendNewAction() {
this.automation.actions.push({ this.automation.actions.push({

View file

@ -121,10 +121,10 @@
variant="clear" variant="clear"
@click.prevent="onClose" @click.prevent="onClose"
> >
{{ $t('AUTOMATION.ADD.CANCEL_BUTTON_TEXT') }} {{ $t('AUTOMATION.EDIT.CANCEL_BUTTON_TEXT') }}
</woot-button> </woot-button>
<woot-button @click="submitAutomation"> <woot-button @click="submitAutomation">
{{ $t('AUTOMATION.ADD.SUBMIT') }} {{ $t('AUTOMATION.EDIT.SUBMIT') }}
</woot-button> </woot-button>
</div> </div>
</div> </div>
@ -351,9 +351,9 @@ export default {
getActionDropdownValues(type) { getActionDropdownValues(type) {
switch (type) { switch (type) {
case 'assign_team': case 'assign_team':
case 'send_message': case 'send_email_to_team':
return this.$store.getters['teams/getTeams']; return this.$store.getters['teams/getTeams'];
case 'add_label': case 'add_label':
return this.$store.getters['labels/getLabels'].map(i => { return this.$store.getters['labels/getLabels'].map(i => {
return { return {
id: i.title, id: i.title,
@ -365,12 +365,32 @@ export default {
} }
}, },
appendNewCondition() { appendNewCondition() {
this.automation.conditions.push({ if (
attribute_key: 'status', !this.automation.conditions[this.automation.conditions.length - 1]
filter_operator: 'equal_to', .query_operator
values: '', ) {
query_operator: 'and', this.automation.conditions[
}); this.automation.conditions.length - 1
].query_operator = 'and';
}
switch (this.automation.event_name) {
case 'message_created':
this.automation.conditions.push({
attribute_key: 'message_type',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
});
break;
default:
this.automation.conditions.push({
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
});
break;
}
}, },
appendNewAction() { appendNewAction() {
this.automation.actions.push({ this.automation.actions.push({
@ -380,14 +400,14 @@ export default {
}, },
removeFilter(index) { removeFilter(index) {
if (this.automation.conditions.length <= 1) { if (this.automation.conditions.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR')); this.showAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
} else { } else {
this.automation.conditions.splice(index, 1); this.automation.conditions.splice(index, 1);
} }
}, },
removeAction(index) { removeAction(index) {
if (this.automation.actions.length <= 1) { if (this.automation.actions.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR')); this.showAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
} else { } else {
this.automation.actions.splice(index, 1); this.automation.actions.splice(index, 1);
} }
@ -421,8 +441,9 @@ export default {
const formattedConditions = automation.conditions.map(condition => { const formattedConditions = automation.conditions.map(condition => {
const inputType = this.automationTypes[ const inputType = this.automationTypes[
automation.event_name automation.event_name
].conditions.find(item => item.key === condition.attribute_key) ].conditions.find(
.inputType; item => item.key === condition.attribute_key
).inputType;
if (inputType === 'plain_text') { if (inputType === 'plain_text') {
return { return {
...condition, ...condition,

View file

@ -219,7 +219,7 @@ export default {
const action = const action =
mode === 'EDIT' ? 'automations/update' : 'automations/create'; mode === 'EDIT' ? 'automations/update' : 'automations/create';
const successMessage = const successMessage =
mode === 'edit' mode === 'EDIT'
? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE') ? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
: this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE'); : this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
@ -229,7 +229,7 @@ export default {
this.hideEditPopup(); this.hideEditPopup();
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
mode === 'edit' mode === 'EDIT'
? this.$t('AUTOMATION.EDIT.API.ERROR_MESSAGE') ? this.$t('AUTOMATION.EDIT.API.ERROR_MESSAGE')
: this.$t('AUTOMATION.ADD.API.ERROR_MESSAGE'); : this.$t('AUTOMATION.ADD.API.ERROR_MESSAGE');
this.showAlert(errorMessage); this.showAlert(errorMessage);

View file

@ -58,8 +58,8 @@ export const AUTOMATIONS = {
filterOperators: OPERATOR_TYPES_1, filterOperators: OPERATOR_TYPES_1,
}, },
{ {
key: 'message_contains', key: 'content',
name: 'Message Contains', name: 'Message Content',
attributeI18nKey: 'MESSAGE_CONTAINS', attributeI18nKey: 'MESSAGE_CONTAINS',
inputType: 'plain_text', inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2, filterOperators: OPERATOR_TYPES_2,
@ -76,11 +76,11 @@ export const AUTOMATIONS = {
name: 'Add a label', name: 'Add a label',
attributeI18nKey: 'ADD_LABEL', attributeI18nKey: 'ADD_LABEL',
}, },
{ // {
key: 'send_message', // key: 'send_email_to_team',
name: 'Send an email to team', // name: 'Send an email to team',
attributeI18nKey: 'SEND_MESSAGE', // attributeI18nKey: 'SEND_MESSAGE',
}, // },
], ],
}, },
conversation_created: { conversation_created: {
@ -120,11 +120,11 @@ export const AUTOMATIONS = {
name: 'Assign a team', name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM', attributeI18nKey: 'ASSIGN_TEAM',
}, },
{ // {
key: 'send_message', // key: 'send_email_to_team',
name: 'Send an email to team', // name: 'Send an email to team',
attributeI18nKey: 'SEND_MESSAGE', // attributeI18nKey: 'SEND_MESSAGE',
}, // },
{ {
key: 'assign_agent', key: 'assign_agent',
name: 'Assign an agent', name: 'Assign an agent',
@ -183,11 +183,11 @@ export const AUTOMATIONS = {
name: 'Assign a team', name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM', attributeI18nKey: 'ASSIGN_TEAM',
}, },
{ // {
key: 'send_message', // key: 'send_email_to_team',
name: 'Send an email to team', // name: 'Send an email to team',
attributeI18nKey: 'SEND_MESSAGE', // attributeI18nKey: 'SEND_MESSAGE',
}, // },
{ {
key: 'assign_agent', key: 'assign_agent',
name: 'Assign an agent', name: 'Assign an agent',
@ -222,8 +222,8 @@ export const AUTOMATION_ACTION_TYPES = [
key: 'add_label', key: 'add_label',
label: 'Add a label', label: 'Add a label',
}, },
{ // {
key: 'send_message', // key: 'send_email_to_team',
label: 'Send an email to team', // label: 'Send an email to team',
}, // },
]; ];

View file

@ -48,7 +48,7 @@ export const actions = {
commit(types.SET_AUTOMATION_UI_FLAG, { isUpdating: true }); commit(types.SET_AUTOMATION_UI_FLAG, { isUpdating: true });
try { try {
const response = await AutomationAPI.update(id, updateObj); const response = await AutomationAPI.update(id, updateObj);
commit(types.EDIT_AUTOMATION, response.data); commit(types.EDIT_AUTOMATION, response.data.payload);
} catch (error) { } catch (error) {
throw new Error(error); throw new Error(error);
} finally { } finally {

View file

@ -50,7 +50,9 @@ describe('#actions', () => {
describe('#update', () => { describe('#update', () => {
it('sends correct actions if API is success', async () => { it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: automationsList[0] }); axios.patch.mockResolvedValue({
data: { payload: automationsList[0] },
});
await actions.update({ commit }, automationsList[0]); await actions.update({ commit }, automationsList[0]);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }], [types.default.SET_AUTOMATION_UI_FLAG, { isUpdating: true }],

View file

@ -1,64 +1,36 @@
export default [ export default [
{ {
id: 12, name: 'Test 5',
description: 'Hello',
id: 46,
account_id: 1, account_id: 1,
name: 'Test',
description: 'This is a test',
event_name: 'conversation_created', event_name: 'conversation_created',
conditions: [ conditions: [
{ {
values: ['open'], values: ['open'],
attribute_key: 'status', attribute_key: 'status',
query_operator: null,
filter_operator: 'equal_to', filter_operator: 'equal_to',
}, },
], ],
actions: [ actions: [{ action_name: 'add_label', action_params: ['testlabel'] }],
{ created_on: '2022-02-08T10:46:32.387Z',
action_name: 'add_label',
action_params: [{}],
},
],
created_on: '2022-01-14T09:17:55.689Z',
active: true, active: true,
}, },
{ {
id: 13, id: 47,
account_id: 1, account_id: 1,
name: 'Auto resolve conversation', name: 'Snooze',
description: 'Auto resolves conversation', description: 'Test Description',
event_name: 'conversation_updated', event_name: 'conversation_created',
conditions: [ conditions: [
{ {
values: ['resolved'], values: ['pending'],
attribute_key: 'status', attribute_key: 'status',
query_operator: null,
filter_operator: 'equal_to', filter_operator: 'equal_to',
}, },
], ],
actions: [ actions: [{ action_name: 'assign_team', action_params: [1] }],
{ created_on: '2022-02-08T11:19:44.714Z',
action_name: 'add_label',
action_params: [{}],
},
],
created_on: '2022-01-14T13:06:31.843Z',
active: true,
},
{
id: 14,
account_id: 1,
name: 'Fayaz',
description: 'This is a test',
event_name: 'conversation_created',
conditions: {},
actions: [
{
action_name: 'add_label',
action_params: [{}],
},
],
created_on: '2022-01-17T06:46:08.098Z',
active: true, active: true,
}, },
]; ];

View file

@ -18,40 +18,18 @@ describe('#mutations', () => {
}); });
}); });
// describe('#EDIT_AUTOMATION', () => { describe('#EDIT_AUTOMATION', () => {
// it('update automation record', () => { it('update automation record', () => {
// const state = { records: [automations[0]] }; const state = { records: [automations[0]] };
// mutations[types.EDIT_AUTOMATION](state, { mutations[types.EDIT_AUTOMATION](state, automations[0]);
// id: 12, expect(state.records[0].name).toEqual('Test 5');
// account_id: 1, });
// name: 'Test Automation', });
// description: 'This is a test',
// event_name: 'conversation_created',
// conditions: [
// {
// values: ['open'],
// attribute_key: 'status',
// query_operator: null,
// filter_operator: 'equal_to',
// },
// ],
// actions: [
// {
// action_name: 'add_label',
// action_params: [{}],
// },
// ],
// created_on: '2022-01-14T09:17:55.689Z',
// active: true,
// });
// expect(state.records[0].name).toEqual('Test Automation');
// });
// });
describe('#DELETE_AUTOMATION', () => { describe('#DELETE_AUTOMATION', () => {
it('delete automation record', () => { it('delete automation record', () => {
const state = { records: [automations[0]] }; const state = { records: [automations[0]] };
mutations[types.DELETE_AUTOMATION](state, 12); mutations[types.DELETE_AUTOMATION](state, 46);
expect(state.records).toEqual([]); expect(state.records).toEqual([]);
}); });
}); });

View file

@ -1,4 +1,14 @@
class AutomationRuleListener < BaseListener class AutomationRuleListener < BaseListener
def conversation_updated(event_obj)
conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_updated', conversation)
@rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform
AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present?
end
end
def conversation_status_changed(event_obj) def conversation_status_changed(event_obj)
conversation = event_obj.data[:conversation] conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_status_changed', conversation) return unless rule_present?('conversation_status_changed', conversation)

View file

@ -162,6 +162,7 @@ class Conversation < ApplicationRecord
def execute_after_update_commit_callbacks def execute_after_update_commit_callbacks
notify_status_change notify_status_change
create_activity create_activity
notify_conversation_updation
end end
def ensure_snooze_until_reset def ensure_snooze_until_reset
@ -181,6 +182,10 @@ class Conversation < ApplicationRecord
dispatcher_dispatch(CONVERSATION_CREATED) dispatcher_dispatch(CONVERSATION_CREATED)
end end
def notify_conversation_updation
dispatcher_dispatch(CONVERSATION_UPDATED)
end
def self_assign?(assignee_id) def self_assign?(assignee_id)
assignee_id.present? && Current.user&.id == assignee_id assignee_id.present? && Current.user&.id == assignee_id
end end

View file

@ -21,14 +21,14 @@ class AutomationRules::ConditionsFilterService < FilterService
records.any? records.any?
end end
def message_conditions(_message) def message_conditions(message)
message_filters = @filters['messages'] message_filters = @filters['messages']
@rule.conditions.each_with_index do |query_hash, current_index| @rule.conditions.each_with_index do |query_hash, current_index|
current_filter = message_filters[query_hash['attribute_key']] current_filter = message_filters[query_hash['attribute_key']]
@query_string += message_query_string(current_filter, query_hash.with_indifferent_access, current_index) @query_string += message_query_string(current_filter, query_hash.with_indifferent_access, current_index)
end end
records = Message.where(conversation: @conversation).where(@query_string, @filter_values.with_indifferent_access) records = Message.where(id: message.id).where(@query_string, @filter_values.with_indifferent_access)
records.any? records.any?
end end

View file

@ -5,5 +5,5 @@ json.description automation_rule.description
json.event_name automation_rule.event_name json.event_name automation_rule.event_name
json.conditions automation_rule.conditions json.conditions automation_rule.conditions
json.actions automation_rule.actions json.actions automation_rule.actions
json.created_on automation_rule.created_at json.created_on automation_rule.created_at.to_i
json.active automation_rule.active? json.active automation_rule.active?

View file

@ -14,6 +14,7 @@ module Events::Types
# conversation events # conversation events
CONVERSATION_CREATED = 'conversation.created' CONVERSATION_CREATED = 'conversation.created'
CONVERSATION_UPDATED = 'conversation.updated'
CONVERSATION_READ = 'conversation.read' CONVERSATION_READ = 'conversation.read'
# FIXME: deprecate the opened and resolved events in future in favor of status changed event. # FIXME: deprecate the opened and resolved events in future in favor of status changed event.
CONVERSATION_OPENED = 'conversation.opened' CONVERSATION_OPENED = 'conversation.opened'

View file

@ -70,6 +70,53 @@ describe AutomationRuleListener do
end end
end end
describe '#conversation_updated' do
before do
automation_rule.update!(
event_name: 'conversation_updated',
name: 'Call actions conversation updated',
description: 'Add labels, assign team after conversation updated'
)
end
let!(:event) do
Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation })
end
context 'when rule matches' do
it 'triggers automation rule to assign team' do
expect(conversation.team_id).not_to eq(team.id)
automation_rule
listener.conversation_updated(event)
conversation.reload
expect(conversation.team_id).to eq(team.id)
end
it 'triggers automation rule to add label' do
expect(conversation.labels).to eq([])
automation_rule
listener.conversation_updated(event)
conversation.reload
expect(conversation.labels.pluck(:name)).to eq(%w[support priority_customer])
end
it 'triggers automation rule to assign best agents' do
expect(conversation.assignee).to be_nil
automation_rule
listener.conversation_updated(event)
conversation.reload
expect(conversation.assignee).to eq(user_1)
end
end
end
describe '#message_created' do describe '#message_created' do
before do before do
automation_rule.update!( automation_rule.update!(