chore: Ability to filter conversations with priority (#10967)

- Ability to filter conversation with priority

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Sojan Jose
2025-03-03 03:08:22 -08:00
committed by GitHub
parent 488c334b72
commit 6040e50265
9 changed files with 258 additions and 75 deletions

View File

@@ -1,4 +1,4 @@
module FilterHelper module Filters::FilterHelper
def build_condition_query(model_filters, query_hash, current_index) def build_condition_query(model_filters, query_hash, current_index)
current_filter = model_filters[query_hash['attribute_key']] current_filter = model_filters[query_hash['attribute_key']]
@@ -89,4 +89,18 @@ module FilterHelper
operator = condition['query_operator'].upcase operator = condition['query_operator'].upcase
raise CustomExceptions::CustomFilter::InvalidQueryOperator.new({}) unless %w[AND OR].include?(operator) raise CustomExceptions::CustomFilter::InvalidQueryOperator.new({}) unless %w[AND OR].include?(operator)
end end
def conversation_status_values(values)
return Conversation.statuses.values if values.include?('all')
values.map { |x| Conversation.statuses[x.to_sym] }
end
def conversation_priority_values(values)
values.map { |x| Conversation.priorities[x.to_sym] }
end
def message_type_values(values)
values.map { |x| Message.message_types[x.to_sym] }
end
end end

View File

@@ -2,7 +2,10 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useOperators } from './operators'; import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js'; import { useMapGetter } from 'dashboard/composables/store.js';
import { buildAttributesFilterTypes } from './helper/filterHelper.js'; import {
buildAttributesFilterTypes,
CONTACT_ATTRIBUTES,
} from './helper/filterHelper.js';
import countries from 'shared/constants/countries.js'; import countries from 'shared/constants/countries.js';
/** /**
@@ -59,7 +62,11 @@ export function useContactFilterContext() {
* @type {import('vue').ComputedRef<FilterType[]>} * @type {import('vue').ComputedRef<FilterType[]>}
*/ */
const customFilterTypes = computed(() => const customFilterTypes = computed(() =>
buildAttributesFilterTypes(contactAttributes.value, getOperatorTypes) buildAttributesFilterTypes(
contactAttributes.value,
getOperatorTypes,
'contact'
)
); );
/** /**
@@ -67,8 +74,8 @@ export function useContactFilterContext() {
*/ */
const filterTypes = computed(() => [ const filterTypes = computed(() => [
{ {
attributeKey: 'name', attributeKey: CONTACT_ATTRIBUTES.NAME,
value: 'name', value: CONTACT_ATTRIBUTES.NAME,
attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'), attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'),
label: t('CONTACTS_LAYOUT.FILTER.NAME'), label: t('CONTACTS_LAYOUT.FILTER.NAME'),
inputType: 'plainText', inputType: 'plainText',
@@ -77,8 +84,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'email', attributeKey: CONTACT_ATTRIBUTES.EMAIL,
value: 'email', value: CONTACT_ATTRIBUTES.EMAIL,
attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'), attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
label: t('CONTACTS_LAYOUT.FILTER.EMAIL'), label: t('CONTACTS_LAYOUT.FILTER.EMAIL'),
inputType: 'plainText', inputType: 'plainText',
@@ -87,8 +94,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'phone_number', attributeKey: CONTACT_ATTRIBUTES.PHONE_NUMBER,
value: 'phone_number', value: CONTACT_ATTRIBUTES.PHONE_NUMBER,
attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'), attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'), label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'),
inputType: 'plainText', inputType: 'plainText',
@@ -97,8 +104,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'identifier', attributeKey: CONTACT_ATTRIBUTES.IDENTIFIER,
value: 'identifier', value: CONTACT_ATTRIBUTES.IDENTIFIER,
attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'), attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'), label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'),
inputType: 'plainText', inputType: 'plainText',
@@ -107,8 +114,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'country_code', attributeKey: CONTACT_ATTRIBUTES.COUNTRY_CODE,
value: 'country_code', value: CONTACT_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -118,8 +125,8 @@ export function useContactFilterContext() {
attributeModel: 'additional', attributeModel: 'additional',
}, },
{ {
attributeKey: 'city', attributeKey: CONTACT_ATTRIBUTES.CITY,
value: 'city', value: CONTACT_ATTRIBUTES.CITY,
attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'), attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'),
label: t('CONTACTS_LAYOUT.FILTER.CITY'), label: t('CONTACTS_LAYOUT.FILTER.CITY'),
inputType: 'plainText', inputType: 'plainText',
@@ -128,8 +135,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'created_at', attributeKey: CONTACT_ATTRIBUTES.CREATED_AT,
value: 'created_at', value: CONTACT_ATTRIBUTES.CREATED_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'), attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'), label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'),
inputType: 'date', inputType: 'date',
@@ -138,8 +145,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'last_activity_at', attributeKey: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
value: 'last_activity_at', value: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'), attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'), label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'),
inputType: 'date', inputType: 'date',
@@ -148,8 +155,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'referer', attributeKey: CONTACT_ATTRIBUTES.REFERER,
value: 'referer', value: CONTACT_ATTRIBUTES.REFERER,
attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'), attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'), label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'),
inputType: 'plainText', inputType: 'plainText',
@@ -158,8 +165,8 @@ export function useContactFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'blocked', attributeKey: CONTACT_ATTRIBUTES.BLOCKED,
value: 'blocked', value: CONTACT_ATTRIBUTES.BLOCKED,
attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'), attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'), label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'),
inputType: 'searchSelect', inputType: 'searchSelect',

