feat: add icons to macro and automation dropdowns (#15447)

This commit is contained in:
Sivin Varghese
2026-08-14 08:45:58 +05:30
committed by GitHub
parent 59942e6890
commit 9a73c1473f
8 changed files with 101 additions and 23 deletions

View File

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

View File

@@ -131,6 +131,11 @@ const toggleOption = option => {
:color="item.iconColor"
class="flex-shrink-0 size-4"
/>
<span
v-if="item.color"
class="flex-shrink-0 rounded-full size-1.5"
:style="{ backgroundColor: item.color }"
/>
<span class="truncate">{{ item.name }}</span>
</div>
<div
@@ -172,12 +177,18 @@ const toggleOption = option => {
preserve-open
@click="toggleOption(option)"
>
<template v-if="option.emoji" #icon>
<template v-if="option.emoji || option.color" #icon>
<EmojiIcon
v-if="option.emoji"
:value="option.emoji"
:color="option.iconColor"
class="flex-shrink-0 size-4"
/>
<span
v-else
class="flex-shrink-0 rounded-full size-1.5"
:style="{ backgroundColor: option.color }"
/>
</template>
<template #label>
{{ option.name }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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