From 1303469087d80706b732e8f4135c138bf7a913ac Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 20 Mar 2024 18:11:50 +0530 Subject: [PATCH] feat: Ability filter blocked contacts (#9048) - This PR introduces the ability to filter blocked contacts from the contacts filter UI --- app/helpers/filter_helper.rb | 24 +- .../i18n/locale/en/contactFilters.json | 3 +- .../components/ContactsAdvancedFilters.vue | 2 +- .../contacts/contactFilterItems/index.js | 12 + lib/filters/filter_keys.yml | 22 +- spec/services/contacts/filter_service_spec.rb | 290 ++++++++++-------- 6 files changed, 197 insertions(+), 156 deletions(-) diff --git a/app/helpers/filter_helper.rb b/app/helpers/filter_helper.rb index b3efc393ab75..bce2de5ea720 100644 --- a/app/helpers/filter_helper.rb +++ b/app/helpers/filter_helper.rb @@ -34,7 +34,7 @@ def build_condition_query_string(current_filter, query_hash, current_index) case current_filter['attribute_type'] when 'additional_attributes' - handle_additional_attributes(query_hash, filter_operator_value) + handle_additional_attributes(query_hash, filter_operator_value, current_filter['data_type']) else handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value) end @@ -45,10 +45,8 @@ def handle_nil_filter(query_hash, current_index) custom_attribute_query(query_hash, attribute_type, current_index) end - # TODO: Change the reliance on entity instead introduce datatype text_case_insensive - # Then we can remove the condition for Contact - def handle_additional_attributes(query_hash, filter_operator_value) - if filter_config[:entity] == 'Contact' + def handle_additional_attributes(query_hash, filter_operator_value, data_type) + if data_type == 'text_case_insensitive' "LOWER(#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}') " \ "#{filter_operator_value} #{query_hash[:query_operator]}" else @@ -63,6 +61,8 @@ def handle_standard_attributes(current_filter, query_hash, current_index, filter date_filter(current_filter, query_hash, filter_operator_value) when 'labels' tag_filter_query(query_hash, current_index) + when 'text_case_insensitive' + text_case_insensitive_filter(query_hash, filter_operator_value) else default_filter(query_hash, filter_operator_value) end @@ -73,14 +73,12 @@ def date_filter(current_filter, query_hash, filter_operator_value) "#{filter_operator_value}#{current_filter['data_type']} #{query_hash[:query_operator]}" end - # TODO: Change the reliance on entity instead introduce datatype text_case_insensive - # Then we can remove the condition for Contact + def text_case_insensitive_filter(query_hash, filter_operator_value) + "LOWER(#{filter_config[:table_name]}.#{query_hash[:attribute_key]}) " \ + "#{filter_operator_value} #{query_hash[:query_operator]}" + end + def default_filter(query_hash, filter_operator_value) - if filter_config[:entity] == 'Contact' - "LOWER(#{filter_config[:table_name]}.#{query_hash[:attribute_key]}) " \ - "#{filter_operator_value} #{query_hash[:query_operator]}" - else - "#{filter_config[:table_name]}.#{query_hash[:attribute_key]} #{filter_operator_value} #{query_hash[:query_operator]}" - end + "#{filter_config[:table_name]}.#{query_hash[:attribute_key]} #{filter_operator_value} #{query_hash[:query_operator]}" end end diff --git a/app/javascript/dashboard/i18n/locale/en/contactFilters.json b/app/javascript/dashboard/i18n/locale/en/contactFilters.json index 09a543984c7e..02d5dcf890c1 100644 --- a/app/javascript/dashboard/i18n/locale/en/contactFilters.json +++ b/app/javascript/dashboard/i18n/locale/en/contactFilters.json @@ -44,7 +44,8 @@ "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CREATED_AT": "Created At", "LAST_ACTIVITY": "Last Activity", - "REFERER_LINK": "Referrer link" + "REFERER_LINK": "Referrer link", + "BLOCKED": "Blocked" }, "GROUPS": { "STANDARD_FILTERS": "Standard Filters", diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue index 41ceb25e513b..6f5e92017d5b 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue @@ -243,7 +243,7 @@ export default { attr.attribute_display_type === 'checkbox' ); }); - if (isCustomAttributeCheckbox) { + if (isCustomAttributeCheckbox || type === 'blocked') { return [ { id: true, diff --git a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js index 2d54c37bc8ef..59376f8ef9b9 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js +++ b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js @@ -76,6 +76,14 @@ const filterTypes = [ filterOperators: OPERATOR_TYPES_5, attributeModel: 'standard', }, + { + attributeKey: 'blocked', + attributeI18nKey: 'BLOCKED', + inputType: 'search_select', + dataType: 'text', + filterOperators: OPERATOR_TYPES_1, + attributeModel: 'standard', + }, ]; export const filterAttributeGroups = [ @@ -115,6 +123,10 @@ export const filterAttributeGroups = [ key: 'last_activity_at', i18nKey: 'LAST_ACTIVITY', }, + { + key: 'blocked', + i18nKey: 'BLOCKED', + }, ], }, ]; diff --git a/lib/filters/filter_keys.yml b/lib/filters/filter_keys.yml index afdcc6be7a8e..598ab84d6405 100644 --- a/lib/filters/filter_keys.yml +++ b/lib/filters/filter_keys.yml @@ -8,7 +8,7 @@ # - Parent Key (conversation, contact, messages) # - Key (attribute_name) # - attribute_type: "standard" : supported ["standard", "additional_attributes (only for conversations and messages)"] -# - data_type: "text" : supported ["text", "number", "labels", "date", "link"] +# - data_type: "text" : supported ["text", "text_case_insensitive", "number", "boolean", "labels", "date", "link"] # - filter_operators: ["equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present", "is_greater_than", "is_less_than", "days_before", "starts_with"] ### ----- Conversation Filters ----- ### @@ -124,7 +124,7 @@ conversations: contacts: name: attribute_type: "standard" - data_type: "text" + data_type: "text_case_insensitive" filter_operators: - "equal_to" - "not_equal_to" @@ -132,7 +132,7 @@ contacts: - "does_not_contain" phone_number: attribute_type: "standard" - data_type: "text" + data_type: "text_case_insensitive" filter_operators: - "equal_to" - "not_equal_to" @@ -141,7 +141,7 @@ contacts: - "starts_with" email: attribute_type: "standard" - data_type: "text" + data_type: "text_case_insensitive" filter_operators: - "equal_to" - "not_equal_to" @@ -149,19 +149,19 @@ contacts: - "does_not_contain" identifier: attribute_type: "standard" - data_type: "text" + data_type: "text_case_insensitive" filter_operators: - "equal_to" - "not_equal_to" country_code: attribute_type: "additional_attributes" - data_type: "text" + data_type: "text_case_insensitive" filter_operators: - "equal_to" - "not_equal_to" city: attribute_type: "additional_attributes" - data_type: "text" + data_type: "text_case_insensitive" filter_operators: - "equal_to" - "not_equal_to" @@ -169,7 +169,7 @@ contacts: - "does_not_contain" company: attribute_type: "additional_attributes" - data_type: "text" + data_type: "text_case_insensitive" filter_operators: - "equal_to" - "not_equal_to" @@ -197,6 +197,12 @@ contacts: - "is_greater_than" - "is_less_than" - "days_before" + blocked: + attribute_type: "standard" + data_type: "boolean" + filter_operators: + - "equal_to" + - "not_equal_to" ### ----- End of Contact Filters ----- ### diff --git a/spec/services/contacts/filter_service_spec.rb b/spec/services/contacts/filter_service_spec.rb index e8c0ff184e45..a373cf81ed06 100644 --- a/spec/services/contacts/filter_service_spec.rb +++ b/spec/services/contacts/filter_service_spec.rb @@ -37,6 +37,8 @@ end describe '#perform' do + let!(:params) { { payload: [], page: 1 } } + before do en_contact.update_labels(%w[random_label support]) cs_contact.update_labels('support') @@ -46,106 +48,184 @@ cs_contact.update!(custom_attributes: { customer_type: 'platinum', signed_in_at: '2022-01-19' }) end - context 'with query present' do - let!(:params) { { payload: [], page: 1 } } - let(:payload) do - [ + context 'with standard attributes - name' do + it 'filter contacts by name' do + params[:payload] = [ { - attribute_key: 'country_code', + attribute_key: 'name', filter_operator: 'equal_to', - values: ['uk'], + values: [en_contact.name], query_operator: nil }.with_indifferent_access ] - end - context 'with label filter' do - it 'returns equal_to filter results properly' do - params[:payload] = [ - { - attribute_key: 'labels', - filter_operator: 'equal_to', - values: ['support'], - query_operator: nil - }.with_indifferent_access - ] - - result = filter_service.new(params, first_user).perform - expect(result[:contacts].length).to be 2 - expect(result[:contacts].first.label_list).to include('support') - expect(result[:contacts].last.label_list).to include('support') - end - - it 'returns not_equal_to filter results properly' do - params[:payload] = [ - { - attribute_key: 'labels', - filter_operator: 'not_equal_to', - values: ['support'], - query_operator: nil - }.with_indifferent_access - ] - - result = filter_service.new(params, first_user).perform - expect(result[:contacts].length).to be 1 - expect(result[:contacts].first.id).to eq el_contact.id - end - - it 'returns is_present filter results properly' do - params[:payload] = [ - { - attribute_key: 'labels', - filter_operator: 'is_present', - values: [], - query_operator: nil - }.with_indifferent_access - ] - - result = filter_service.new(params, first_user).perform - expect(result[:contacts].length).to be 2 - expect(result[:contacts].first.label_list).to include('support') - expect(result[:contacts].last.label_list).to include('support') - end - - it 'returns is_not_present filter results properly' do - params[:payload] = [ - { - attribute_key: 'labels', - filter_operator: 'is_not_present', - values: [], - query_operator: nil - }.with_indifferent_access - ] - - result = filter_service.new(params, first_user).perform - expect(result[:contacts].length).to be 1 - expect(result[:contacts].first.id).to eq el_contact.id - end + result = filter_service.new(params, first_user).perform + expect(result[:count]).to be 1 + expect(result[:contacts].length).to be 1 + expect(result[:contacts].first.name).to eq(en_contact.name) end + end - it 'filter contacts by additional_attributes' do - params[:payload] = payload + context 'with standard attributes - blocked' do + it 'filter contacts by blocked' do + blocked_contact = create(:contact, account: account, blocked: true) + params = { payload: [{ attribute_key: 'blocked', filter_operator: 'equal_to', values: ['true'], + query_operator: nil }.with_indifferent_access] } result = filter_service.new(params, first_user).perform expect(result[:count]).to be 1 - expect(result[:contacts].first.id).to eq(en_contact.id) + expect(result[:contacts].first.id).to eq(blocked_contact.id) end - it 'filter contacts by name' do + it 'filter contacts by not_blocked' do + params = { payload: [{ attribute_key: 'blocked', filter_operator: 'equal_to', values: [false], + query_operator: nil }.with_indifferent_access] } + result = filter_service.new(params, first_user).perform + # existing contacts are not blocked + expect(result[:count]).to be 3 + end + end + + context 'with standard attributes - label' do + it 'returns equal_to filter results properly' do params[:payload] = [ { - attribute_key: 'name', + attribute_key: 'labels', filter_operator: 'equal_to', - values: [en_contact.name], + values: ['support'], + query_operator: nil + }.with_indifferent_access + ] + + result = filter_service.new(params, first_user).perform + expect(result[:contacts].length).to be 2 + expect(result[:contacts].first.label_list).to include('support') + expect(result[:contacts].last.label_list).to include('support') + end + + it 'returns not_equal_to filter results properly' do + params[:payload] = [ + { + attribute_key: 'labels', + filter_operator: 'not_equal_to', + values: ['support'], query_operator: nil }.with_indifferent_access ] result = filter_service.new(params, first_user).perform - expect(result[:count]).to be 1 expect(result[:contacts].length).to be 1 - expect(result[:contacts].first.name).to eq(en_contact.name) + expect(result[:contacts].first.id).to eq el_contact.id + end + + it 'returns is_present filter results properly' do + params[:payload] = [ + { + attribute_key: 'labels', + filter_operator: 'is_present', + values: [], + query_operator: nil + }.with_indifferent_access + ] + + result = filter_service.new(params, first_user).perform + expect(result[:contacts].length).to be 2 + expect(result[:contacts].first.label_list).to include('support') + expect(result[:contacts].last.label_list).to include('support') + end + + it 'returns is_not_present filter results properly' do + params[:payload] = [ + { + attribute_key: 'labels', + filter_operator: 'is_not_present', + values: [], + query_operator: nil + }.with_indifferent_access + ] + + result = filter_service.new(params, first_user).perform + expect(result[:contacts].length).to be 1 + expect(result[:contacts].first.id).to eq el_contact.id + end + end + + context 'with standard attributes - last_activity_at' do + before do + Time.zone = 'UTC' + el_contact.update(last_activity_at: (Time.zone.today - 4.days)) + cs_contact.update(last_activity_at: (Time.zone.today - 5.days)) + en_contact.update(last_activity_at: (Time.zone.today - 2.days)) + end + + it 'filter by last_activity_at 3_days_before and custom_attributes' do + params[:payload] = [ + { + attribute_key: 'last_activity_at', + filter_operator: 'days_before', + values: [3], + query_operator: 'AND' + }.with_indifferent_access, + { + attribute_key: 'contact_additional_information', + filter_operator: 'equal_to', + values: ['test custom data'], + query_operator: nil + }.with_indifferent_access + ] + + expected_count = Contact.where( + "last_activity_at < ? AND + custom_attributes->>'contact_additional_information' = ?", + (Time.zone.today - 3.days), + 'test custom data' + ).count + + result = filter_service.new(params, first_user).perform + expect(result[:contacts].length).to be expected_count + expect(result[:contacts].first.id).to eq(el_contact.id) + end + + it 'filter by last_activity_at 2_days_before and custom_attributes' do + params[:payload] = [ + { + attribute_key: 'last_activity_at', + filter_operator: 'days_before', + values: [2], + query_operator: nil + }.with_indifferent_access + ] + + expected_count = Contact.where('last_activity_at < ?', (Time.zone.today - 2.days)).count + + result = filter_service.new(params, first_user).perform + expect(result[:contacts].length).to be expected_count + expect(result[:contacts].pluck(:id)).to include(el_contact.id) + expect(result[:contacts].pluck(:id)).to include(cs_contact.id) + expect(result[:contacts].pluck(:id)).not_to include(en_contact.id) end + end + context 'with additional attributes' do + let(:payload) do + [ + { + attribute_key: 'country_code', + filter_operator: 'equal_to', + values: ['uk'], + query_operator: nil + }.with_indifferent_access + ] + end + + it 'filter contacts by additional_attributes' do + params[:payload] = payload + result = filter_service.new(params, first_user).perform + expect(result[:count]).to be 1 + expect(result[:contacts].first.id).to eq(en_contact.id) + end + end + + context 'with custom attributes' do it 'filter by custom_attributes and labels' do params[:payload] = [ { @@ -220,62 +300,6 @@ expect(result[:contacts].length).to be expected_count expect(result[:contacts].pluck(:id)).to include(el_contact.id) end - - context 'with x_days_before filter' do - before do - Time.zone = 'UTC' - el_contact.update(last_activity_at: (Time.zone.today - 4.days)) - cs_contact.update(last_activity_at: (Time.zone.today - 5.days)) - en_contact.update(last_activity_at: (Time.zone.today - 2.days)) - end - - it 'filter by last_activity_at 3_days_before and custom_attributes' do - params[:payload] = [ - { - attribute_key: 'last_activity_at', - filter_operator: 'days_before', - values: [3], - query_operator: 'AND' - }.with_indifferent_access, - { - attribute_key: 'contact_additional_information', - filter_operator: 'equal_to', - values: ['test custom data'], - query_operator: nil - }.with_indifferent_access - ] - - expected_count = Contact.where( - "last_activity_at < ? AND - custom_attributes->>'contact_additional_information' = ?", - (Time.zone.today - 3.days), - 'test custom data' - ).count - - result = filter_service.new(params, first_user).perform - expect(result[:contacts].length).to be expected_count - expect(result[:contacts].first.id).to eq(el_contact.id) - end - - it 'filter by last_activity_at 2_days_before and custom_attributes' do - params[:payload] = [ - { - attribute_key: 'last_activity_at', - filter_operator: 'days_before', - values: [2], - query_operator: nil - }.with_indifferent_access - ] - - expected_count = Contact.where('last_activity_at < ?', (Time.zone.today - 2.days)).count - - result = filter_service.new(params, first_user).perform - expect(result[:contacts].length).to be expected_count - expect(result[:contacts].pluck(:id)).to include(el_contact.id) - expect(result[:contacts].pluck(:id)).to include(cs_contact.id) - expect(result[:contacts].pluck(:id)).not_to include(en_contact.id) - end - end end end end