diff --git a/app/javascript/dashboard/composables/spec/useMacros.spec.js b/app/javascript/dashboard/composables/spec/useMacros.spec.js index e7f6aa495..58ea78c15 100644 --- a/app/javascript/dashboard/composables/spec/useMacros.spec.js +++ b/app/javascript/dashboard/composables/spec/useMacros.spec.js @@ -119,24 +119,30 @@ describe('useMacros', () => { const { getMacroDropdownValues } = useMacros(); expect(getMacroDropdownValues('add_label')).toHaveLength(mockLabels.length); expect(getMacroDropdownValues('assign_team')).toHaveLength( - mockTeams.length - ); + mockTeams.length + 1 + ); // +1 for "None" expect(getMacroDropdownValues('assign_agent')).toHaveLength( - mockAgents.length + 1 - ); // +1 for "Self" + mockAgents.length + 2 + ); // +2 for "None" and "Self" }); - it('returns teams for assign_team and send_email_to_team types', () => { + it('returns teams with "None" option for assign_team and teams only for send_email_to_team', () => { const { getMacroDropdownValues } = useMacros(); - expect(getMacroDropdownValues('assign_team')).toEqual(mockTeams); + const assignTeamResult = getMacroDropdownValues('assign_team'); + expect(assignTeamResult[0]).toEqual({ + id: 'nil', + name: 'AUTOMATION.NONE_OPTION', + }); + expect(assignTeamResult.slice(1)).toEqual(mockTeams); expect(getMacroDropdownValues('send_email_to_team')).toEqual(mockTeams); }); - it('returns agents with "Self" option for assign_agent type', () => { + it('returns agents with "None" and "Self" options for assign_agent type', () => { const { getMacroDropdownValues } = useMacros(); const result = getMacroDropdownValues('assign_agent'); - expect(result[0]).toEqual({ id: 'self', name: 'Self' }); - expect(result.slice(1)).toEqual(mockAgents); + expect(result[0]).toEqual({ id: 'nil', name: 'AUTOMATION.NONE_OPTION' }); + expect(result[1]).toEqual({ id: 'self', name: 'Self' }); + expect(result.slice(2)).toEqual(mockAgents); }); it('returns formatted labels for add_label and remove_label types', () => { @@ -172,8 +178,11 @@ describe('useMacros', () => { const { getMacroDropdownValues } = useMacros(); expect(getMacroDropdownValues('add_label')).toEqual([]); - expect(getMacroDropdownValues('assign_team')).toEqual([]); + expect(getMacroDropdownValues('assign_team')).toEqual([ + { id: 'nil', name: 'AUTOMATION.NONE_OPTION' }, + ]); expect(getMacroDropdownValues('assign_agent')).toEqual([ + { id: 'nil', name: 'AUTOMATION.NONE_OPTION' }, { id: 'self', name: 'Self' }, ]); }); diff --git a/app/javascript/dashboard/composables/useMacros.js b/app/javascript/dashboard/composables/useMacros.js index 437690eff..52aacd73f 100644 --- a/app/javascript/dashboard/composables/useMacros.js +++ b/app/javascript/dashboard/composables/useMacros.js @@ -15,6 +15,11 @@ export const useMacros = () => { const teams = computed(() => getters['teams/getTeams'].value); const agents = computed(() => getters['agents/getVerifiedAgents'].value); + const withNoneOption = options => [ + { id: 'nil', name: t('AUTOMATION.NONE_OPTION') }, + ...(options || []), + ]; + /** * Get dropdown values based on the specified type * @param {string} type - The type of dropdown values to retrieve @@ -23,10 +28,15 @@ export const useMacros = () => { const getMacroDropdownValues = type => { switch (type) { case 'assign_team': + return withNoneOption(teams.value); case 'send_email_to_team': return teams.value; case 'assign_agent': - return [{ id: 'self', name: 'Self' }, ...agents.value]; + return [ + ...withNoneOption(), + { id: 'self', name: 'Self' }, + ...agents.value, + ]; case 'add_label': case 'remove_label': return labels.value.map(i => ({ diff --git a/app/javascript/dashboard/helper/specs/macrosHelper.spec.js b/app/javascript/dashboard/helper/specs/macrosHelper.spec.js index d4c8dc5fb..fb5b62b38 100644 --- a/app/javascript/dashboard/helper/specs/macrosHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/macrosHelper.spec.js @@ -45,6 +45,10 @@ describe('#resolveTeamIds', () => { const resolvedTeams = '⚙️ sales team, 🤷‍♂️ fayaz'; expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams); }); + + it('resolves nil as None', () => { + expect(resolveTeamIds(teams, ['nil'])).toEqual('None'); + }); }); describe('#resolveLabels', () => { @@ -59,6 +63,10 @@ describe('#resolveAgents', () => { const resolvedAgents = 'John Doe'; expect(resolveAgents(agents, [1])).toEqual(resolvedAgents); }); + + it('resolves nil and self values', () => { + expect(resolveAgents(agents, ['nil', 'self'])).toEqual('None, Self'); + }); }); describe('#getFileName', () => { diff --git a/app/javascript/dashboard/helper/validations.js b/app/javascript/dashboard/helper/validations.js index e425047a2..eaea81eb7 100644 --- a/app/javascript/dashboard/helper/validations.js +++ b/app/javascript/dashboard/helper/validations.js @@ -125,6 +125,7 @@ const validateSingleAction = action => { 'mute_conversation', 'snooze_conversation', 'resolve_conversation', + 'remove_assigned_agent', 'remove_assigned_team', 'open_conversation', 'pending_conversation', diff --git a/app/javascript/dashboard/i18n/locale/en/automation.json b/app/javascript/dashboard/i18n/locale/en/automation.json index d338fa9a2..46f4a520e 100644 --- a/app/javascript/dashboard/i18n/locale/en/automation.json +++ b/app/javascript/dashboard/i18n/locale/en/automation.json @@ -140,6 +140,8 @@ "ACTIONS": { "ASSIGN_AGENT": "Assign to Agent", "ASSIGN_TEAM": "Assign a Team", + "REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent", + "REMOVE_ASSIGNED_TEAM": "Remove Assigned Team", "ADD_LABEL": "Add a Label", "REMOVE_LABEL": "Remove a Label", "SEND_EMAIL_TO_TEAM": "Send an Email to Team", diff --git a/app/javascript/dashboard/i18n/locale/en/macros.json b/app/javascript/dashboard/i18n/locale/en/macros.json index e12f0ca73..67a5c5003 100644 --- a/app/javascript/dashboard/i18n/locale/en/macros.json +++ b/app/javascript/dashboard/i18n/locale/en/macros.json @@ -93,6 +93,7 @@ "ASSIGN_AGENT": "Assign an Agent", "ADD_LABEL": "Add a Label", "REMOVE_LABEL": "Remove a Label", + "REMOVE_ASSIGNED_AGENT": "Remove Assigned Agent", "REMOVE_ASSIGNED_TEAM": "Remove Assigned Team", "SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript", "MUTE_CONVERSATION": "Mute Conversation", diff --git a/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroPreview.vue b/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroPreview.vue index d5a2ae0b8..9e57c34c5 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroPreview.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroPreview.vue @@ -25,6 +25,7 @@ const getActionValue = (key, params) => { add_label: resolveLabels(labels.value, params), remove_label: resolveLabels(labels.value, params), assign_agent: resolveAgents(agents.value, params), + remove_assigned_agent: null, mute_conversation: null, snooze_conversation: null, resolve_conversation: null, diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js index 24947c63b..c7f4529b8 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js @@ -90,6 +90,14 @@ export const AUTOMATIONS = { key: 'assign_team', name: 'ASSIGN_TEAM', }, + { + key: 'remove_assigned_agent', + name: 'REMOVE_ASSIGNED_AGENT', + }, + { + key: 'remove_assigned_team', + name: 'REMOVE_ASSIGNED_TEAM', + }, { key: 'add_label', name: 'ADD_LABEL', @@ -218,6 +226,14 @@ export const AUTOMATIONS = { key: 'assign_team', name: 'ASSIGN_TEAM', }, + { + key: 'remove_assigned_agent', + name: 'REMOVE_ASSIGNED_AGENT', + }, + { + key: 'remove_assigned_team', + name: 'REMOVE_ASSIGNED_TEAM', + }, { key: 'assign_agent', name: 'ASSIGN_AGENT', @@ -350,6 +366,14 @@ export const AUTOMATIONS = { key: 'assign_team', name: 'ASSIGN_TEAM', }, + { + key: 'remove_assigned_agent', + name: 'REMOVE_ASSIGNED_AGENT', + }, + { + key: 'remove_assigned_team', + name: 'REMOVE_ASSIGNED_TEAM', + }, { key: 'assign_agent', name: 'ASSIGN_AGENT', @@ -476,6 +500,14 @@ export const AUTOMATIONS = { key: 'assign_team', name: 'ASSIGN_TEAM', }, + { + key: 'remove_assigned_agent', + name: 'REMOVE_ASSIGNED_AGENT', + }, + { + key: 'remove_assigned_team', + name: 'REMOVE_ASSIGNED_TEAM', + }, { key: 'assign_agent', name: 'ASSIGN_AGENT', @@ -592,6 +624,14 @@ export const AUTOMATIONS = { key: 'assign_team', name: 'ASSIGN_TEAM', }, + { + key: 'remove_assigned_agent', + name: 'REMOVE_ASSIGNED_AGENT', + }, + { + key: 'remove_assigned_team', + name: 'REMOVE_ASSIGNED_TEAM', + }, { key: 'send_email_to_team', name: 'SEND_EMAIL_TO_TEAM', @@ -650,6 +690,16 @@ export const AUTOMATION_ACTION_TYPES = [ label: 'ASSIGN_TEAM', inputType: 'search_select', }, + { + key: 'remove_assigned_agent', + label: 'REMOVE_ASSIGNED_AGENT', + inputType: null, + }, + { + key: 'remove_assigned_team', + label: 'REMOVE_ASSIGNED_TEAM', + inputType: null, + }, { key: 'add_label', label: 'ADD_LABEL', diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/constants.js b/app/javascript/dashboard/routes/dashboard/settings/macros/constants.js index e8ee4fdef..8a0088d06 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/macros/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/constants.js @@ -19,6 +19,11 @@ export const MACRO_ACTION_TYPES = [ label: 'REMOVE_LABEL', inputType: 'multi_select', }, + { + key: 'remove_assigned_agent', + label: 'REMOVE_ASSIGNED_AGENT', + inputType: null, + }, { key: 'remove_assigned_team', label: 'REMOVE_ASSIGNED_TEAM', diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/macroHelper.js b/app/javascript/dashboard/routes/dashboard/settings/macros/macroHelper.js index 3fc204ac0..c498ac4e3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/macros/macroHelper.js +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/macroHelper.js @@ -17,6 +17,7 @@ export const resolveActionName = key => { export const resolveTeamIds = (teams, ids) => { return ids .map(id => { + if (id === 'nil') return 'None'; const team = teams.find(i => i.id === id); return team ? team.name : ''; }) @@ -35,6 +36,8 @@ export const resolveLabels = (labels, ids) => { export const resolveAgents = (agents, ids) => { return ids .map(id => { + if (id === 'nil') return 'None'; + if (id === 'self') return 'Self'; const agent = agents.find(i => i.id === id); return agent ? agent.name : ''; }) diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb index 3ab23530d..ceac24dfb 100644 --- a/app/models/automation_rule.rb +++ b/app/models/automation_rule.rb @@ -40,9 +40,10 @@ class AutomationRule < ApplicationRecord end def actions_attributes - %w[send_message add_label remove_label send_email_to_team assign_team assign_agent send_webhook_event mute_conversation - send_attachment change_status resolve_conversation open_conversation pending_conversation snooze_conversation change_priority - send_email_transcript add_private_note].freeze + %w[send_message add_label remove_label send_email_to_team assign_team assign_agent remove_assigned_agent + remove_assigned_team send_webhook_event mute_conversation send_attachment change_status resolve_conversation + open_conversation pending_conversation snooze_conversation change_priority send_email_transcript + add_private_note].freeze end def file_base_data diff --git a/app/models/macro.rb b/app/models/macro.rb index 1fe8192dc..fc25beeac 100644 --- a/app/models/macro.rb +++ b/app/models/macro.rb @@ -30,9 +30,9 @@ class Macro < ApplicationRecord validate :json_actions_format - ACTIONS_ATTRS = %w[send_message add_label assign_team assign_agent mute_conversation change_status remove_label remove_assigned_team - resolve_conversation snooze_conversation change_priority send_email_transcript send_attachment - add_private_note send_webhook_event].freeze + ACTIONS_ATTRS = %w[send_message add_label assign_team assign_agent mute_conversation change_status remove_label remove_assigned_agent + remove_assigned_team resolve_conversation snooze_conversation change_priority send_email_transcript + send_attachment add_private_note send_webhook_event].freeze def set_visibility(user, params) self.visibility = params[:visibility] diff --git a/app/services/action_service.rb b/app/services/action_service.rb index 86e2810cf..fa0d1a7fc 100644 --- a/app/services/action_service.rb +++ b/app/services/action_service.rb @@ -60,8 +60,7 @@ class ActionService end def assign_team(team_ids = []) - # FIXME: The explicit checks for zero or nil (string) is bad. Move - # this to a separate unassign action. + # Keep nil/0 handling for existing automation and macro payloads. should_unassign = team_ids.blank? || %w[nil 0].include?(team_ids[0].to_s) return @conversation.update!(team_id: nil) if should_unassign @@ -72,6 +71,10 @@ class ActionService @conversation.update!(team_id: team_ids[0]) end + def remove_assigned_agent(_params) + @conversation.update!(assignee_id: nil) + end + def remove_assigned_team(_params) @conversation.update!(team_id: nil) end diff --git a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb index ff882da4f..5b7c0112d 100644 --- a/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/automation_rules_controller_spec.rb @@ -68,6 +68,12 @@ RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do 'action_name': :assign_team, 'action_params': [1] }, + { + 'action_name': :remove_assigned_agent + }, + { + 'action_name': :remove_assigned_team + }, { 'action_name': :add_label, 'action_params': %w[support priority_customer] diff --git a/spec/controllers/api/v1/accounts/macros_controller_spec.rb b/spec/controllers/api/v1/accounts/macros_controller_spec.rb index 87d301d88..1af908e11 100644 --- a/spec/controllers/api/v1/accounts/macros_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/macros_controller_spec.rb @@ -78,6 +78,9 @@ RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do 'action_name': :add_label, 'action_params': %w[support priority_customer] }, + { + 'action_name': :remove_assigned_agent + }, { 'action_name': :remove_assigned_team }, @@ -484,6 +487,22 @@ RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do expect(conversation.reload.team_id).to be_nil end + + it 'Unassign the agent' do + macro.update!(actions: [ + { 'action_name' => 'remove_assigned_agent' } + ]) + conversation.update!(assignee: user_1) + expect(conversation.reload.assignee).to be_present + + perform_enqueued_jobs do + post "/api/v1/accounts/#{account.id}/macros/#{macro.id}/execute", + params: { conversation_ids: [conversation.display_id] }, + headers: administrator.create_new_auth_token + end + + expect(conversation.reload.assignee).to be_nil + end end end end diff --git a/spec/models/automation_rule_spec.rb b/spec/models/automation_rule_spec.rb index cd6297713..7ae73496c 100644 --- a/spec/models/automation_rule_spec.rb +++ b/spec/models/automation_rule_spec.rb @@ -37,6 +37,12 @@ RSpec.describe AutomationRule do action_name: :assign_team, action_params: [1] }, + { + action_name: :remove_assigned_agent + }, + { + action_name: :remove_assigned_team + }, { action_name: :add_label, action_params: %w[support priority_customer] diff --git a/spec/services/action_service_spec.rb b/spec/services/action_service_spec.rb index f4c20b191..6742b85fb 100644 --- a/spec/services/action_service_spec.rb +++ b/spec/services/action_service_spec.rb @@ -112,4 +112,15 @@ describe ActionService do end end end + + describe '#remove_assigned_agent' do + let(:conversation) { create(:conversation, :with_assignee, account: account) } + let(:action_service) { described_class.new(conversation) } + + it 'unassigns the conversation' do + expect(conversation.reload.assignee).to be_present + action_service.remove_assigned_agent(nil) + expect(conversation.reload.assignee).to be_nil + end + end end diff --git a/spec/services/automation_rules/action_service_spec.rb b/spec/services/automation_rules/action_service_spec.rb index b4eaa5dd0..0667726ff 100644 --- a/spec/services/automation_rules/action_service_spec.rb +++ b/spec/services/automation_rules/action_service_spec.rb @@ -88,8 +88,31 @@ RSpec.describe AutomationRules::ActionService do end end + describe '#perform with remove assignment actions' do + let!(:team) { create(:team, account: account) } + + before do + conversation.update!(assignee: agent, team: team) + rule.actions = [ + { action_name: 'remove_assigned_agent', action_params: [] }, + { action_name: 'remove_assigned_team', action_params: [] } + ] + rule.save! + end + + it 'removes assignee and team from the conversation' do + described_class.new(rule, account, conversation).perform + + expect(conversation.reload.assignee).to be_nil + expect(conversation.team).to be_nil + end + end + describe '#perform with send_email_transcript action' do before do + allow(account).to receive(:email_transcript_enabled?).and_return(true) + allow(account).to receive(:within_email_rate_limit?).and_return(true) + allow(account).to receive(:increment_email_sent_count).and_return(true) rule.actions << { action_name: 'send_email_transcript', action_params: ['contact@example.com, agent@example.com,agent1@example.com'] } rule.save end diff --git a/spec/services/macros/execution_service_spec.rb b/spec/services/macros/execution_service_spec.rb index b5bf13044..d44f7793a 100644 --- a/spec/services/macros/execution_service_spec.rb +++ b/spec/services/macros/execution_service_spec.rb @@ -49,6 +49,18 @@ RSpec.describe Macros::ExecutionService, type: :service do end end + describe '#assign_team' do + let(:team) { create(:team, account: account, allow_auto_assign: false) } + + context 'when team_id is nil' do + it 'unassigns the team from the conversation' do + conversation.update!(team_id: team.id) + service.send(:assign_team, ['nil']) + expect(conversation.reload.team).to be_nil + end + end + end + describe '#assign_agent' do context 'when agent_ids contains self' do it 'updates the conversation assignee to the current user' do @@ -69,6 +81,14 @@ RSpec.describe Macros::ExecutionService, type: :service do expect(conversation.reload.assignee).to eq(other_user) end end + + context 'when agent_ids contains nil' do + it 'unassigns the conversation' do + conversation.update!(assignee: user) + service.send(:assign_agent, ['nil']) + expect(conversation.reload.assignee).to be_nil + end + end end describe '#add_private_note' do