mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	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:
		| @@ -62,6 +62,7 @@ const segmentsQuery = ref({}); | |||||||
|  |  | ||||||
| const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4'); | const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4'); | ||||||
| const contactAttributes = useMapGetter('attributes/getContactAttributes'); | const contactAttributes = useMapGetter('attributes/getContactAttributes'); | ||||||
|  | const labels = useMapGetter('labels/getLabels'); | ||||||
| const hasActiveSegments = computed( | const hasActiveSegments = computed( | ||||||
|   () => props.activeSegment && props.segmentsId !== 0 |   () => props.activeSegment && props.segmentsId !== 0 | ||||||
| ); | ); | ||||||
| @@ -215,6 +216,7 @@ const setParamsForEditSegmentModal = () => { | |||||||
|     countries, |     countries, | ||||||
|     filterTypes: contactFilterItems, |     filterTypes: contactFilterItems, | ||||||
|     allCustomAttributes: useSnakeCase(contactAttributes.value), |     allCustomAttributes: useSnakeCase(contactAttributes.value), | ||||||
|  |     labels: labels.value || [], | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -34,14 +34,17 @@ const formatOperatorLabel = operator => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const formatFilterValue = value => { | const formatFilterValue = value => { | ||||||
|  |   // Case 1: null, undefined, empty string | ||||||
|   if (!value) return ''; |   if (!value) return ''; | ||||||
|  |  | ||||||
|  |   // Case 2: array → map each item, use name if present, else the item itself | ||||||
|   if (Array.isArray(value)) { |   if (Array.isArray(value)) { | ||||||
|     return value.join(', '); |     return value.map(item => item?.name ?? item).join(', '); | ||||||
|   } |   } | ||||||
|   if (typeof value === 'object' && value.name) { |  | ||||||
|     return value.name; |   // Case 3: object with a "name" property → return name | ||||||
|   } |   // Case 4: primitive (string, number, etc.) → return as is | ||||||
|   return value; |   return value?.name ?? value; | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| @@ -66,6 +69,7 @@ const formatFilterValue = value => { | |||||||
|           </span> |           </span> | ||||||
|           <span |           <span | ||||||
|             v-if="filter.values" |             v-if="filter.values" | ||||||
|  |             :title="formatFilterValue(filter.values)" | ||||||
|             class="lowercase truncate text-n-slate-12" |             class="lowercase truncate text-n-slate-12" | ||||||
|             :class="{ |             :class="{ | ||||||
|               'first-letter:capitalize': shouldCapitalizeFirstLetter( |               'first-letter:capitalize': shouldCapitalizeFirstLetter( | ||||||
|   | |||||||
| @@ -50,6 +50,7 @@ export function useContactFilterContext() { | |||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
|  |  | ||||||
|   const contactAttributes = useMapGetter('attributes/getContactAttributes'); |   const contactAttributes = useMapGetter('attributes/getContactAttributes'); | ||||||
|  |   const labels = useMapGetter('labels/getLabels'); | ||||||
|  |  | ||||||
|   const { |   const { | ||||||
|     equalityOperators, |     equalityOperators, | ||||||
| @@ -184,6 +185,20 @@ export function useContactFilterContext() { | |||||||
|       filterOperators: equalityOperators.value, |       filterOperators: equalityOperators.value, | ||||||
|       attributeModel: 'standard', |       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, |     ...customFilterTypes.value, | ||||||
|   ]); |   ]); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ export const CONTACT_ATTRIBUTES = { | |||||||
|   LAST_ACTIVITY_AT: 'last_activity_at', |   LAST_ACTIVITY_AT: 'last_activity_at', | ||||||
|   REFERER: 'referer', |   REFERER: 'referer', | ||||||
|   BLOCKED: 'blocked', |   BLOCKED: 'blocked', | ||||||
|  |   LABELS: 'labels', | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -48,7 +48,8 @@ | |||||||
|       "CREATED_AT": "Created At", |       "CREATED_AT": "Created At", | ||||||
|       "LAST_ACTIVITY": "Last Activity", |       "LAST_ACTIVITY": "Last Activity", | ||||||
|       "REFERER_LINK": "Referrer link", |       "REFERER_LINK": "Referrer link", | ||||||
|       "BLOCKED": "Blocked" |       "BLOCKED": "Blocked", | ||||||
|  |       "LABELS": "Labels" | ||||||
|     }, |     }, | ||||||
|     "GROUPS": { |     "GROUPS": { | ||||||
|       "STANDARD_FILTERS": "Standard Filters", |       "STANDARD_FILTERS": "Standard Filters", | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { | import { | ||||||
|   OPERATOR_TYPES_1, |   OPERATOR_TYPES_1, | ||||||
|  |   OPERATOR_TYPES_2, | ||||||
|   OPERATOR_TYPES_3, |   OPERATOR_TYPES_3, | ||||||
|   OPERATOR_TYPES_5, |   OPERATOR_TYPES_5, | ||||||
| } from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; | } from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; | ||||||
| @@ -84,6 +85,14 @@ const filterTypes = [ | |||||||
|     filterOperators: OPERATOR_TYPES_1, |     filterOperators: OPERATOR_TYPES_1, | ||||||
|     attributeModel: 'standard', |     attributeModel: 'standard', | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     attributeKey: 'labels', | ||||||
|  |     attributeI18nKey: 'LABELS', | ||||||
|  |     inputType: 'multi_select', | ||||||
|  |     dataType: 'text', | ||||||
|  |     filterOperators: OPERATOR_TYPES_2, | ||||||
|  |     attributeModel: 'standard', | ||||||
|  |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| export const filterAttributeGroups = [ | export const filterAttributeGroups = [ | ||||||
| @@ -127,6 +136,10 @@ export const filterAttributeGroups = [ | |||||||
|         key: 'blocked', |         key: 'blocked', | ||||||
|         i18nKey: 'BLOCKED', |         i18nKey: 'BLOCKED', | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         key: 'labels', | ||||||
|  |         i18nKey: 'LABELS', | ||||||
|  |       }, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sivin Varghese
					Sivin Varghese