feat: Ability filter blocked contacts (#9048)

- This PR introduces the ability to filter blocked contacts from the contacts filter UI
This commit is contained in:
Sojan Jose
2024-03-20 18:11:50 +05:30
committed by GitHub
parent f78f278e2f
commit 1303469087
6 changed files with 204 additions and 163 deletions

View File

@@ -34,7 +34,7 @@ module FilterHelper
case current_filter['attribute_type'] case current_filter['attribute_type']
when 'additional_attributes' when 'additional_attributes'
handle_additional_attributes(query_hash, filter_operator_value) handle_additional_attributes(query_hash, filter_operator_value, current_filter['data_type'])
else else
handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value) handle_standard_attributes(current_filter, query_hash, current_index, filter_operator_value)
end end
@@ -45,10 +45,8 @@ module FilterHelper
custom_attribute_query(query_hash, attribute_type, current_index) custom_attribute_query(query_hash, attribute_type, current_index)
end end
# TODO: Change the reliance on entity instead introduce datatype text_case_insensive def handle_additional_attributes(query_hash, filter_operator_value, data_type)
# Then we can remove the condition for Contact if data_type == 'text_case_insensitive'
def handle_additional_attributes(query_hash, filter_operator_value)
if filter_config[:entity] == 'Contact'
"LOWER(#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}') " \ "LOWER(#{filter_config[:table_name]}.additional_attributes ->> '#{query_hash[:attribute_key]}') " \
"#{filter_operator_value} #{query_hash[:query_operator]}" "#{filter_operator_value} #{query_hash[:query_operator]}"
else else
@@ -63,6 +61,8 @@ module FilterHelper
date_filter(current_filter, query_hash, filter_operator_value) date_filter(current_filter, query_hash, filter_operator_value)
when 'labels' when 'labels'
tag_filter_query(query_hash, current_index) tag_filter_query(query_hash, current_index)
when 'text_case_insensitive'
text_case_insensitive_filter(query_hash, filter_operator_value)
else else
default_filter(query_hash, filter_operator_value) default_filter(query_hash, filter_operator_value)
end end
@@ -73,14 +73,12 @@ module FilterHelper
"#{filter_operator_value}#{current_filter['data_type']} #{query_hash[:query_operator]}" "#{filter_operator_value}#{current_filter['data_type']} #{query_hash[:query_operator]}"
end end
# TODO: Change the reliance on entity instead introduce datatype text_case_insensive def text_case_insensitive_filter(query_hash, filter_operator_value)
# Then we can remove the condition for Contact "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) def default_filter(query_hash, filter_operator_value)
if filter_config[:entity] == 'Contact' "#{filter_config[:table_name]}.#{query_hash[:attribute_key]} #{filter_operator_value} #{query_hash[:query_operator]}"
"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
end end
end end

View File

@@ -44,7 +44,8 @@
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"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"
}, },
"GROUPS": { "GROUPS": {
"STANDARD_FILTERS": "Standard Filters", "STANDARD_FILTERS": "Standard Filters",

View File

@@ -243,7 +243,7 @@ export default {
attr.attribute_display_type === 'checkbox' attr.attribute_display_type === 'checkbox'
); );
}); });
if (isCustomAttributeCheckbox) { if (isCustomAttributeCheckbox || type === 'blocked') {
return [ return [
{ {
id: true, id: true,

View File

@@ -76,6 +76,14 @@ const filterTypes = [
filterOperators: OPERATOR_TYPES_5, filterOperators: OPERATOR_TYPES_5,
attributeModel: 'standard', attributeModel: 'standard',
}, },
{
attributeKey: 'blocked',
attributeI18nKey: 'BLOCKED',
inputType: 'search_select',
dataType: 'text',
filterOperators: OPERATOR_TYPES_1,
attributeModel: 'standard',
},
]; ];
export const filterAttributeGroups = [ export const filterAttributeGroups = [
@@ -115,6 +123,10 @@ export const filterAttributeGroups = [
key: 'last_activity_at', key: 'last_activity_at',
i18nKey: 'LAST_ACTIVITY', i18nKey: 'LAST_ACTIVITY',
}, },
{
key: 'blocked',
i18nKey: 'BLOCKED',
},
], ],
}, },
]; ];

View File

