mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-20 13:05:16 +00:00
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:
@@ -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
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user