refactor(captain): split useAudienceFilterTypes into smaller computeds
Extracts standard/custom/conversation sections into named computeds with a shared option factory, and adds specs for the composable.
This commit is contained in:
+30
-36
@@ -35,10 +35,12 @@ const CUSTOM_TYPE_ICONS = {
|
||||
|
||||
const DEFAULT_ICON = 'i-lucide-tag';
|
||||
|
||||
/**
|
||||
* Filter types for a Captain assistant audience: contact attributes (reused from the contact
|
||||
* segment builder) plus conversation-scoped fields, grouped via disabled header options.
|
||||
*/
|
||||
const header = (id, label) => ({
|
||||
value: `__group_${id}`,
|
||||
label,
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
export function useAudienceFilterTypes() {
|
||||
const { t } = useI18n();
|
||||
const { filterTypes: contactFilterTypes } = useContactFilterContext();
|
||||
@@ -67,7 +69,7 @@ export function useAudienceFilterTypes() {
|
||||
attributeModel: 'additional',
|
||||
});
|
||||
|
||||
const conversationFilterTypes = computed(() => [
|
||||
const conversationTypes = computed(() => [
|
||||
conversationOption(
|
||||
'hmac_verified',
|
||||
t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.LOGGED_IN'),
|
||||
@@ -94,22 +96,17 @@ export function useAudienceFilterTypes() {
|
||||
),
|
||||
]);
|
||||
|
||||
const header = (id, label) => ({
|
||||
value: `__group_${id}`,
|
||||
label,
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const filterTypes = computed(() => {
|
||||
const standard = contactFilterTypes.value
|
||||
const standardTypes = computed(() =>
|
||||
contactFilterTypes.value
|
||||
.filter(type => type.attributeModel !== 'customAttributes')
|
||||
.map(type => ({
|
||||
...type,
|
||||
icon: STANDARD_ICONS[type.attributeKey] || DEFAULT_ICON,
|
||||
}));
|
||||
}))
|
||||
);
|
||||
|
||||
// Only contact-model custom attributes belong here; never conversation/company ones.
|
||||
const custom = contactFilterTypes.value
|
||||
const customTypes = computed(() =>
|
||||
contactFilterTypes.value
|
||||
.filter(type => type.attributeModel === 'customAttributes')
|
||||
.filter(type => type.attributeKey in customTypeByKey.value)
|
||||
.map(type => ({
|
||||
@@ -117,27 +114,24 @@ export function useAudienceFilterTypes() {
|
||||
icon:
|
||||
CUSTOM_TYPE_ICONS[customTypeByKey.value[type.attributeKey]] ||
|
||||
DEFAULT_ICON,
|
||||
}));
|
||||
}))
|
||||
);
|
||||
|
||||
return [
|
||||
header('contact', t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.GROUP_CONTACT')),
|
||||
...standard,
|
||||
header(
|
||||
'conversation',
|
||||
t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.GROUP_CONVERSATION')
|
||||
),
|
||||
...conversationFilterTypes.value,
|
||||
...(custom.length
|
||||
? [
|
||||
header(
|
||||
'custom',
|
||||
t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.GROUP_CUSTOM')
|
||||
),
|
||||
...custom,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
});
|
||||
const filterTypes = computed(() => [
|
||||
header('contact', t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.GROUP_CONTACT')),
|
||||
...standardTypes.value,
|
||||
header(
|
||||
'conversation',
|
||||
t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.GROUP_CONVERSATION')
|
||||
),
|
||||
...conversationTypes.value,
|
||||
...(customTypes.value.length
|
||||
? [
|
||||
header('custom', t('CAPTAIN.ASSISTANTS.FORM.AUDIENCE.GROUP_CUSTOM')),
|
||||
...customTypes.value,
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
|
||||
return { filterTypes };
|
||||
}
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
|
||||
import { useAudienceFilterTypes } from './useAudienceFilterTypes';
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
contactFilterTypes: [],
|
||||
contactAttributes: [],
|
||||
equalityOperators: [{ value: 'equal_to' }, { value: 'not_equal_to' }],
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: key => key }),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/composables/store.js', () => ({
|
||||
useMapGetter: () => ({ value: state.contactAttributes }),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/components-next/filter/contactProvider.js', () => ({
|
||||
useContactFilterContext: () => ({
|
||||
filterTypes: { value: state.contactFilterTypes },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/components-next/filter/operators.js', () => ({
|
||||
useOperators: () => ({
|
||||
equalityOperators: { value: state.equalityOperators },
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useAudienceFilterTypes', () => {
|
||||
beforeEach(() => {
|
||||
state.contactFilterTypes = [
|
||||
{ attributeKey: 'email', attributeModel: 'standard' },
|
||||
{ attributeKey: 'mystery', attributeModel: 'standard' },
|
||||
{ attributeKey: 'signed_up_on', attributeModel: 'customAttributes' },
|
||||
{ attributeKey: 'company_size', attributeModel: 'customAttributes' },
|
||||
];
|
||||
state.contactAttributes = [
|
||||
{ attributeKey: 'signed_up_on', attributeDisplayType: 'date' },
|
||||
];
|
||||
});
|
||||
|
||||
const build = () => useAudienceFilterTypes().filterTypes.value;
|
||||
|
||||
it('assembles grouped sections in order', () => {
|
||||
const keys = build().map(item => item.attributeKey ?? item.value);
|
||||
expect(keys).toEqual([
|
||||
'__group_contact',
|
||||
'email',
|
||||
'mystery',
|
||||
'__group_conversation',
|
||||
'hmac_verified',
|
||||
'browser_language',
|
||||
'conversation_language',
|
||||
'__group_custom',
|
||||
'signed_up_on',
|
||||
]);
|
||||
});
|
||||
|
||||
it('marks headers as disabled options', () => {
|
||||
const headers = build().filter(item => item.value?.startsWith('__group_'));
|
||||
expect(headers).toHaveLength(3);
|
||||
expect(headers.every(header => header.disabled)).toBe(true);
|
||||
});
|
||||
|
||||
it('excludes custom attributes that are not contact-model attributes', () => {
|
||||
const keys = build().map(item => item.attributeKey);
|
||||
expect(keys).not.toContain('company_size');
|
||||
});
|
||||
|
||||
it('omits the custom section when there are no contact custom attributes', () => {
|
||||
state.contactAttributes = [];
|
||||
const values = build().map(item => item.attributeKey ?? item.value);
|
||||
expect(values).not.toContain('__group_custom');
|
||||
expect(values).not.toContain('signed_up_on');
|
||||
});
|
||||
|
||||
it('assigns icons by attribute key with a default fallback', () => {
|
||||
const byKey = Object.fromEntries(
|
||||
build().map(item => [item.attributeKey, item.icon])
|
||||
);
|
||||
expect(byKey.email).toBe('i-lucide-mail');
|
||||
expect(byKey.mystery).toBe('i-lucide-tag');
|
||||
expect(byKey.signed_up_on).toBe('i-lucide-calendar');
|
||||
});
|
||||
|
||||
it('builds conversation options as searchSelects with equality operators', () => {
|
||||
const types = build();
|
||||
const hmac = types.find(item => item.attributeKey === 'hmac_verified');
|
||||
expect(hmac.inputType).toBe('searchSelect');
|
||||
expect(hmac.filterOperators).toEqual(state.equalityOperators);
|
||||
expect(hmac.options.map(option => option.id)).toEqual(['true', 'false']);
|
||||
|
||||
const browserLanguage = types.find(
|
||||
item => item.attributeKey === 'browser_language'
|
||||
);
|
||||
expect(browserLanguage.options).toEqual(languages);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user