@@ -8,7 +8,7 @@
# - Parent Key (conversation, contact, messages) # - Parent Key (conversation, contact, messages)
# - Key (attribute_name) # - Key (attribute_name)
# - attribute_type: "standard" : supported ["standard", "additional_attributes (only for conversations and messages)"] # - 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"] # - 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 ----- ### ### ----- Conversation Filters ----- ###
@@ -124,7 +124,7 @@ conversations:
contacts: contacts:
name: name:
attribute_type: "standard" attribute_type: "standard"
data_type: "text" data_type: "text_case_insensitive"
filter_operators: filter_operators:
- "equal_to" - "equal_to"
- "not_equal_to" - "not_equal_to"
@@ -132,7 +132,7 @@ contacts:
- "does_not_contain" - "does_not_contain"
phone_number: phone_number:
attribute_type: "standard" attribute_type: "standard"
data_type: "text" data_type: "text_case_insensitive"
filter_operators: filter_operators:
- "equal_to" - "equal_to"
- "not_equal_to" - "not_equal_to"
@@ -141,7 +141,7 @@ contacts:
- "starts_with" - "starts_with"
email: email:
attribute_type: "standard" attribute_type: "standard"
data_type: "text" data_type: "text_case_insensitive"
filter_operators: filter_operators:
- "equal_to" - "equal_to"
- "not_equal_to" - "not_equal_to"
@@ -149,19 +149,19 @@ contacts:
- "does_not_contain" - "does_not_contain"
identifier: identifier:
attribute_type: "standard" attribute_type: "standard"
data_type: "text" data_type: "text_case_insensitive"
filter_operators: filter_operators:
- "equal_to" - "equal_to"
- "not_equal_to" - "not_equal_to"
country_code: country_code:
attribute_type: "additional_attributes" attribute_type: "additional_attributes"
data_type: "text" data_type: "text_case_insensitive"
filter_operators: filter_operators:
- "equal_to" - "equal_to"
- "not_equal_to" - "not_equal_to"
city: city:
attribute_type: "additional_attributes" attribute_type: "additional_attributes"
data_type: "text" data_type: "text_case_insensitive"
filter_operators: filter_operators:
- "equal_to" - "equal_to"
- "not_equal_to" - "not_equal_to"
@@ -169,7 +169,7 @@ contacts:
- "does_not_contain" - "does_not_contain"
company: company:
attribute_type: "additional_attributes" attribute_type: "additional_attributes"
data_type: "text" data_type: "text_case_insensitive"
filter_operators: filter_operators:
- "equal_to" - "equal_to"
- "not_equal_to" - "not_equal_to"
@@ -197,6 +197,12 @@ contacts:
- "is_greater_than" - "is_greater_than"
- "is_less_than" - "is_less_than"
- "days_before" - "days_before"
blocked:
attribute_type: "standard"
data_type: "boolean"
filter_operators:
- "equal_to"
- "not_equal_to"
### ----- End of Contact Filters ----- ### ### ----- End of Contact Filters ----- ###

View File

@@ -37,6 +37,8 @@ describe Contacts::FilterService do
end end
describe '#perform' do describe '#perform' do
let!(:params) { { payload: [], page: 1 } }
before do before do
en_contact.update_labels(%w[random_label support]) en_contact.update_labels(%w[random_label support])
cs_contact.update_labels('support') cs_contact.update_labels('support')
@@ -46,90 +48,7 @@ describe Contacts::FilterService do
cs_contact.update!(custom_attributes: { customer_type: 'platinum', signed_in_at: '2022-01-19' }) cs_contact.update!(custom_attributes: { customer_type: 'platinum', signed_in_at: '2022-01-19' })
end end
context 'with query present' do context 'with standard attributes - name' do
let!(:params) { { payload: [], page: 1 } }
let(:payload) do
[
{
attribute_key: 'country_code',
filter_operator: 'equal_to',
values: ['uk'],
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
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
it 'filter contacts by name' do it 'filter contacts by name' do
params[:payload] = [ params[:payload] = [
{ {
@@ -145,7 +64,168 @@ describe Contacts::FilterService do
expect(result[:contacts].length).to be 1 expect(result[:contacts].length).to be 1
expect(result[:contacts].first.name).to eq(en_contact.name) expect(result[:contacts].first.name).to eq(en_contact.name)
end end
end
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(blocked_contact.id)
end
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: '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
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 it 'filter by custom_attributes and labels' do
params[:payload] = [ params[:payload] = [
{ {
@@ -220,62 +300,6 @@ describe Contacts::FilterService do
expect(result[:contacts].length).to be expected_count 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(el_contact.id)
end 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 end
end end