feat: add icons to macro and automation dropdowns (#15447)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user