From 9a73c1473ffa0ae6a9c7725046b8ca17922dcc83 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Fri, 14 Aug 2026 08:45:58 +0530
Subject: [PATCH] feat: add icons to macro and automation dropdowns (#15447)
---
.../filter/helper/filterAttributeIcons.js | 25 ++++++--
.../filter/inputs/MultiSelect.vue | 13 +++-
.../widgets/AutomationActionInput.vue | 6 +-
.../composables/spec/useMacros.spec.js | 1 +
.../dashboard/composables/useMacros.js | 10 ++--
.../dashboard/helper/automationHelper.js | 60 +++++++++++++++----
.../automation/AutomationRuleForm.vue | 7 +++
.../dashboard/settings/macros/MacroEditor.vue | 2 +
8 files changed, 101 insertions(+), 23 deletions(-)
diff --git a/app/javascript/dashboard/components-next/filter/helper/filterAttributeIcons.js b/app/javascript/dashboard/components-next/filter/helper/filterAttributeIcons.js
index 87ba25680..a3e2ff2ef 100644
--- a/app/javascript/dashboard/components-next/filter/helper/filterAttributeIcons.js
+++ b/app/javascript/dashboard/components-next/filter/helper/filterAttributeIcons.js
@@ -1,9 +1,10 @@
/**
* Leading icons and grouped section headers for the attribute picker rendered by FilterSelect,
- * so the conversation and contact filters read like the Captain audience picker.
+ * so the conversation and contact filters read like the Captain audience picker. The icon table is
+ * shared with the automation condition picker, which draws from the same attribute keys.
*/
-// Icon per known attribute key, across the conversation and contact filters.
+// Icon per known attribute key, across the conversation, contact and automation attributes.
const ATTRIBUTE_ICONS = {
// Contact attributes
name: 'i-lucide-user',
@@ -25,6 +26,12 @@ const ATTRIBUTE_ICONS = {
campaign_id: 'i-lucide-megaphone',
browser_language: 'i-lucide-globe',
referer: 'i-lucide-link',
+ // Automation attributes
+ message_type: 'i-lucide-message-square',
+ private_note: 'i-lucide-sticky-note',
+ content: 'i-lucide-message-square-text',
+ mail_subject: 'i-lucide-mail-open',
+ conversation_language: 'i-lucide-languages',
// Shared
labels: 'i-lucide-tags',
created_at: 'i-lucide-calendar',
@@ -45,10 +52,16 @@ const CUSTOM_TYPE_ICONS = {
const DEFAULT_ICON = 'i-lucide-tag';
-const getAttributeIcon = type =>
- (type.attributeModel === 'customAttributes'
- ? CUSTOM_TYPE_ICONS[type.attributeDisplayType]
- : ATTRIBUTE_ICONS[type.attributeKey]) || DEFAULT_ICON;
+/**
+ * Resolve the leading icon for an attribute. Only custom attributes carry a display type, so they
+ * are iconed by that; every other attribute is iconed by its key.
+ * @param {{attributeKey?: string, attributeDisplayType?: string}} type
+ * @returns {string} Icon class.
+ */
+export const getAttributeIcon = ({ attributeKey, attributeDisplayType }) =>
+ CUSTOM_TYPE_ICONS[attributeDisplayType] ||
+ ATTRIBUTE_ICONS[attributeKey] ||
+ DEFAULT_ICON;
// The order groups appear in, keyed by attributeModel. Labels resolve against the caller's i18n
// namespace so the conversation and contact filters can name their own sections.
diff --git a/app/javascript/dashboard/components-next/filter/inputs/MultiSelect.vue b/app/javascript/dashboard/components-next/filter/inputs/MultiSelect.vue
index 062f1c057..5899d2d53 100644
--- a/app/javascript/dashboard/components-next/filter/inputs/MultiSelect.vue
+++ b/app/javascript/dashboard/components-next/filter/inputs/MultiSelect.vue
@@ -131,6 +131,11 @@ const toggleOption = option => {
:color="item.iconColor"
class="flex-shrink-0 size-4"
/>
+
{{ item.name }}
{
preserve-open
@click="toggleOption(option)"
>
-
+
+
{{ option.name }}
diff --git a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
index 9e33bb29f..aa0d7d17b 100644
--- a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
+++ b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
@@ -92,7 +92,11 @@ export default {
},
},
actionTypesAsOptions() {
- return this.actionTypes.map(a => ({ id: a.key, name: a.label }));
+ return this.actionTypes.map(a => ({
+ id: a.key,
+ name: a.label,
+ icon: a.icon,
+ }));
},
isVerticalLayout() {
return ['team_message', 'textarea', 'email'].includes(this.inputType);
diff --git a/app/javascript/dashboard/composables/spec/useMacros.spec.js b/app/javascript/dashboard/composables/spec/useMacros.spec.js
index 601fc7e8a..cd1c2ad54 100644
--- a/app/javascript/dashboard/composables/spec/useMacros.spec.js
+++ b/app/javascript/dashboard/composables/spec/useMacros.spec.js
@@ -156,6 +156,7 @@ describe('useMacros', () => {
const expectedLabels = mockLabels.map(i => ({
id: i.title,
name: i.title,
+ color: i.color,
}));
expect(getMacroDropdownValues('add_label')).toEqual(expectedLabels);
expect(getMacroDropdownValues('remove_label')).toEqual(expectedLabels);
diff --git a/app/javascript/dashboard/composables/useMacros.js b/app/javascript/dashboard/composables/useMacros.js
index 562ce092b..61b45dd72 100644
--- a/app/javascript/dashboard/composables/useMacros.js
+++ b/app/javascript/dashboard/composables/useMacros.js
@@ -2,7 +2,10 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStoreGetters } from 'dashboard/composables/store';
import { PRIORITY_CONDITION_VALUES } from 'dashboard/constants/automation';
-import { generateTeamOptions } from 'dashboard/helper/automationHelper';
+import {
+ generateLabelOptions,
+ generateTeamOptions,
+} from 'dashboard/helper/automationHelper';
import {
resolveActionName,
getFileName,
@@ -42,10 +45,7 @@ export const useMacros = () => {
];
case 'add_label':
case 'remove_label':
- return labels.value.map(i => ({
- id: i.title,
- name: i.title,
- }));
+ return generateLabelOptions(labels.value);
case 'change_priority':
return PRIORITY_CONDITION_VALUES.map(item => ({
id: item.id,
diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js
index 294faf677..178b8260a 100644
--- a/app/javascript/dashboard/helper/automationHelper.js
+++ b/app/javascript/dashboard/helper/automationHelper.js
@@ -1,16 +1,16 @@
+import {
+ DEFAULT_ACTIONS,
+ DEFAULT_CONVERSATION_CONDITION,
+ DEFAULT_MESSAGE_CREATED_CONDITION,
+ DEFAULT_OTHER_CONDITION,
+} from 'dashboard/constants/automation';
import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_3,
OPERATOR_TYPES_4,
} from 'dashboard/routes/dashboard/settings/automation/operators';
-import {
- DEFAULT_MESSAGE_CREATED_CONDITION,
- DEFAULT_CONVERSATION_CONDITION,
- DEFAULT_OTHER_CONDITION,
- DEFAULT_ACTIONS,
-} from 'dashboard/constants/automation';
-import filterQueryGenerator from './filterQueryGenerator';
import actionQueryGenerator from './actionQueryGenerator';
+import filterQueryGenerator from './filterQueryGenerator';
export const getCustomAttributeInputType = key => {
const customAttributeMap = {
@@ -78,12 +78,45 @@ export const generateCustomAttributeTypes = (customAttributes, type) => {
key: attr.attribute_key,
name: attr.attribute_display_name,
inputType: getCustomAttributeInputType(attr.attribute_display_type),
+ attributeDisplayType: attr.attribute_display_type,
filterOperators: getOperatorTypes(attr.attribute_display_type),
customAttributeType: type,
};
});
};
+// Leading icon per action key, shared by the automation and macro action pickers.
+const ACTION_ICONS = {
+ assign_agent: 'i-lucide-user-round',
+ assign_team: 'i-lucide-users-round',
+ remove_assigned_agent: 'i-lucide-user-round-x',
+ remove_assigned_team: 'i-lucide-users',
+ add_label: 'i-lucide-tag',
+ remove_label: 'i-woot-tag-remove',
+ send_email_to_team: 'i-lucide-send',
+ send_email_transcript: 'i-lucide-mail',
+ send_message: 'i-lucide-message-square',
+ add_private_note: 'i-lucide-sticky-note',
+ send_attachment: 'i-lucide-paperclip',
+ send_webhook_event: 'i-lucide-webhook',
+ mute_conversation: 'i-lucide-bell-off',
+ snooze_conversation: 'i-lucide-clock',
+ open_conversation: 'i-lucide-circle-dot',
+ pending_conversation: 'i-lucide-circle-dashed',
+ resolve_conversation: 'i-lucide-circle-check',
+ change_priority: 'i-lucide-signal-high',
+ add_sla: 'i-lucide-gauge',
+};
+
+const DEFAULT_ACTION_ICON = 'i-lucide-zap';
+
+/**
+ * Resolve the leading icon for an automation or macro action.
+ * @param {string} key - The action key.
+ * @returns {string} Icon class.
+ */
+export const getActionIcon = key => ACTION_ICONS[key] || DEFAULT_ACTION_ICON;
+
export const generateConditionOptions = (options, key = 'id') => {
if (!options || !Array.isArray(options)) return [];
return options.map(i => {
@@ -104,6 +137,13 @@ export const generateTeamOptions = teams =>
iconColor: team.icon_color,
}));
+export const generateLabelOptions = labels =>
+ (labels || []).map(label => ({
+ id: label.title,
+ name: label.title,
+ color: label.color,
+ }));
+
export const getActionOptions = ({
agents,
teams,
@@ -119,8 +159,8 @@ export const getActionOptions = ({
? addNoneToListFn(generateTeamOptions(teams))
: generateTeamOptions(teams),
send_email_to_team: generateTeamOptions(teams),
- add_label: generateConditionOptions(labels, 'title'),
- remove_label: generateConditionOptions(labels, 'title'),
+ add_label: generateLabelOptions(labels),
+ remove_label: generateLabelOptions(labels),
change_priority: priorityOptions,
add_sla: slaPolicies,
};
@@ -164,7 +204,7 @@ export const getConditionOptions = ({
message_type: messageTypeOptions,
private_note: booleanFilterOptions,
priority: priorityOptions,
- labels: generateConditionOptions(labels, 'title'),
+ labels: generateLabelOptions(labels),
};
return conditionFilterMaps[type];
diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue
index d7bf5c78e..14b475b0a 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue
@@ -7,8 +7,10 @@ import NextButton from 'dashboard/components-next/button/Button.vue';
import SidePanel from 'dashboard/components-next/side-panel/SidePanel.vue';
import {
generateAutomationPayload,
+ getActionIcon,
getAttributes,
} from 'dashboard/helper/automationHelper';
+import { getAttributeIcon } from 'dashboard/components-next/filter/helper/filterAttributeIcons';
import { validateAutomation } from 'dashboard/helper/validations';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { DURATION_UNITS } from 'dashboard/components-next/input/constants';
@@ -198,6 +200,10 @@ const filterTypes = computed(() => {
value: attr.key,
attributeName: attr.name,
label: attr.name,
+ icon: getAttributeIcon({
+ attributeKey: attr.key,
+ attributeDisplayType: attr.attributeDisplayType,
+ }),
inputType: mappedInputType,
options,
filterOperators,
@@ -229,6 +235,7 @@ const automationActionTypes = computed(() => {
return actionTypes.map(action => ({
...action,
label: t(`AUTOMATION.ACTIONS.${action.label}`),
+ icon: getActionIcon(action.key),
}));
});
diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue
index f8c58fcff..1312122f9 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue
@@ -7,6 +7,7 @@ import MacroForm from './MacroForm.vue';
import { MACRO_ACTION_TYPES } from './constants';
import { useAlert } from 'dashboard/composables';
import actionQueryGenerator from 'dashboard/helper/actionQueryGenerator.js';
+import { getActionIcon } from 'dashboard/helper/automationHelper';
import { useMacros } from 'dashboard/composables/useMacros';
import { useAdmin } from 'dashboard/composables/useAdmin';
@@ -28,6 +29,7 @@ const macroActionTypes = computed(() => {
return MACRO_ACTION_TYPES.map(type => ({
...type,
label: t(`MACROS.ACTIONS.${type.label}`),
+ icon: getActionIcon(type.key),
}));
});