refactor(captain): simplify useAudienceFilterTypes composable

This commit is contained in:
Pranav
2026-07-14 17:53:50 -07:00
parent 2dec6ef362
commit aae21dfbb8
@@ -5,7 +5,6 @@ import { useContactFilterContext } from 'dashboard/components-next/filter/contac
import { useOperators } from 'dashboard/components-next/filter/operators.js';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
// Icons for the standard contact attributes, keyed by attribute key.
const STANDARD_ICONS = {
name: 'i-lucide-user',
email: 'i-lucide-mail',
@@ -18,11 +17,11 @@ const STANDARD_ICONS = {
last_activity_at: 'i-lucide-activity',
blocked: 'i-lucide-ban',
labels: 'i-lucide-tags',
hmac_verified: 'i-lucide-user-check',
browser_language: 'i-lucide-globe',
conversation_language: 'i-lucide-languages',
};
// Icons for custom attributes, keyed by the attribute's display type.
const CUSTOM_TYPE_ICONS = {
text: 'i-lucide-type',
number: 'i-lucide-hash',
@@ -38,11 +37,7 @@ const DEFAULT_ICON = 'i-lucide-tag';
/**
* Filter types for a Captain assistant audience: contact attributes (reused from the contact
* segment builder) plus the two conversation language fields. Each option carries an icon, and the
* options are split into grouped sections (contact / conversation / custom) via disabled headers,
* which FilterSelect renders as non-clickable section titles.
*
* @returns {{ filterTypes: import('vue').ComputedRef<Array> }}
* segment builder) plus conversation-scoped fields, grouped via disabled header options.
*/
export function useAudienceFilterTypes() {
const { t } = useI18n();
@@ -50,25 +45,33 @@ export function useAudienceFilterTypes() {
const { equalityOperators } = useOperators();
const contactAttributes = useMapGetter('attributes/getContactAttributes');
// Map custom attribute key -> display type, so each custom option gets a type-based icon.
const customTypeByKey = computed(() =>
(contactAttributes.value || []).reduce((acc, attr) => {
acc[attr.attributeKey] = attr.attributeDisplayType;
return acc;
}, {})
Object.fromEntries(
(contactAttributes.value || []).map(attr => [
attr.attributeKey,
attr.attributeDisplayType,
])
)
);
// Conversation-level attributes: the logged-in (HMAC verified) flag and the language fields.
// Languages use a searchable dropdown (same option list as automation rules), not free text.
const conversationOption = (key, label, options) => ({
attributeKey: key,
value: key,
attributeName: label,
label,
icon: STANDARD_ICONS[key],
inputType: 'searchSelect',
options,
dataType: 'text',
filterOperators: equalityOperators.value,
attributeModel: 'additional',
});
const conversationFilterTypes = computed(() => [
{
attributeKey: 'hmac_verified',
value: 'hmac_verified',
attributeName: t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.LOGGED_IN'),
label: t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.LOGGED_IN'),
icon: 'i-lucide-user-check',
inputType: 'searchSelect',
options: [
conversationOption(
'hmac_verified',
t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.LOGGED_IN'),
[
{
id: 'true',
name: t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.LOGGED_IN_TRUE'),
@@ -77,23 +80,18 @@ export function useAudienceFilterTypes() {
id: 'false',
name: t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.LOGGED_IN_FALSE'),
},
],
dataType: 'text',
filterOperators: equalityOperators.value,
attributeModel: 'additional',
},
...['browser_language', 'conversation_language'].map(key => ({
attributeKey: key,
value: key,
attributeName: t(`CAPTAIN.ASSISTANTS.FORM.AUDIENCE.${key.toUpperCase()}`),
label: t(`CAPTAIN.ASSISTANTS.FORM.AUDIENCE.${key.toUpperCase()}`),
icon: STANDARD_ICONS[key],
inputType: 'searchSelect',
options: languages,
dataType: 'text',
filterOperators: equalityOperators.value,
attributeModel: 'additional',
})),
]
),
conversationOption(
'browser_language',
t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.BROWSER_LANGUAGE'),
languages
),
conversationOption(
'conversation_language',
t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.CONVERSATION_LANGUAGE'),
languages
),
]);
const header = (id, label) => ({
@@ -103,25 +101,23 @@ export function useAudienceFilterTypes() {
});
const filterTypes = computed(() => {
const standard = [];
const custom = [];
const standard = contactFilterTypes.value
.filter(type => type.attributeModel !== 'customAttributes')
.map(type => ({
...type,
icon: STANDARD_ICONS[type.attributeKey] || DEFAULT_ICON,
}));
contactFilterTypes.value.forEach(type => {
if (type.attributeModel === 'customAttributes') {
// Only contact-model custom attributes belong here; never conversation/company ones.
if (!(type.attributeKey in customTypeByKey.value)) return;
const displayType = customTypeByKey.value[type.attributeKey];
custom.push({
...type,
icon: CUSTOM_TYPE_ICONS[displayType] || DEFAULT_ICON,
});
} else {
standard.push({
...type,
icon: STANDARD_ICONS[type.attributeKey] || DEFAULT_ICON,
});
}
});
// Only contact-model custom attributes belong here; never conversation/company ones.
const custom = contactFilterTypes.value
.filter(type => type.attributeModel === 'customAttributes')
.filter(type => type.attributeKey in customTypeByKey.value)
.map(type => ({
...type,
icon:
CUSTOM_TYPE_ICONS[customTypeByKey.value[type.attributeKey]] ||
DEFAULT_ICON,
}));
return [
header('contact', t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.GROUP_CONTACT')),