diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/audience/useAudienceFilterTypes.js b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/audience/useAudienceFilterTypes.js index ad5c5046a..f3479ff23 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/audience/useAudienceFilterTypes.js +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/audience/useAudienceFilterTypes.js @@ -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 }; } diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/audience/useAudienceFilterTypes.spec.js b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/audience/useAudienceFilterTypes.spec.js new file mode 100644 index 000000000..2ef1e3cbb --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/audience/useAudienceFilterTypes.spec.js @@ -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); + }); +});