View File

@@ -1,3 +1,35 @@
/**
* Standard attributes of the conversation model
*/
export const CONVERSATION_ATTRIBUTES = {
STATUS: 'status',
PRIORITY: 'priority',
ASSIGNEE_ID: 'assignee_id',
INBOX_ID: 'inbox_id',
TEAM_ID: 'team_id',
DISPLAY_ID: 'display_id',
CAMPAIGN_ID: 'campaign_id',
LABELS: 'labels',
BROWSER_LANGUAGE: 'browser_language',
COUNTRY_CODE: 'country_code',
REFERER: 'referer',
CREATED_AT: 'created_at',
LAST_ACTIVITY_AT: 'last_activity_at',
};
export const CONTACT_ATTRIBUTES = {
NAME: 'name',
EMAIL: 'email',
PHONE_NUMBER: 'phone_number',
IDENTIFIER: 'identifier',
COUNTRY_CODE: 'country_code',
CITY: 'city',
CREATED_AT: 'created_at',
LAST_ACTIVITY_AT: 'last_activity_at',
REFERER: 'referer',
BLOCKED: 'blocked',
};
/** /**
* Determines the input type for a custom attribute based on its key * Determines the input type for a custom attribute based on its key
* @param {string} key - The attribute display type key * @param {string} key - The attribute display type key
@@ -20,12 +52,25 @@ export const getCustomAttributeInputType = key => {
/** /**
* Builds filter types for custom attributes * Builds filter types for custom attributes
* This also removes any conflicting attributes
* @param {Array} attributes - The attributes array * @param {Array} attributes - The attributes array
* @param {Function} getOperatorTypes - Function to get operator types * @param {Function} getOperatorTypes - Function to get operator types
* @returns {Array} Array of filter types * @returns {Array} Array of filter types
*/ */
export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => { export const buildAttributesFilterTypes = (
return attributes.map(attr => ({ attributes,
getOperatorTypes,
filterModel = 'conversation'
) => {
const standardAttributes = Object.values(
filterModel === 'conversation'
? CONVERSATION_ATTRIBUTES
: CONTACT_ATTRIBUTES
);
return attributes
.filter(attr => !standardAttributes.includes(attr.attributeKey))
.map(attr => ({
attributeKey: attr.attributeKey, attributeKey: attr.attributeKey,
value: attr.attributeKey, value: attr.attributeKey,
attributeName: attr.attributeDisplayName, attributeName: attr.attributeDisplayName,

View File

@@ -3,7 +3,10 @@ import { useI18n } from 'vue-i18n';
import { useOperators } from './operators'; import { useOperators } from './operators';
import { useMapGetter } from 'dashboard/composables/store.js'; import { useMapGetter } from 'dashboard/composables/store.js';
import { useChannelIcon } from 'next/icon/provider'; import { useChannelIcon } from 'next/icon/provider';
import { buildAttributesFilterTypes } from './helper/filterHelper'; import {
buildAttributesFilterTypes,
CONVERSATION_ATTRIBUTES,
} from './helper/filterHelper';
import countries from 'shared/constants/countries.js'; import countries from 'shared/constants/countries.js';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
@@ -70,7 +73,11 @@ export function useConversationFilterContext() {
* @type {import('vue').ComputedRef<FilterType[]>} * @type {import('vue').ComputedRef<FilterType[]>}
*/ */
const customFilterTypes = computed(() => const customFilterTypes = computed(() =>
buildAttributesFilterTypes(conversationAttributes.value, getOperatorTypes) buildAttributesFilterTypes(
conversationAttributes.value,
getOperatorTypes,
'conversation'
)
); );
/** /**
@@ -78,8 +85,8 @@ export function useConversationFilterContext() {
*/ */
const filterTypes = computed(() => [ const filterTypes = computed(() => [
{ {
attributeKey: 'status', attributeKey: CONVERSATION_ATTRIBUTES.STATUS,
value: 'status', value: CONVERSATION_ATTRIBUTES.STATUS,
attributeName: t('FILTER.ATTRIBUTES.STATUS'), attributeName: t('FILTER.ATTRIBUTES.STATUS'),
label: t('FILTER.ATTRIBUTES.STATUS'), label: t('FILTER.ATTRIBUTES.STATUS'),
inputType: 'multiSelect', inputType: 'multiSelect',
@@ -94,8 +101,24 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'assignee_id', attributeKey: CONVERSATION_ATTRIBUTES.PRIORITY,
value: 'assignee_id', value: CONVERSATION_ATTRIBUTES.PRIORITY,
attributeName: t('FILTER.ATTRIBUTES.PRIORITY'),
label: t('FILTER.ATTRIBUTES.PRIORITY'),
inputType: 'multiSelect',
options: ['low', 'medium', 'high', 'urgent'].map(id => {
return {
id,
name: t(`CONVERSATION.PRIORITY.OPTIONS.${id.toUpperCase()}`),
};
}),
dataType: 'text',
filterOperators: equalityOperators.value,
attributeModel: 'standard',
},
{
attributeKey: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
value: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID,
attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'), attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'), label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -110,8 +133,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'inbox_id', attributeKey: CONVERSATION_ATTRIBUTES.INBOX_ID,
value: 'inbox_id', value: CONVERSATION_ATTRIBUTES.INBOX_ID,
attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'), attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'),
label: t('FILTER.ATTRIBUTES.INBOX_NAME'), label: t('FILTER.ATTRIBUTES.INBOX_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -126,8 +149,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'team_id', attributeKey: CONVERSATION_ATTRIBUTES.TEAM_ID,
value: 'team_id', value: CONVERSATION_ATTRIBUTES.TEAM_ID,
attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'), attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'),
label: t('FILTER.ATTRIBUTES.TEAM_NAME'), label: t('FILTER.ATTRIBUTES.TEAM_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -137,8 +160,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'display_id', attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
value: 'display_id', value: CONVERSATION_ATTRIBUTES.DISPLAY_ID,
attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'), attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'), label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'),
inputType: 'plainText', inputType: 'plainText',
@@ -147,8 +170,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'campaign_id', attributeKey: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
value: 'campaign_id', value: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID,
attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'), attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'), label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -161,8 +184,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'labels', attributeKey: CONVERSATION_ATTRIBUTES.LABELS,
value: 'labels', value: CONVERSATION_ATTRIBUTES.LABELS,
attributeName: t('FILTER.ATTRIBUTES.LABELS'), attributeName: t('FILTER.ATTRIBUTES.LABELS'),
label: t('FILTER.ATTRIBUTES.LABELS'), label: t('FILTER.ATTRIBUTES.LABELS'),
inputType: 'multiSelect', inputType: 'multiSelect',
@@ -185,8 +208,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'browser_language', attributeKey: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
value: 'browser_language', value: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE,
attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'), attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'), label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -196,8 +219,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional', attributeModel: 'additional',
}, },
{ {
attributeKey: 'country_code', attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
value: 'country_code', value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE,
attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'),
inputType: 'searchSelect', inputType: 'searchSelect',
@@ -207,8 +230,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional', attributeModel: 'additional',
}, },
{ {
attributeKey: 'referer', attributeKey: CONVERSATION_ATTRIBUTES.REFERER,
value: 'referer', value: CONVERSATION_ATTRIBUTES.REFERER,
attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'), attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'),
label: t('FILTER.ATTRIBUTES.REFERER_LINK'), label: t('FILTER.ATTRIBUTES.REFERER_LINK'),
inputType: 'plainText', inputType: 'plainText',
@@ -217,8 +240,8 @@ export function useConversationFilterContext() {
attributeModel: 'additional', attributeModel: 'additional',
}, },
{ {
attributeKey: 'created_at', attributeKey: CONVERSATION_ATTRIBUTES.CREATED_AT,
value: 'created_at', value: CONVERSATION_ATTRIBUTES.CREATED_AT,
attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'), attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'),
label: t('FILTER.ATTRIBUTES.CREATED_AT'), label: t('FILTER.ATTRIBUTES.CREATED_AT'),
inputType: 'date', inputType: 'date',
@@ -227,8 +250,8 @@ export function useConversationFilterContext() {
attributeModel: 'standard', attributeModel: 'standard',
}, },
{ {
attributeKey: 'last_activity_at', attributeKey: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
value: 'last_activity_at', value: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT,
attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'), attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'), label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'),
inputType: 'date', inputType: 'date',

View File

@@ -22,6 +22,12 @@
# index_custom_attribute_definitions_on_account_id (account_id) # index_custom_attribute_definitions_on_account_id (account_id)
# #
class CustomAttributeDefinition < ApplicationRecord class CustomAttributeDefinition < ApplicationRecord
STANDARD_ATTRIBUTES = {
:conversation => %w[status priority assignee_id inbox_id team_id display_id campaign_id labels browser_language country_code referer created_at
last_activity_at],
:contact => %w[name email phone_number identifier country_code city created_at last_activity_at referer blocked]
}.freeze
scope :with_attribute_model, ->(attribute_model) { attribute_model.presence && where(attribute_model: attribute_model) } scope :with_attribute_model, ->(attribute_model) { attribute_model.presence && where(attribute_model: attribute_model) }
validates :attribute_display_name, presence: true validates :attribute_display_name, presence: true
@@ -31,6 +37,7 @@ class CustomAttributeDefinition < ApplicationRecord
validates :attribute_display_type, presence: true validates :attribute_display_type, presence: true
validates :attribute_model, presence: true validates :attribute_model, presence: true
validate :attribute_must_not_conflict, on: :create
enum attribute_model: { conversation_attribute: 0, contact_attribute: 1 } enum attribute_model: { conversation_attribute: 0, contact_attribute: 1 }
enum attribute_display_type: { text: 0, number: 1, currency: 2, percent: 3, link: 4, date: 5, list: 6, checkbox: 7 } enum attribute_display_type: { text: 0, number: 1, currency: 2, percent: 3, link: 4, date: 5, list: 6, checkbox: 7 }
@@ -48,4 +55,11 @@ class CustomAttributeDefinition < ApplicationRecord
def update_widget_pre_chat_custom_fields def update_widget_pre_chat_custom_fields
::Inboxes::UpdateWidgetPreChatCustomFieldsJob.perform_later(account, self) ::Inboxes::UpdateWidgetPreChatCustomFieldsJob.perform_later(account, self)
end end
def attribute_must_not_conflict
model_keys = attribute_model.to_sym == :conversation_attribute ? :conversation : :contact
return unless attribute_key.in?(STANDARD_ATTRIBUTES[model_keys])
errors.add(:attribute_key, I18n.t('errors.custom_attribute_definition.key_conflict'))
end
end end

View File

@@ -1,7 +1,7 @@
require 'json' require 'json'
class FilterService class FilterService
include FilterHelper include Filters::FilterHelper
include CustomExceptions::CustomFilter include CustomExceptions::CustomFilter
ATTRIBUTE_MODEL = 'conversation_attribute'.freeze ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
@@ -43,19 +43,16 @@ class FilterService
end end
def filter_values(query_hash) def filter_values(query_hash)
case query_hash['attribute_key'] attribute_key = query_hash['attribute_key']
when 'status' values = query_hash['values']
return Conversation.statuses.values if query_hash['values'].include?('all')
return conversation_status_values(values) if attribute_key == 'status'
return conversation_priority_values(values) if attribute_key == 'priority'
return message_type_values(values) if attribute_key == 'message_type'
return downcase_array_values(values) if attribute_key == 'content'
query_hash['values'].map { |x| Conversation.statuses[x.to_sym] }
when 'message_type'
query_hash['values'].map { |x| Message.message_types[x.to_sym] }
when 'content'
downcase_array_values(query_hash['values'])
else
case_insensitive_values(query_hash) case_insensitive_values(query_hash)
end end
end
def downcase_array_values(values) def downcase_array_values(values)
values.map(&:downcase) values.map(&:downcase)

View File

@@ -82,6 +82,8 @@ en:
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}]. invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
invalid_query_operator: Query operator must be either "AND" or "OR". invalid_query_operator: Query operator must be either "AND" or "OR".
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
custom_attribute_definition:
key_conflict: The provided key is not allowed as it might conflict with default attributes.
reports: reports:
period: Reporting period %{since} to %{until} period: Reporting period %{since} to %{until}
utc_warning: The report generated is in UTC timezone utc_warning: The report generated is in UTC timezone

View File

@@ -89,6 +89,30 @@ RSpec.describe 'Custom Attribute Definitions API', type: :request do
json_response = response.parsed_body json_response = response.parsed_body
expect(json_response['attribute_key']).to eq 'developer_id' expect(json_response['attribute_key']).to eq 'developer_id'
end end
context 'when creating with a conflicting attribute_key' do
let(:standard_key) { CustomAttributeDefinition::STANDARD_ATTRIBUTES[:conversation].first }
let(:conflicting_payload) do
{
custom_attribute_definition: {
attribute_display_name: 'Conflicting Key',
attribute_key: standard_key,
attribute_model: 'conversation_attribute',
attribute_display_type: 'text'
}
}
end
it 'returns error for conflicting key' do
post "/api/v1/accounts/#{account.id}/custom_attribute_definitions",
headers: user.create_new_auth_token,
params: conflicting_payload
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to include('The provided key is not allowed as it might conflict with default attributes.')
end
end
end end
end end

View File

@@ -77,6 +77,63 @@ describe Conversations::FilterService do
expect(result[:count][:all_count]).to be conversations.count expect(result[:count][:all_count]).to be conversations.count
end end
it 'filter conversations by priority' do
conversation = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :high)
params[:payload] = [
{
attribute_key: 'priority',
filter_operator: 'equal_to',
values: ['high'],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1).perform
expect(result[:conversations].length).to eq 1
expect(result[:conversations][0][:id]).to eq conversation.id
end
it 'filter conversations by multiple priority values' do
high_priority = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :high)
urgent_priority = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :urgent)
create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :low)
params[:payload] = [
{
attribute_key: 'priority',
filter_operator: 'equal_to',
values: %w[high urgent],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1).perform
expect(result[:conversations].length).to eq 2
expect(result[:conversations].pluck(:id)).to include(high_priority.id, urgent_priority.id)
end
it 'filter conversations with not_equal_to priority operator' do
create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :high)
create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :urgent)
low_priority = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :low)
medium_priority = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :medium)
params[:payload] = [
{
attribute_key: 'priority',
filter_operator: 'not_equal_to',
values: %w[high urgent],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1).perform
# Only include conversations with medium and low priority, excluding high and urgent
expect(result[:conversations].length).to eq 2
expect(result[:conversations].pluck(:id)).to include(low_priority.id, medium_priority.id)
end
it 'filter conversations by additional_attributes and status with pagination' do it 'filter conversations by additional_attributes and status with pagination' do
params[:payload] = payload params[:payload] = payload
params[:page] = 2 params[:page] = 2