feat: Ability to filter contact based on labels (#12343)

# Pull Request Template

## Description

This PR add the ability to filter contact based on labels.

Fixes
https://linear.app/chatwoot/issue/CW-4001/feat-ability-to-filter-contact-based-on-labels

## Type of change

- [x] New feature (non-breaking change which adds functionality

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/f3d58d0fcee844b7817325a9a19929d3?sid=075b9448-7e6d-4180-af3c-9466fbf2138b


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Sivin Varghese
2025-09-02 12:35:18 +05:30
committed by GitHub
parent d68ac25187
commit 8606aa1310
6 changed files with 42 additions and 6 deletions

View File

@@ -62,6 +62,7 @@ const segmentsQuery = ref({});
const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4');
const contactAttributes = useMapGetter('attributes/getContactAttributes');
const labels = useMapGetter('labels/getLabels');
const hasActiveSegments = computed(
() => props.activeSegment && props.segmentsId !== 0
);
@@ -215,6 +216,7 @@ const setParamsForEditSegmentModal = () => {
countries,
filterTypes: contactFilterItems,
allCustomAttributes: useSnakeCase(contactAttributes.value),
labels: labels.value || [],
};
};

View File

@@ -34,14 +34,17 @@ const formatOperatorLabel = operator => {
};
const formatFilterValue = value => {
// Case 1: null, undefined, empty string
if (!value) return '';
// Case 2: array → map each item, use name if present, else the item itself
if (Array.isArray(value)) {
return value.join(', ');
return value.map(item => item?.name ?? item).join(', ');
}
if (typeof value === 'object' && value.name) {
return value.name;
}
return value;
// Case 3: object with a "name" property → return name
// Case 4: primitive (string, number, etc.) → return as is
return value?.name ?? value;
};
</script>
@@ -66,6 +69,7 @@ const formatFilterValue = value => {
</span>
<span
v-if="filter.values"
:title="formatFilterValue(filter.values)"
class="lowercase truncate text-n-slate-12"
:class="{
'first-letter:capitalize': shouldCapitalizeFirstLetter(

View File

@@ -50,6 +50,7 @@ export function useContactFilterContext() {
const { t } = useI18n();
const contactAttributes = useMapGetter('attributes/getContactAttributes');
const labels = useMapGetter('labels/getLabels');
const {
equalityOperators,
@@ -184,6 +185,20 @@ export function useContactFilterContext() {
filterOperators: equalityOperators.value,
attributeModel: 'standard',
},
{
attributeKey: CONTACT_ATTRIBUTES.LABELS,
value: CONTACT_ATTRIBUTES.LABELS,
attributeName: t('CONTACTS_FILTER.ATTRIBUTES.LABELS'),
label: t('CONTACTS_FILTER.ATTRIBUTES.LABELS'),
inputType: 'multiSelect',
options: labels.value?.map(label => ({
id: label.title,
name: label.title,
})),
dataType: 'text',
filterOperators: equalityOperators.value,
attributeModel: 'standard',
},
...customFilterTypes.value,
]);

View File

@@ -28,6 +28,7 @@ export const CONTACT_ATTRIBUTES = {
LAST_ACTIVITY_AT: 'last_activity_at',
REFERER: 'referer',
BLOCKED: 'blocked',
LABELS: 'labels',
};
/**

View File

@@ -48,7 +48,8 @@
"CREATED_AT": "Created At",
"LAST_ACTIVITY": "Last Activity",
"REFERER_LINK": "Referrer link",
"BLOCKED": "Blocked"
"BLOCKED": "Blocked",
"LABELS": "Labels"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard Filters",

View File

@@ -1,5 +1,6 @@
import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_2,
OPERATOR_TYPES_3,
OPERATOR_TYPES_5,
} from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
@@ -84,6 +85,14 @@ const filterTypes = [
filterOperators: OPERATOR_TYPES_1,
attributeModel: 'standard',
},
{
attributeKey: 'labels',
attributeI18nKey: 'LABELS',
inputType: 'multi_select',
dataType: 'text',
filterOperators: OPERATOR_TYPES_2,
attributeModel: 'standard',
},
];
export const filterAttributeGroups = [
@@ -127,6 +136,10 @@ export const filterAttributeGroups = [
key: 'blocked',
i18nKey: 'BLOCKED',
},
{
key: 'labels',
i18nKey: 'LABELS',
},
],
},
];