diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 476fc7438..05207664f 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -4,6 +4,7 @@ import { mapGetters } from 'vuex'; import { useUISettings } from 'dashboard/composables/useUISettings'; import { useAlert } from 'dashboard/composables'; import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; +import { useFilter } from 'shared/composables/useFilter'; import VirtualList from 'vue-virtual-scroll-list'; import ChatListHeader from './ChatListHeader.vue'; @@ -16,7 +17,6 @@ import filterQueryGenerator from '../helper/filterQueryGenerator.js'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue'; import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue'; -import filterMixin from 'shared/mixins/filterMixin'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import countries from 'shared/constants/countries'; import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper'; @@ -41,7 +41,6 @@ export default { IntersectionObserver, VirtualList, }, - mixins: [filterMixin], provide() { return { // Actions to be performed on virtual list item and context menu. @@ -91,6 +90,15 @@ export default { const conversationListRef = ref(null); + const { + setFilterAttributes, + initializeStatusAndAssigneeFilterToModal, + initializeInboxTeamAndLabelFilterToModal, + } = useFilter({ + filteri18nKey: 'FILTER', + attributeModel: 'conversation_attribute', + }); + const getKeyboardListenerParams = () => { const allConversations = conversationListRef.value.querySelectorAll( 'div.conversations-list div.conversation' @@ -146,6 +154,9 @@ export default { return { uiSettings, conversationListRef, + setFilterAttributes, + initializeStatusAndAssigneeFilterToModal, + initializeInboxTeamAndLabelFilterToModal, }; }, data() { @@ -860,6 +871,25 @@ export default { onContextMenuToggle(state) { this.isContextMenuOpen = state; }, + initializeExistingFilterToModal() { + const statusFilter = this.initializeStatusAndAssigneeFilterToModal( + this.activeStatus, + this.currentUserDetails, + this.activeAssigneeTab + ); + if (statusFilter) { + this.appliedFilter.push(statusFilter); + } + + const otherFilters = this.initializeInboxTeamAndLabelFilterToModal( + this.conversationInbox, + this.inbox, + this.teamId, + this.activeTeam, + this.label + ); + this.appliedFilter.push(...otherFilters); + }, }, }; diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue index b0be89160..7bd5fdb13 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue @@ -5,7 +5,7 @@ import languages from './advancedFilterItems/languages'; import countries from 'shared/constants/countries.js'; import { mapGetters } from 'vuex'; import { filterAttributeGroups } from './advancedFilterItems'; -import filterMixin from 'shared/mixins/filterMixin'; +import { useFilter } from 'shared/composables/useFilter'; import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js'; @@ -14,7 +14,6 @@ export default { components: { FilterInputBox, }, - mixins: [filterMixin], props: { onClose: { type: Function, @@ -37,6 +36,15 @@ export default { default: false, }, }, + setup() { + const { setFilterAttributes } = useFilter({ + filteri18nKey: 'FILTER', + attributeModel: 'conversation_attribute', + }); + return { + setFilterAttributes, + }; + }, data() { return { show: true, @@ -67,7 +75,11 @@ export default { }, }, mounted() { - this.setFilterAttributes(); + const { filterGroups, filterTypes } = this.setFilterAttributes(); + + this.filterTypes = [...this.filterTypes, ...filterTypes]; + this.filterGroups = filterGroups; + this.$store.dispatch('campaigns/get'); if (this.getAppliedConversationFilters.length) { this.appliedFilters = []; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue index 41c986d03..2339f3be7 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue @@ -4,7 +4,7 @@ import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue import countries from 'shared/constants/countries.js'; import { mapGetters } from 'vuex'; import { filterAttributeGroups } from '../contactFilterItems'; -import filterMixin from 'shared/mixins/filterMixin'; +import { useFilter } from 'shared/composables/useFilter'; import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js'; @@ -13,7 +13,6 @@ export default { components: { FilterInputBox, }, - mixins: [filterMixin], props: { onClose: { type: Function, @@ -36,6 +35,15 @@ export default { default: '', }, }, + setup() { + const { setFilterAttributes } = useFilter({ + filteri18nKey: 'CONTACTS_FILTER', + attributeModel: 'contact_attribute', + }); + return { + setFilterAttributes, + }; + }, data() { return { show: true, @@ -69,7 +77,10 @@ export default { }, }, mounted() { - this.setFilterAttributes(); + const { filterGroups, filterTypes } = this.setFilterAttributes(); + this.filterTypes = [...this.filterTypes, ...filterTypes]; + this.filterGroups = filterGroups; + if (this.getAppliedContactFilters.length) { this.appliedFilters = [...this.getAppliedContactFilters]; } else if (!this.isSegmentsView) { diff --git a/app/javascript/shared/composables/specs/useFilter.spec.js b/app/javascript/shared/composables/specs/useFilter.spec.js new file mode 100644 index 000000000..68922403a --- /dev/null +++ b/app/javascript/shared/composables/specs/useFilter.spec.js @@ -0,0 +1,145 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { useFilter } from '../useFilter'; +import { useStore } from 'dashboard/composables/store'; +import { useI18n } from 'dashboard/composables/useI18n'; + +// Mock the dependencies +vi.mock('dashboard/composables/store'); +vi.mock('dashboard/composables/useI18n'); + +describe('useFilter', () => { + // Setup mocks + beforeEach(() => { + vi.mocked(useStore).mockReturnValue({ + getters: { + 'attributes/getAttributesByModel': vi.fn(), + }, + }); + vi.mocked(useI18n).mockReturnValue({ + t: vi.fn(key => key), + }); + }); + + it('should return the correct functions', () => { + const { + setFilterAttributes, + initializeStatusAndAssigneeFilterToModal, + initializeInboxTeamAndLabelFilterToModal, + } = useFilter({ filteri18nKey: 'TEST', attributeModel: 'conversation' }); + + expect(setFilterAttributes).toBeDefined(); + expect(initializeStatusAndAssigneeFilterToModal).toBeDefined(); + expect(initializeInboxTeamAndLabelFilterToModal).toBeDefined(); + }); + + describe('setFilterAttributes', () => { + it('should return filterGroups and filterTypes', () => { + const mockAttributes = [ + { + attribute_key: 'test_key', + attribute_display_name: 'Test Name', + attribute_display_type: 'text', + }, + ]; + vi.mocked(useStore)().getters[ + 'attributes/getAttributesByModel' + ].mockReturnValue(mockAttributes); + + const { setFilterAttributes } = useFilter({ + filteri18nKey: 'TEST', + attributeModel: 'conversation', + }); + const result = setFilterAttributes(); + + expect(result).toHaveProperty('filterGroups'); + expect(result).toHaveProperty('filterTypes'); + expect(result.filterGroups.length).toBeGreaterThan(0); + expect(result.filterTypes.length).toBeGreaterThan(0); + }); + }); + + describe('initializeStatusAndAssigneeFilterToModal', () => { + it('should return status filter when activeStatus is provided', () => { + const { initializeStatusAndAssigneeFilterToModal } = useFilter({ + filteri18nKey: 'TEST', + attributeModel: 'conversation', + }); + const result = initializeStatusAndAssigneeFilterToModal('open', {}, ''); + + expect(result).toEqual({ + attribute_key: 'status', + attribute_model: 'standard', + filter_operator: 'equal_to', + values: [ + { id: 'open', name: 'CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT' }, + ], + query_operator: 'and', + custom_attribute_type: '', + }); + }); + + it('should return null when no active filters', () => { + const { initializeStatusAndAssigneeFilterToModal } = useFilter({ + filteri18nKey: 'TEST', + attributeModel: 'conversation', + }); + const result = initializeStatusAndAssigneeFilterToModal('', {}, ''); + + expect(result).toBeNull(); + }); + }); + + describe('initializeInboxTeamAndLabelFilterToModal', () => { + it('should return filters for inbox, team, and label when provided', () => { + const { initializeInboxTeamAndLabelFilterToModal } = useFilter({ + filteri18nKey: 'TEST', + attributeModel: 'conversation', + }); + const result = initializeInboxTeamAndLabelFilterToModal( + 1, + { name: 'Inbox 1' }, + 2, + [{ id: 2, name: 'Team 2' }], + 'Label 1' + ); + + expect(result).toHaveLength(3); + expect(result[0]).toHaveProperty('attribute_key', 'inbox_id'); + expect(result[1]).toHaveProperty('attribute_key', 'team_id'); + expect(result[2]).toHaveProperty('attribute_key', 'labels'); + }); + + it('should return empty array when no filters are provided', () => { + const { initializeInboxTeamAndLabelFilterToModal } = useFilter({ + filteri18nKey: 'TEST', + attributeModel: 'conversation', + }); + const result = initializeInboxTeamAndLabelFilterToModal( + null, + null, + null, + null, + null + ); + + expect(result).toEqual([]); + }); + + it('should return only inbox filter when only inbox is provided', () => { + const { initializeInboxTeamAndLabelFilterToModal } = useFilter({ + filteri18nKey: 'TEST', + attributeModel: 'conversation', + }); + const result = initializeInboxTeamAndLabelFilterToModal( + 1, + { name: 'Inbox 1' }, + null, + null, + null + ); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty('attribute_key', 'inbox_id'); + }); + }); +}); diff --git a/app/javascript/shared/composables/useFilter.js b/app/javascript/shared/composables/useFilter.js new file mode 100644 index 000000000..903269ffb --- /dev/null +++ b/app/javascript/shared/composables/useFilter.js @@ -0,0 +1,175 @@ +import wootConstants from 'dashboard/constants/globals'; +import { useStore } from 'dashboard/composables/store'; +import { useI18n } from 'dashboard/composables/useI18n'; +import { filterAttributeGroups } from 'dashboard/components/widgets/conversation/advancedFilterItems'; +import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; + +const customAttributeInputType = key => { + switch (key) { + case 'date': + return 'date'; + case 'text': + return 'plain_text'; + case 'list': + return 'search_select'; + case 'checkbox': + return 'search_select'; + default: + return 'plain_text'; + } +}; + +const getOperatorTypes = key => { + switch (key) { + case 'list': + return OPERATORS.OPERATOR_TYPES_1; + case 'text': + return OPERATORS.OPERATOR_TYPES_3; + case 'number': + return OPERATORS.OPERATOR_TYPES_1; + case 'link': + return OPERATORS.OPERATOR_TYPES_1; + case 'date': + return OPERATORS.OPERATOR_TYPES_4; + case 'checkbox': + return OPERATORS.OPERATOR_TYPES_1; + default: + return OPERATORS.OPERATOR_TYPES_1; + } +}; + +export const useFilter = ({ filteri18nKey, attributeModel }) => { + const { t: $t } = useI18n(); + const { getters } = useStore(); + + const setFilterAttributes = () => { + const allCustomAttributes = + getters['attributes/getAttributesByModel'](attributeModel); + + const customAttributesFormatted = { + name: $t(`${filteri18nKey}.GROUPS.CUSTOM_ATTRIBUTES`), + attributes: allCustomAttributes.map(attr => { + return { + key: attr.attribute_key, + name: attr.attribute_display_name, + }; + }), + }; + + const allFilterGroups = filterAttributeGroups.map(group => { + return { + name: $t(`${filteri18nKey}.GROUPS.${group.i18nGroup}`), + attributes: group.attributes.map(attribute => { + return { + key: attribute.key, + name: $t(`${filteri18nKey}.ATTRIBUTES.${attribute.i18nKey}`), + }; + }), + }; + }); + + const customAttributeTypes = allCustomAttributes.map(attr => { + return { + attributeKey: attr.attribute_key, + attributeI18nKey: `CUSTOM_ATTRIBUTE_${attr.attribute_display_type.toUpperCase()}`, + inputType: customAttributeInputType(attr.attribute_display_type), + filterOperators: getOperatorTypes(attr.attribute_display_type), + attributeModel: 'custom_attributes', + }; + }); + + return { + filterGroups: [...allFilterGroups, customAttributesFormatted], + filterTypes: [...customAttributeTypes], + }; + }; + + const initializeStatusAndAssigneeFilterToModal = ( + activeStatus, + currentUserDetails, + activeAssigneeTab + ) => { + if (activeStatus !== '') { + return { + attribute_key: 'status', + attribute_model: 'standard', + filter_operator: 'equal_to', + values: [ + { + id: activeStatus, + name: $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`), + }, + ], + query_operator: 'and', + custom_attribute_type: '', + }; + } + if (activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ME) { + return { + attribute_key: 'assignee_id', + filter_operator: 'equal_to', + values: currentUserDetails, + query_operator: 'and', + custom_attribute_type: '', + }; + } + return null; + }; + + const initializeInboxTeamAndLabelFilterToModal = ( + conversationInbox, + inbox, + teamId, + activeTeam, + label + ) => { + const filters = []; + if (conversationInbox) { + filters.push({ + attribute_key: 'inbox_id', + attribute_model: 'standard', + filter_operator: 'equal_to', + values: [ + { + id: conversationInbox, + name: inbox.name, + }, + ], + query_operator: 'and', + custom_attribute_type: '', + }); + } + if (teamId) { + filters.push({ + attribute_key: 'team_id', + attribute_model: 'standard', + filter_operator: 'equal_to', + values: activeTeam, + query_operator: 'and', + custom_attribute_type: '', + }); + } + if (label) { + filters.push({ + attribute_key: 'labels', + attribute_model: 'standard', + filter_operator: 'equal_to', + values: [ + { + id: label, + name: label, + }, + ], + query_operator: 'and', + custom_attribute_type: '', + }); + } + return filters; + }; + + return { + setFilterAttributes, + initializeStatusAndAssigneeFilterToModal, + initializeInboxTeamAndLabelFilterToModal, + }; +}; diff --git a/app/javascript/shared/mixins/filterMixin.js b/app/javascript/shared/mixins/filterMixin.js deleted file mode 100644 index 7d402ba02..000000000 --- a/app/javascript/shared/mixins/filterMixin.js +++ /dev/null @@ -1,119 +0,0 @@ -import wootConstants from 'dashboard/constants/globals'; - -export default { - methods: { - setFilterAttributes() { - const allCustomAttributes = this.$store.getters[ - 'attributes/getAttributesByModel' - ](this.attributeModel); - const customAttributesFormatted = { - name: this.$t(`${this.filtersFori18n}.GROUPS.CUSTOM_ATTRIBUTES`), - attributes: allCustomAttributes.map(attr => { - return { - key: attr.attribute_key, - name: attr.attribute_display_name, - }; - }), - }; - const allFilterGroups = this.filterAttributeGroups.map(group => { - return { - name: this.$t(`${this.filtersFori18n}.GROUPS.${group.i18nGroup}`), - attributes: group.attributes.map(attribute => { - return { - key: attribute.key, - name: this.$t( - `${this.filtersFori18n}.ATTRIBUTES.${attribute.i18nKey}` - ), - }; - }), - }; - }); - const customAttributeTypes = allCustomAttributes.map(attr => { - return { - attributeKey: attr.attribute_key, - attributeI18nKey: `CUSTOM_ATTRIBUTE_${attr.attribute_display_type.toUpperCase()}`, - inputType: this.customAttributeInputType(attr.attribute_display_type), - filterOperators: this.getOperatorTypes(attr.attribute_display_type), - attributeModel: 'custom_attributes', - }; - }); - this.filterTypes = [...this.filterTypes, ...customAttributeTypes]; - this.filterGroups = [...allFilterGroups, customAttributesFormatted]; - }, - - initializeExistingFilterToModal() { - this.initializeStatusAndAssigneeFilterToModal(); - this.initializeInboxTeamAndLabelFilterToModal(); - }, - initializeStatusAndAssigneeFilterToModal() { - if (this.activeStatus !== '') { - this.appliedFilter.push({ - attribute_key: 'status', - attribute_model: 'standard', - filter_operator: 'equal_to', - values: [ - { - id: this.activeStatus, - name: this.$t( - `CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${this.activeStatus}.TEXT` - ), - }, - ], - query_operator: 'and', - custom_attribute_type: '', - }); - } - if (this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ME) { - this.appliedFilter.push({ - attribute_key: 'assignee_id', - filter_operator: 'equal_to', - values: this.currentUserDetails, - query_operator: 'and', - custom_attribute_type: '', - }); - } - }, - initializeInboxTeamAndLabelFilterToModal() { - if (this.conversationInbox) { - this.appliedFilter.push({ - attribute_key: 'inbox_id', - attribute_model: 'standard', - filter_operator: 'equal_to', - values: [ - { - id: this.conversationInbox, - name: this.inbox.name, - }, - ], - query_operator: 'and', - custom_attribute_type: '', - }); - } - if (this.teamId) { - this.appliedFilter.push({ - attribute_key: 'team_id', - attribute_model: 'standard', - filter_operator: 'equal_to', - values: this.activeTeam, - query_operator: 'and', - custom_attribute_type: '', - }); - } - if (this.label) { - this.appliedFilter.push({ - attribute_key: 'labels', - attribute_model: 'standard', - filter_operator: 'equal_to', - values: [ - { - id: this.label, - name: this.label, - }, - ], - query_operator: 'and', - custom_attribute_type: '', - }); - } - }, - }, -}; diff --git a/app/javascript/shared/mixins/specs/filterMixin.spec.js b/app/javascript/shared/mixins/specs/filterMixin.spec.js deleted file mode 100644 index 4b54e9326..000000000 --- a/app/javascript/shared/mixins/specs/filterMixin.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import filterMixin from '../filterMixin'; -import { shallowMount } from '@vue/test-utils'; -import MockComponent from './MockComponent.vue'; - -describe('Test mixin function', () => { - const wrapper = shallowMount(MockComponent, { - mixins: [filterMixin], - }); - - it('should return proper value from bool', () => { - expect(wrapper.vm.setFilterAttributes).toBeTruthy(); - }); - - it('should return proper value from bool', () => { - expect(wrapper.vm.initializeExistingFilterToModal).toBeTruthy(); - }); -});