fix: add explicit remove assignment actions to macros and automations (#12172)

This updates macros and automations so agents can explicitly remove
assigned agents or teams, while keeping the existing `Assign -> None`
flow working for backward compatibility.

Fixes: #7551
Closes: #7551

## Why
The original macro change exposed unassignment only through `Assign ->
None`, which made macros behave differently from automations and left
the explicit remove actions inconsistent across the product. This keeps
the lower-risk compatibility path and adds the explicit remove actions
requested in review.

## What this change does
- Adds `Remove Assigned Agent` and `Remove Assigned Team` as explicit
actions in macros.
- Adds the same explicit remove actions in automations.
- Keeps `Assign Agent -> None` and `Assign Team -> None` working for
existing behavior and stored payloads.
- Preserves backward compatibility for existing macro and automation
execution payloads.
- Downmerges the latest `develop` and resolves the conflicts while
keeping both the new remove actions and current `develop` behavior.

## Validation
- Verified both remove actions are available and selectable in the macro
editor.
- Verified both remove actions are available and selectable in the
automation builder.
- Applied a disposable macro with `Remove Assigned Agent` and `Remove
Assigned Team` on a real conversation and confirmed both fields were
cleared.
- Applied a disposable macro with `Assign Agent -> None` and `Assign
Team -> None` on a real conversation and confirmed both fields were
still cleared.
This commit is contained in:
Sojan Jose
2026-04-16 15:57:41 +05:30
committed by GitHub
parent 72b8a31f2d
commit aee979ee0b
19 changed files with 198 additions and 19 deletions

View File

@@ -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' },
]);
});

View File

@@ -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 => ({

View File

@@ -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', () => {

View File

@@ -125,6 +125,7 @@ const validateSingleAction = action => {
'mute_conversation',
'snooze_conversation',
'resolve_conversation',
'remove_assigned_agent',
'remove_assigned_team',
'open_conversation',
'pending_conversation',

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 : '';
})

View File

@@ -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

View File

@@ -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]

View File

@@ -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