Compare commits

...

34 commits

Author SHA1 Message Date
Fayaz Ahmed
62ed94efde Changed the payload keys
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2021-10-29 19:20:17 +05:30
Fayaz Ahmed
10d83b0c92 If applied, Adds payload key and wraps the data in an object 2021-10-29 19:20:17 +05:30
Fayaz Ahmed
697fa2d301 Removes default hardcoded values for props 2021-10-29 19:19:13 +05:30
Fayaz Ahmed
31e0c0742c Pass the correct data to helper method in test spec 2021-10-29 19:17:27 +05:30
Fayaz Ahmed
792ee4dd53 Test case for filter payload generator 2021-10-29 19:16:44 +05:30
Fayaz Ahmed
2e716b9375 Helper to generate new query structure 2021-10-29 19:16:44 +05:30
Fayaz Ahmed
638ac99cdb If applied, always wraps filters values in array 2021-10-29 19:16:44 +05:30
Fayaz Ahmed
daf147180c If applied, excludes codeclimate for filter types file. 2021-10-29 19:16:44 +05:30
Fayaz Ahmed
b06aaf5dab If applied, fixes padding ui on query operator 2021-10-29 19:16:27 +05:30
Fayaz Ahmed
9549bc3da1 If applied, removed unnecessary modal wrap 2021-10-29 19:16:21 +05:30
Fayaz Ahmed
41d4a2b4ce If applied, adds the filter action for conversation filters 2021-10-29 19:15:59 +05:30
Fayaz Ahmed
1e27c85a4a If applied, adds browser_language and country filters 2021-10-29 19:15:26 +05:30
Fayaz Ahmed
786087c1ec i18n compatible labels in dropdowns 2021-10-29 19:12:30 +05:30
Fayaz Ahmed
1f5489817c Revert accidental schema file changes 2021-10-29 19:12:30 +05:30
Fayaz Ahmed
2fe4efa3d2 If applied, adds i18n to form labels 2021-10-29 19:11:11 +05:30
Fayaz Ahmed
c6cde3c024 Ifa applied, adds v-if on parent divs instead of children 2021-10-29 19:11:11 +05:30
Fayaz Ahmed
c5b0db59a9 If applied, adds a getter to return all campaigns 2021-10-29 19:11:11 +05:30
Fayaz Ahmed
0fa24d344b If applied, replaces hardcoded css values with variables 2021-10-29 19:11:08 +05:30
Fayaz Ahmed
4fb6e54df9 If applied, applied i18n for labels and titles 2021-10-29 19:10:23 +05:30
Fayaz Ahmed
151318334e If applied, adds validations for empty filter values 2021-10-29 19:10:19 +05:30
Fayaz Ahmed
5668ad94e5 Updated getter keys to match the attribute key 2021-10-29 19:09:32 +05:30
Fayaz Ahmed
e7f70e6a16 If applied, fixes the dynamic v-model operation on filter-inputs 2021-10-29 19:08:17 +05:30
Fayaz Ahmed
3b9deb2434 Refactor to make the filter component resuable. 2021-10-29 19:07:36 +05:30
Fayaz Ahmed
449245c087 If applied, adds advanced filters modal feature 2021-10-29 19:06:14 +05:30
Sojan Jose
d653902053
Merge branch 'develop' into feat/3187-filter-with-received-payload 2021-10-29 18:17:09 +05:30
Tejaswini
b9ce597382 Rubocop and spec fixes 2021-10-28 18:16:41 +05:30
Tejaswini
d74ad49872 PR comments: contact related queries 2021-10-28 16:10:48 +05:30
Tejaswini
6122593a6f pagination added 2021-10-28 15:19:05 +05:30
Tejaswini
ab5bddac1d Refer current account conversations 2021-10-26 21:40:18 +05:30
Tejaswini
810ebd7866 Update according to UI values, id-name pairs 2021-10-26 18:47:34 +05:30
Tejaswini
01f1d5216f set count of the conversations and updated specs 2021-10-26 18:44:58 +05:30
Tejaswini
e2cb2c34fa [3187] Filter spec updates 2021-10-26 18:44:57 +05:30
Tejaswini
f2de6f1435 Feat: Conversation query builder 2021-10-26 18:44:57 +05:30
Tejaswini
da9110c7a8 feat: Filter the contacts based on payload 2021-10-26 18:44:57 +05:30
28 changed files with 1841 additions and 32 deletions

View file

@ -36,3 +36,4 @@ exclude_patterns:
- "app/javascript/dashboard/i18n/locale"
- "**/*.stories.js"
- "stories/"
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js"

View file

@ -51,7 +51,10 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def show; end
def filter
@contacts = Current.account.contacts.limit(10)
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
contacts = result[:contacts]
@contacts_count = result[:count]
@contacts = fetch_contacts_with_conversation_count(contacts)
end
def contactable_inboxes

View file

@ -32,7 +32,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def show; end
def filter
@conversations = Current.account.conversations.limit(10)
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
@conversations = result[:conversations]
@conversations_count = result[:count]
end
def mute

View file

@ -1,4 +1,5 @@
/* global axios */
import actions from '../../store/modules/conversations/actions';
import ApiClient from '../ApiClient';
class ConversationApi extends ApiClient {
@ -19,6 +20,10 @@ class ConversationApi extends ApiClient {
});
}
filter(payload) {
return axios.post(`${this.url}/filter`, payload);
}
search({ q }) {
return axios.get(`${this.url}/search`, {
params: {

View file

@ -29,7 +29,6 @@
}
}
.page-top-bar {
@include padding($space-large $space-large $zero);
@ -48,13 +47,11 @@
position: relative;
width: 60rem;
.content-box {
@include padding($zero);
height: auto;
}
h2 {
color: $color-heading;
font-size: $font-size-medium;
@ -89,6 +86,11 @@
button {
font-size: $font-size-small;
}
&.justify-content-end {
justify-content: end;
width: 100%;
}
}
.delete-item {
@ -97,7 +99,6 @@
@include margin($zero);
}
}
}
.modal-enter,

View file

@ -5,10 +5,22 @@
<h1 class="page-title text-truncate" :title="pageTitle">
{{ pageTitle }}
</h1>
<chat-filter @statusFilterChange="updateStatusType" />
<div class="filter--actions">
<button
v-if="filtersApplied"
class="btn-clear-filters"
@click="resetAndFetchData"
>
Clear Filters
</button>
<button class="btn-filter" @click="onToggleAdvanceFiltersModal">
<i class="icon ion-ios-settings-strong" />
</button>
</div>
</div>
<chat-type-tabs
v-if="!filtersApplied"
:items="assigneeTabItems"
:active-tab="activeAssigneeTab"
class="tab--chat-type"
@ -53,19 +65,33 @@
{{ $t('CHAT_LIST.EOF') }}
</p>
</div>
<woot-modal
:show.sync="showAdvancedFilters"
:on-close="onToggleAdvanceFiltersModal"
>
<chat-advanced-filter
v-if="showAdvancedFilters"
:filter-types="advancedFilterTypes"
:on-close="onToggleAdvanceFiltersModal"
@applyFilter="fetchFilteredConversations"
/>
</woot-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import ChatFilter from './widgets/conversation/ChatFilter';
import ChatAdvancedFilter from './widgets/conversation/ChatAdvancedFilter';
import ChatTypeTabs from './widgets/ChatTypeTabs';
import ConversationCard from './widgets/conversation/ConversationCard';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from '../constants';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import {
hasPressedAltAndJKey,
hasPressedAltAndKKey,
@ -75,7 +101,7 @@ export default {
components: {
ChatTypeTabs,
ConversationCard,
ChatFilter,
ChatAdvancedFilter,
},
mixins: [timeMixin, conversationMixin, eventListenerMixins],
props: {
@ -96,6 +122,9 @@ export default {
return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
activeStatus: wootConstants.STATUS_TYPE.OPEN,
showAdvancedFilters: false,
advancedFilterTypes,
filtersApplied: false,
};
},
computed: {
@ -160,13 +189,17 @@ export default {
},
conversationList() {
let conversationList = [];
const filters = this.conversationFilters;
if (this.activeAssigneeTab === 'me') {
conversationList = [...this.mineChatsList(filters)];
} else if (this.activeAssigneeTab === 'unassigned') {
conversationList = [...this.unAssignedChatsList(filters)];
if (!this.filtersApplied) {
const filters = this.conversationFilters;
if (this.activeAssigneeTab === 'me') {
conversationList = [...this.mineChatsList(filters)];
} else if (this.activeAssigneeTab === 'unassigned') {
conversationList = [...this.unAssignedChatsList(filters)];
} else {
conversationList = [...this.allChatList(filters)];
}
} else {
conversationList = [...this.allChatList(filters)];
conversationList = [...this.chatLists];
}
return conversationList;
@ -198,6 +231,9 @@ export default {
});
},
methods: {
onToggleAdvanceFiltersModal() {
this.showAdvancedFilters = !this.showAdvancedFilters;
},
getKeyboardListenerParams() {
const allConversations = this.$refs.activeConversation.querySelectorAll(
'div.conversations-list div.conversation'
@ -251,6 +287,20 @@ export default {
this.$store
.dispatch('fetchAllConversations', this.conversationFilters)
.then(() => this.$emit('conversation-load'));
this.filtersApplied = false;
},
fetchFilteredConversations(payload) {
try {
this.filtersApplied = true;
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
this.$store
.dispatch('fetchFilteredConversations', filterQueryGenerator(payload))
.then(() => this.$emit('conversation-load'));
this.onToggleAdvanceFiltersModal();
} catch (err) {
console.log(err);
}
},
updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) {
@ -295,4 +345,22 @@ export default {
flex-basis: 46rem;
}
}
.filter--actions {
display: flex;
align-items: center;
.btn-filter {
margin: 0 var(--space-normal);
cursor: pointer;
i {
font-size: 2rem;
}
}
.btn-clear-filters {
color: tomato;
cursor: pointer;
}
}
</style>

View file

@ -0,0 +1,270 @@
<template>
<div class="column">
<woot-modal-header header-title="Filter Conversations">
<p>
{{ $t('FILTER.SUBTITLE') }}
</p>
</woot-modal-header>
<div class="row modal-content">
<div class="medium-12 columns filter-modal-content">
<filter-input-box
v-for="(filter, i) in appliedFilters"
:key="i"
v-model="appliedFilters[i]"
:filter-data="filter"
:filter-attributes="filterAttributes"
:input-type="getInputType(appliedFilters[i].attribute_key)"
:operators="getOperators(appliedFilters[i].attribute_key)"
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
:show-query-operator="i !== appliedFilters.length - 1"
:v="$v.appliedFilters.$each[i]"
@clearPreviousValues="clearPreviousValues(i)"
@removeFilter="removeFilter(i)"
/>
<div class="filter-actions">
<button class="append-filter-btn" @click="appendNewFilter">
<i class="icon ion-plus-circled margin-right-small" />
<span>{{ $t('FILTER.ADD_NEW_FILTER') }}</span>
</button>
</div>
<div class="modal-footer justify-content-end">
<woot-button class="button clear" @click.prevent="onClose">
{{ $t('FILTER.CANCEL_BUTTON_LABEL') }}
</woot-button>
<woot-button @click="submitFilterQuery">
{{ $t('FILTER.SUBMIT_BUTTON_LABEL') }}
</woot-button>
</div>
</div>
</div>
</div>
</template>
<script>
import Modal from '../../../components/Modal';
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators';
import filterInputBox from './components/FilterInput.vue';
import languages from './advancedFilterItems/languages';
import countries from './advancedFilterItems/countries';
export default {
components: {
Modal,
filterInputBox,
},
mixins: [alertMixin],
props: {
onClose: {
type: Function,
default: () => {},
},
filterTypes: {
type: Array,
default: () => [],
},
},
validations: {
appliedFilters: {
required,
$each: {
values: {
required,
},
},
},
},
data() {
return {
show: true,
appliedFilters: [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
},
],
};
},
computed: {
filterAttributes() {
return this.filterTypes.map(type => {
return {
key: type.attributeKey,
name: type.attributeName,
};
});
},
},
mounted() {
this.$store.dispatch('campaigns/get');
},
methods: {
getInputType(key) {
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type.inputType;
},
getOperators(key) {
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type.filterOperators;
},
// eslint-disable-next-line consistent-return
getDropdownValues(type) {
switch (type) {
case 'status':
return [
{
id: 'open',
name: 'Open',
},
{
id: 'resolved',
name: 'Resolved',
},
{
id: 'pending',
name: 'Pending',
},
{
id: 'snoozed',
name: 'Snoozed',
},
{
id: 'all',
name: 'All',
},
];
case 'assignee_id':
return this.$store.getters['agents/getAgents'];
case 'contact_id':
return this.$store.getters['contacts/getContacts'];
case 'inbox_id':
return this.$store.getters['inboxes/getInboxes'];
case 'team_id':
return this.$store.getters['teams/getTeams'];
case 'campaign_id':
return this.$store.getters['campaigns/getAllCampaigns'].map(i => {
return {
id: i.id,
name: i.title,
};
});
case 'labels':
return this.$store.getters['labels/getLabels'].map(i => {
return {
id: i.id,
name: i.title,
};
});
case 'browser_language':
return languages;
case 'country_code':
return countries;
default:
break;
}
},
appendNewFilter() {
this.appliedFilters.push({
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
});
},
removeFilter(index) {
if (this.appliedFilters.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
} else {
this.appliedFilters.splice(index, 1);
}
},
submitFilterQuery() {
this.$v.$touch();
if (this.$v.$invalid) return;
this.appliedFilters[this.appliedFilters.length - 1].query_operator = null;
this.$emit('applyFilter', this.appliedFilters);
},
clearPreviousValues(index) {
this.appliedFilters[index].values = '';
},
},
};
</script>
<style lang="scss">
@import '~widget/assets/scss/variables.scss';
.filter-modal-content {
border: 1px solid $color-border;
border-radius: $space-small;
padding: $space-normal;
}
.filter--attributes {
display: flex;
align-items: center;
margin-bottom: $space-normal;
}
.filter--attribute_clearbtn {
font-size: $font-size-bigger;
margin-left: $space-normal;
cursor: pointer;
}
.filter--attributes_select {
margin-bottom: $zero !important;
}
.filter--values_select {
margin-bottom: $zero !important;
}
.padding-right-small {
padding-right: $space-normal;
}
.margin-right-small {
margin-right: $space-slab;
}
.append-filter-btn {
width: 100%;
border: 1px solid $color-border;
border-radius: $space-small;
display: flex;
align-items: center;
justify-content: center;
color: $color-woot;
font-size: $font-size-big;
padding: $space-normal;
height: 38px;
cursor: pointer;
}
.filter-actions {
margin: $space-large $zero $space-normal $zero;
}
.filter--attributes_input {
margin-bottom: $zero !important;
}
.filter--query_operator {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin: $space-normal $zero;
}
.filter--query_operator_line {
position: absolute;
z-index: 10;
width: 100%;
border-bottom: 1px solid $color-border;
}
.filter--query_operator_container {
position: relative;
z-index: 20;
margin: $zero;
}
.filter--query_operator_select {
width: 100%;
margin-bottom: $zero !important;
border: none;
padding: $zero $space-larger $zero $space-two;
}
</style>

View file

@ -0,0 +1,257 @@
const countries = [
{ name: 'Afghanistan', id: 'AF' },
{ name: 'Åland Islands', id: 'AX' },
{ name: 'Albania', id: 'AL' },
{ name: 'Algeria', id: 'DZ' },
{ name: 'American Samoa', id: 'AS' },
{ name: 'Andorra', id: 'AD' },
{ name: 'Angola', id: 'AO' },
{ name: 'Anguilla', id: 'AI' },
{ name: 'Antarctica', id: 'AQ' },
{ name: 'Antigua and Barbuda', id: 'AG' },
{ name: 'Argentina', id: 'AR' },
{ name: 'Armenia', id: 'AM' },
{ name: 'Aruba', id: 'AW' },
{ name: 'Australia', id: 'AU' },
{ name: 'Austria', id: 'AT' },
{ name: 'Azerbaijan', id: 'AZ' },
{ name: 'Bahamas', id: 'BS' },
{ name: 'Bahrain', id: 'BH' },
{ name: 'Bangladesh', id: 'BD' },
{ name: 'Barbados', id: 'BB' },
{ name: 'Belarus', id: 'BY' },
{ name: 'Belgium', id: 'BE' },
{ name: 'Belize', id: 'BZ' },
{ name: 'Benin', id: 'BJ' },
{ name: 'Bermuda', id: 'BM' },
{ name: 'Bhutan', id: 'BT' },
{ name: 'Bolivia (Plurinational State of)', id: 'BO' },
{ name: 'Bonaire, Sint Eustatius and Saba', id: 'BQ' },
{ name: 'Bosnia and Herzegovina', id: 'BA' },
{ name: 'Botswana', id: 'BW' },
{ name: 'Bouvet Island', id: 'BV' },
{ name: 'Brazil', id: 'BR' },
{ name: 'British Indian Ocean Territory', id: 'IO' },
{ name: 'United States Minor Outlying Islands', id: 'UM' },
{ name: 'Virgin Islands (British)', id: 'VG' },
{ name: 'Virgin Islands (U.S.)', id: 'VI' },
{ name: 'Brunei Darussalam', id: 'BN' },
{ name: 'Bulgaria', id: 'BG' },
{ name: 'Burkina Faso', id: 'BF' },
{ name: 'Burundi', id: 'BI' },
{ name: 'Cambodia', id: 'KH' },
{ name: 'Cameroon', id: 'CM' },
{ name: 'Canada', id: 'CA' },
{ name: 'Cabo Verde', id: 'CV' },
{ name: 'Cayman Islands', id: 'KY' },
{ name: 'Central African Republic', id: 'CF' },
{ name: 'Chad', id: 'TD' },
{ name: 'Chile', id: 'CL' },
{ name: 'China', id: 'CN' },
{ name: 'Christmas Island', id: 'CX' },
{ name: 'Cocos (Keeling) Islands', id: 'CC' },
{ name: 'Colombia', id: 'CO' },
{ name: 'Comoros', id: 'KM' },
{ name: 'Congo', id: 'CG' },
{ name: 'Congo (Democratic Republic of the)', id: 'CD' },
{ name: 'Cook Islands', id: 'CK' },
{ name: 'Costa Rica', id: 'CR' },
{ name: 'Croatia', id: 'HR' },
{ name: 'Cuba', id: 'CU' },
{ name: 'Curaçao', id: 'CW' },
{ name: 'Cyprus', id: 'CY' },
{ name: 'Czech Republic', id: 'CZ' },
{ name: 'Denmark', id: 'DK' },
{ name: 'Djibouti', id: 'DJ' },
{ name: 'Dominica', id: 'DM' },
{ name: 'Dominican Republic', id: 'DO' },
{ name: 'Ecuador', id: 'EC' },
{ name: 'Egypt', id: 'EG' },
{ name: 'El Salvador', id: 'SV' },
{ name: 'Equatorial Guinea', id: 'GQ' },
{ name: 'Eritrea', id: 'ER' },
{ name: 'Estonia', id: 'EE' },
{ name: 'Ethiopia', id: 'ET' },
{ name: 'Falkland Islands (Malvinas)', id: 'FK' },
{ name: 'Faroe Islands', id: 'FO' },
{ name: 'Fiji', id: 'FJ' },
{ name: 'Finland', id: 'FI' },
{ name: 'France', id: 'FR' },
{ name: 'French Guiana', id: 'GF' },
{ name: 'French Polynesia', id: 'PF' },
{ name: 'French Southern Territories', id: 'TF' },
{ name: 'Gabon', id: 'GA' },
{ name: 'Gambia', id: 'GM' },
{ name: 'Georgia', id: 'GE' },
{ name: 'Germany', id: 'DE' },
{ name: 'Ghana', id: 'GH' },
{ name: 'Gibraltar', id: 'GI' },
{ name: 'Greece', id: 'GR' },
{ name: 'Greenland', id: 'GL' },
{ name: 'Grenada', id: 'GD' },
{ name: 'Guadeloupe', id: 'GP' },
{ name: 'Guam', id: 'GU' },
{ name: 'Guatemala', id: 'GT' },
{ name: 'Guernsey', id: 'GG' },
{ name: 'Guinea', id: 'GN' },
{ name: 'Guinea-Bissau', id: 'GW' },
{ name: 'Guyana', id: 'GY' },
{ name: 'Haiti', id: 'HT' },
{ name: 'Heard Island and McDonald Islands', id: 'HM' },
{ name: 'Vatican City', id: 'VA' },
{ name: 'Honduras', id: 'HN' },
{ name: 'Hungary', id: 'HU' },
{ name: 'Hong Kong', id: 'HK' },
{ name: 'Iceland', id: 'IS' },
{ name: 'India', id: 'IN' },
{ name: 'Indonesia', id: 'ID' },
{ name: 'Ivory Coast', id: 'CI' },
{ name: 'Iran (Islamic Republic of)', id: 'IR' },
{ name: 'Iraq', id: 'IQ' },
{ name: 'Ireland', id: 'IE' },
{ name: 'Isle of Man', id: 'IM' },
{ name: 'Israel', id: 'IL' },
{ name: 'Italy', id: 'IT' },
{ name: 'Jamaica', id: 'JM' },
{ name: 'Japan', id: 'JP' },
{ name: 'Jersey', id: 'JE' },
{ name: 'Jordan', id: 'JO' },
{ name: 'Kazakhstan', id: 'KZ' },
{ name: 'Kenya', id: 'KE' },
{ name: 'Kiribati', id: 'KI' },
{ name: 'Kuwait', id: 'KW' },
{ name: 'Kyrgyzstan', id: 'KG' },
{ name: "Lao People's Democratic Republic", id: 'LA' },
{ name: 'Latvia', id: 'LV' },
{ name: 'Lebanon', id: 'LB' },
{ name: 'Lesotho', id: 'LS' },
{ name: 'Liberia', id: 'LR' },
{ name: 'Libya', id: 'LY' },
{ name: 'Liechtenstein', id: 'LI' },
{ name: 'Lithuania', id: 'LT' },
{ name: 'Luxembourg', id: 'LU' },
{ name: 'Macao', id: 'MO' },
{ name: 'North Macedonia', id: 'MK' },
{ name: 'Madagascar', id: 'MG' },
{ name: 'Malawi', id: 'MW' },
{ name: 'Malaysia', id: 'MY' },
{ name: 'Maldives', id: 'MV' },
{ name: 'Mali', id: 'ML' },
{ name: 'Malta', id: 'MT' },
{ name: 'Marshall Islands', id: 'MH' },
{ name: 'Martinique', id: 'MQ' },
{ name: 'Mauritania', id: 'MR' },
{ name: 'Mauritius', id: 'MU' },
{ name: 'Mayotte', id: 'YT' },
{ name: 'Mexico', id: 'MX' },
{ name: 'Micronesia (Federated States of)', id: 'FM' },
{ name: 'Moldova (Republic of)', id: 'MD' },
{ name: 'Monaco', id: 'MC' },
{ name: 'Mongolia', id: 'MN' },
{ name: 'Montenegro', id: 'ME' },
{ name: 'Montserrat', id: 'MS' },
{ name: 'Morocco', id: 'MA' },
{ name: 'Mozambique', id: 'MZ' },
{ name: 'Myanmar', id: 'MM' },
{ name: 'Namibia', id: 'NA' },
{ name: 'Nauru', id: 'NR' },
{ name: 'Nepal', id: 'NP' },
{ name: 'Netherlands', id: 'NL' },
{ name: 'New Caledonia', id: 'NC' },
{ name: 'New Zealand', id: 'NZ' },
{ name: 'Nicaragua', id: 'NI' },
{ name: 'Niger', id: 'NE' },
{ name: 'Nigeria', id: 'NG' },
{ name: 'Niue', id: 'NU' },
{ name: 'Norfolk Island', id: 'NF' },
{ name: "Korea (Democratic People's Republic of)", id: 'KP' },
{ name: 'Northern Mariana Islands', id: 'MP' },
{ name: 'Norway', id: 'NO' },
{ name: 'Oman', id: 'OM' },
{ name: 'Pakistan', id: 'PK' },
{ name: 'Palau', id: 'PW' },
{ name: 'Palestine, State of', id: 'PS' },
{ name: 'Panama', id: 'PA' },
{ name: 'Papua New Guinea', id: 'PG' },
{ name: 'Paraguay', id: 'PY' },
{ name: 'Peru', id: 'PE' },
{ name: 'Philippines', id: 'PH' },
{ name: 'Pitcairn', id: 'PN' },
{ name: 'Poland', id: 'PL' },
{ name: 'Portugal', id: 'PT' },
{ name: 'Puerto Rico', id: 'PR' },
{ name: 'Qatar', id: 'QA' },
{ name: 'Republic of Kosovo', id: 'XK' },
{ name: 'Réunion', id: 'RE' },
{ name: 'Romania', id: 'RO' },
{ name: 'Russian Federation', id: 'RU' },
{ name: 'Rwanda', id: 'RW' },
{ name: 'Saint Barthélemy', id: 'BL' },
{ name: 'Saint Helena, Ascension and Tristan da Cunha', id: 'SH' },
{ name: 'Saint Kitts and Nevis', id: 'KN' },
{ name: 'Saint Lucia', id: 'LC' },
{ name: 'Saint Martin (French part)', id: 'MF' },
{ name: 'Saint Pierre and Miquelon', id: 'PM' },
{ name: 'Saint Vincent and the Grenadines', id: 'VC' },
{ name: 'Samoa', id: 'WS' },
{ name: 'San Marino', id: 'SM' },
{ name: 'Sao Tome and Principe', id: 'ST' },
{ name: 'Saudi Arabia', id: 'SA' },
{ name: 'Senegal', id: 'SN' },
{ name: 'Serbia', id: 'RS' },
{ name: 'Seychelles', id: 'SC' },
{ name: 'Sierra Leone', id: 'SL' },
{ name: 'Singapore', id: 'SG' },
{ name: 'Sint Maarten (Dutch part)', id: 'SX' },
{ name: 'Slovakia', id: 'SK' },
{ name: 'Slovenia', id: 'SI' },
{ name: 'Solomon Islands', id: 'SB' },
{ name: 'Somalia', id: 'SO' },
{ name: 'South Africa', id: 'ZA' },
{ name: 'South Georgia and the South Sandwich Islands', id: 'GS' },
{ name: 'Korea (Republic of)', id: 'KR' },
{ name: 'Spain', id: 'ES' },
{ name: 'Sri Lanka', id: 'LK' },
{ name: 'Sudan', id: 'SD' },
{ name: 'South Sudan', id: 'SS' },
{ name: 'Suriname', id: 'SR' },
{ name: 'Svalbard and Jan Mayen', id: 'SJ' },
{ name: 'Swaziland', id: 'SZ' },
{ name: 'Sweden', id: 'SE' },
{ name: 'Switzerland', id: 'CH' },
{ name: 'Syrian Arab Republic', id: 'SY' },
{ name: 'Taiwan', id: 'TW' },
{ name: 'Tajikistan', id: 'TJ' },
{ name: 'Tanzania, United Republic of', id: 'TZ' },
{ name: 'Thailand', id: 'TH' },
{ name: 'Timor-Leste', id: 'TL' },
{ name: 'Togo', id: 'TG' },
{ name: 'Tokelau', id: 'TK' },
{ name: 'Tonga', id: 'TO' },
{ name: 'Trinidad and Tobago', id: 'TT' },
{ name: 'Tunisia', id: 'TN' },
{ name: 'Turkey', id: 'TR' },
{ name: 'Turkmenistan', id: 'TM' },
{ name: 'Turks and Caicos Islands', id: 'TC' },
{ name: 'Tuvalu', id: 'TV' },
{ name: 'Uganda', id: 'UG' },
{ name: 'Ukraine', id: 'UA' },
{ name: 'United Arab Emirates', id: 'AE' },
{
name: 'United Kingdom of Great Britain and Northern Ireland',
id: 'GB',
},
{ name: 'United States of America', id: 'US' },
{ name: 'Uruguay', id: 'UY' },
{ name: 'Uzbekistan', id: 'UZ' },
{ name: 'Vanuatu', id: 'VU' },
{ name: 'Venezuela (Bolivarian Republic of)', id: 'VE' },
{ name: 'Vietnam', id: 'VN' },
{ name: 'Wallis and Futuna', id: 'WF' },
{ name: 'Western Sahara', id: 'EH' },
{ name: 'Yemen', id: 'YE' },
{ name: 'Zambia', id: 'ZM' },
{ name: 'Zimbabwe', id: 'ZW' },
];
export default countries;

View file

@ -0,0 +1,292 @@
const filterTypes = [
{ attributeKey: 'status',
attributeName: 'Status',
inputType: 'multi_select',
dataType: 'text',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
],
attribute_type: 'standard',
},
{ attributeKey: 'assignee_id',
attributeName: 'Assignee Name',
inputType: 'search_select',
dataType: 'text',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
{
value: 'is_present',
label: 'Is present',
},
{
value: 'is_not_present',
label: 'Is not present',
},
],
attribute_type: 'standard',
},
{ attributeKey: 'contact_id',
attributeName: 'Contact Name',
inputType: 'search_select',
dataType: 'text',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
{
value: 'is_present',
label: 'Is present',
},
{
value: 'is_not_present',
label: 'Is not present',
},
],
attribute_type: 'standard',
},
{ attributeKey: 'inbox_id',
attributeName: 'Inbox Name',
inputType: 'search_select',
dataType: 'text',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
{
value: 'is_present',
label: 'Is present',
},
{
value: 'is_not_present',
label: 'Is not present',
},
],
attribute_type: 'standard',
},
{ attributeKey: 'team_id',
attributeName: 'Team Name',
inputType: 'search_select',
dataType: 'number',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
{
value: 'is_present',
label: 'Is present',
},
{
value: 'is_not_present',
label: 'Is not present',
},
],
attribute_type: 'standard',
},
{ attributeKey: 'id',
attributeName: 'Conversation Identifier',
inputType: 'plain_text',
dataType: 'Number',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
],
attribute_type: 'standard',
},
{ attributeKey: 'campaign_id',
attributeName: 'Campaign Name',
inputType: 'search_select',
dataType: 'Number',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
{
value: 'is_present',
label: 'Is present',
},
{
value: 'is_not_present',
label: 'Is not present',
},
],
attribute_type: 'standard',
},
{ attributeKey: 'labels',
attributeName: 'Labels',
inputType: 'multi_select',
dataType: 'text',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
{
value: 'is_present',
label: 'Is present',
},
{
value: 'is_not_present',
label: 'Is not present',
},
],
attribute_type: 'standard',
},
{ attributeKey: 'browser_language',
attributeName: 'Browser Language',
inputType: 'search_select',
dataType: 'text',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
],
attribute_type: 'additional_attributes',
},
{ attributeKey: 'country_code',
attributeName: 'Country Name',
inputType: 'search_select',
dataType: 'text',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
],
attribute_type: 'additional_attributes',
},
{ attributeKey: 'referer',
attributeName: 'Referer link',
inputType: 'plain_text',
dataType: 'text',
filterOperators: [
{
value: 'equal_to',
label: 'Equal to',
},
{
value: 'not_equal_to',
label: 'Not equal to',
},
{
value: 'contains',
label: 'Contains',
},
{
value: 'does_not_contain',
label: 'Does not contain',
},
],
attribute_type: 'additional_attributes',
},
];
export default filterTypes;

View file

@ -0,0 +1,132 @@
const languages = [
{
id: 'eng',
name: 'English (en)',
},
{
id: 'ar',
name: 'العربية (ar)',
},
{
id: 'nl',
name: 'Nederlands (nl)',
},
{
id: 'fr',
name: 'Français (fr)',
},
{
id: 'de',
name: 'Deutsch (de)',
},
{
id: 'हिन्दी (hi)',
name: 'hi',
},
{
id: 'it',
name: 'Italiano (it)',
},
{
id: 'ja',
name: '日本語 (ja)',
},
{
id: 'ko',
name: '한국어 (ko)',
},
{
id: 'pt',
name: 'Português (pt)',
},
{
id: 'ru',
name: 'русский (ru)',
},
{
id: 'zh',
name: '中文 (zh)',
},
{
id: 'es',
name: 'Español (es)',
},
{
id: 'ml',
name: 'മലയാളം (ml)',
},
{
id: 'ca',
name: 'Català (ca)',
},
{
id: 'el',
name: 'ελληνικά (el)',
},
{
id: 'pt-BR',
name: 'Português Brasileiro (pt-BR)',
},
{
id: 'ro',
name: 'Română (ro)',
},
{
id: 'ta',
name: 'தமிழ் (ta)',
},
{
id: 'fa',
name: 'فارسی (fa)',
},
{
id: 'zh-TW',
name: '中文 (台湾) (zh-TW)',
},
{
id: 'vi',
name: 'Tiếng Việt (vi)',
},
{
id: 'da',
name: 'dansk (da)',
},
{
id: 'tr',
name: 'Türkçe (tr)',
},
{
id: 'cs',
name: 'čeština (cs)',
},
{
id: 'fi',
name: 'suomi, suomen kieli (fi)',
},
{
id: 'id',
name: 'Bahasa Indonesia (id)',
},
{
id: 'sv',
name: 'Svenska (sv)',
},
{
id: 'hu',
name: 'magyar nyelv (hu)',
},
{
id: 'no',
name: 'norsk (no)',
},
{
id: 'zh-CN',
name: '中文 (zh-CN)',
},
{
id: 'pl',
name: 'język polski (pl)',
},
];
export default languages;

View file

@ -0,0 +1,191 @@
<template>
<div class="filters">
<div class="filter--attributes">
<select
v-model="attribute_key"
class="filter--attributes_select"
@change="clearPreviousValues()"
>
<option
v-for="attribute in filterAttributes"
:key="attribute.key"
:value="attribute.key"
>
{{ attribute.name }}
</option>
</select>
<button class="filter--attribute_clearbtn" @click="removeFilter">
<i class="icon ion-close-circled" />
</button>
</div>
<div class="filter-values">
<div class="row">
<div class="small-4 columns padding-right-small">
<select v-model="filter_operator" class="filter--values_select">
<option
v-for="(operator, o) in operators"
:key="o"
:value="operator.value"
>
{{ $t(`FILTER.OPERATOR_LABELS.${operator.value}`) }}
</option>
</select>
</div>
<div class="small-8 columns">
<div
v-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="values"
track-by="id"
label="name"
:placeholder="'Select'"
:multiple="true"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:options="dropdownValues"
:allow-empty="false"
/>
</div>
<div
v-else-if="inputType === 'search_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="values"
track-by="id"
label="name"
:placeholder="'Select'"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<input
v-else
v-model="values"
type="text"
class="filter--attributes_input"
placeholder="Enter value"
/>
<div v-if="v.values.$dirty && v.values.$error" class="filter-error">
Value is required.
</div>
</div>
</div>
</div>
<div v-if="showQueryOperator" class="filter--query_operator">
<hr class="filter--query_operator_line" />
<div class="filter--query_operator_container">
<select v-model="query_operator" class="filter--query_operator_select">
<option value="and">
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.AND') }}
</option>
<option value="or">
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.OR') }}
</option>
</select>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: () => null,
},
filterAttributes: {
type: Array,
default: () => [],
},
inputType: {
type: String,
default: 'plain_text',
},
operators: {
type: Array,
default: () => [],
},
dropdownValues: {
type: Array,
default: () => [],
},
showQueryOperator: {
type: Boolean,
default: false,
},
v: {
type: Object,
default: () => null,
},
},
computed: {
attribute_key: {
get() {
if (!this.value) return null;
return this.value.attribute_key;
},
set(value) {
const payload = this.value || {};
this.$emit('input', { ...payload, attribute_key: value });
},
},
filter_operator: {
get() {
if (!this.value) return null;
return this.value.filter_operator;
},
set(value) {
const payload = this.value || {};
this.$emit('input', { ...payload, filter_operator: value });
},
},
values: {
get() {
if (!this.value) return null;
return this.value.values;
},
set(value) {
const payload = this.value || {};
this.$emit('input', { ...payload, values: value });
},
},
query_operator: {
get() {
if (!this.value) return null;
return this.value.query_operator;
},
set(value) {
const payload = this.value || {};
this.$emit('input', { ...payload, query_operator: value });
},
},
},
methods: {
removeFilter() {
this.$emit('removeFilter');
},
clearPreviousValues() {
this.$emit('clearPreviousValues');
},
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
.multiselect {
margin-bottom: $zero !important;
}
.filter-error {
color: $color-error;
}
</style>

View file

@ -0,0 +1,16 @@
const generatePayload = data => {
let payload = data.map(item => {
if (Array.isArray(item.values)) {
item.values = item.values.map(val => val.id);
} else if (typeof item.values === 'object') {
item.values = [item.values.id];
} else {
item.values = [item.values];
}
return item;
});
return { payload };
};
export default generatePayload;

View file

@ -0,0 +1,68 @@
import filterQueryGenerator from '../filterQueryGenerator';
const testData = [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: [
{ id: 'pending', name: 'Pending' },
{ id: 'resolved', name: 'Resolved' },
],
query_operator: 'and',
},
{
attribute_key: 'assignee',
filter_operator: 'equal_to',
values: {
id: 3,
account_id: 1,
auto_offline: true,
confirmed: true,
email: 'fayazara@gmail.com',
available_name: 'Fayaz',
name: 'Fayaz',
role: 'agent',
thumbnail:
'https://www.gravatar.com/avatar/a35bf18a632f734c8d0c883dcc9fa0ef?d=404',
},
query_operator: 'and',
},
{
attribute_key: 'id',
filter_operator: 'equal_to',
values: 'This is a test',
query_operator: null,
},
];
const finalResult = {
payload: [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['pending', 'resolved'],
query_operator: 'and',
},
{
attribute_key: 'assignee',
filter_operator: 'equal_to',
values: [3],
query_operator: 'and',
},
{
attribute_key: 'id',
filter_operator: 'equal_to',
values: ['This is a test'],
query_operator: null,
},
],
};
describe('#filterQueryGenerator', () => {
it('returns the correct format of filter query', () => {
expect(filterQueryGenerator(testData)).toMatchObject(finalResult);
expect(
filterQueryGenerator(testData).payload.every(i => Array.isArray(i.values))
).toBe(true);
});
});

View file

@ -0,0 +1,21 @@
{
"FILTER": {
"SUBTITLE": "Add filters below and hit 'Submit' to filter conversations.",
"ADD_NEW_FILTER": "Add Filter",
"FILTER_DELETE_ERROR": "You should have atleast one filter to save",
"SUBMIT_BUTTON_LABEL": "Submit",
"CANCEL_BUTTON_LABEL": "Cancel",
"QUERY_DROPDOWN_LABELS": {
"AND": "AND",
"OR": "OR"
},
"OPERATOR_LABELS": {
"equal_to": "Equal to",
"not_equal_to": "Not equal to",
"contains": "Contains",
"does_not_contain": "Does not contain",
"is_present": "Is present",
"is_not_present": "Is not present"
}
}
}

View file

@ -18,6 +18,7 @@ import { default as _setNewPassword } from './setNewPassword.json';
import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json';
import { default as _teamsSettings } from './teamsSettings.json';
import { default as _advancedFilters } from './advancedFilters.json';
export default {
..._agentMgmt,
@ -40,4 +41,5 @@ export default {
..._settings,
..._signup,
..._teamsSettings,
..._advancedFilters,
};

View file

@ -19,6 +19,9 @@ export const getters = {
record => record.campaign_type === campaignType
);
},
getAllCampaigns: _state => {
return _state.records;
},
};
export const actions = {

View file

@ -52,6 +52,37 @@ const actions = {
}
},
fetchFilteredConversations: async ({ commit, dispatch }, payload) => {
commit(types.default.SET_LIST_LOADING_STATUS);
try {
const { data } = await ConversationApi.filter(payload);
const { payload: chatList, meta: metaData } = data;
commit(types.default.SET_ALL_CONVERSATION, chatList);
dispatch('conversationStats/set', metaData);
dispatch('conversationLabels/setBulkConversationLabels', chatList);
commit(types.default.CLEAR_LIST_LOADING_STATUS);
commit(
`contacts/${types.default.SET_CONTACTS}`,
chatList.map(chat => chat.meta.sender)
);
dispatch(
'conversationPage/setCurrentPage',
{ filter: params.assigneeType, page: params.page },
{ root: true }
);
if (!chatList.length) {
dispatch(
'conversationPage/setEndReached',
{ filter: params.assigneeType },
{ root: true }
);
}
} catch (error) {
console.log(error);
// Handle error
}
},
emptyAllConversations({ commit }) {
commit(types.default.EMPTY_ALL_CONVERSATION);
},

View file

@ -0,0 +1,44 @@
class Contacts::FilterService < FilterService
def perform
@contacts = contact_query_builder
{
contacts: @contacts,
count: {
all_count: @contacts.count
}
}
end
def contact_query_builder
contact_filters = @filters['contacts']
@params[:payload].each_with_index do |query_hash, current_index|
current_filter = contact_filters[query_hash['attribute_key']]
@query_string += contact_query_string(current_filter, query_hash, current_index)
end
base_relation.where(@query_string, @filter_values.with_indifferent_access)
end
def contact_query_string(current_filter, query_hash, current_index)
attribute_key = query_hash[:attribute_key]
query_operator = query_hash[:query_operator]
filter_operator_value = filter_operation(query_hash, current_index)
case current_filter['attribute_type']
when 'additional_attributes'
" contacts.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
when 'standard'
if attribute_key == 'labels'
" tags.id #{filter_operator_value} #{query_operator} "
else
" contacts.#{attribute_key} #{filter_operator_value} #{query_operator} "
end
end
end
def base_relation
Current.account.contacts.left_outer_joins(:labels)
end
end

View file

@ -0,0 +1,59 @@
class Conversations::FilterService < FilterService
def perform
@conversations = conversation_query_builder
mine_count, unassigned_count, all_count, = set_count_for_all_conversations
assigned_count = all_count - unassigned_count
{
conversations: conversations,
count: {
mine_count: mine_count,
assigned_count: assigned_count,
unassigned_count: unassigned_count,
all_count: all_count
}
}
end
def conversation_query_builder
conversation_filters = @filters['conversations']
@params[:payload].each_with_index do |query_hash, current_index|
current_filter = conversation_filters[query_hash['attribute_key']]
@query_string += conversation_query_string(current_filter, query_hash, current_index)
end
base_relation.where(@query_string, @filter_values.with_indifferent_access)
end
def conversation_query_string(current_filter, query_hash, current_index)
attribute_key = query_hash[:attribute_key]
query_operator = query_hash[:query_operator]
filter_operator_value = filter_operation(query_hash, current_index)
case current_filter['attribute_type']
when 'additional_attributes'
" conversations.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
when 'standard'
if attribute_key == 'labels'
" tags.id #{filter_operator_value} #{query_operator} "
else
" conversations.#{attribute_key} #{filter_operator_value} #{query_operator} "
end
end
end
def base_relation
Current.account.conversations.left_outer_joins(:labels)
end
def current_page
@params[:page] || 1
end
def conversations
@conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
)
@conversations.latest.page(current_page)
end
end

View file

@ -0,0 +1,50 @@
require 'json'
class FilterService
def initialize(params, user)
@params = params
@user = user
file = File.read('./lib/filters/filter_keys.json')
@filters = JSON.parse(file)
@query_string = ''
@filter_values = {}
end
def perform; end
def filter_operation(query_hash, current_index)
case query_hash[:filter_operator]
when 'equal_to'
@filter_values["value_#{current_index}"] = filter_values(query_hash)
"IN (:value_#{current_index})"
when 'not_equal_to'
@filter_values["value_#{current_index}"] = filter_values(query_hash)
"NOT IN (:value_#{current_index})"
when 'contains'
@filter_values["value_#{current_index}"] = "%#{filter_values(query_hash)}%"
"LIKE :value_#{current_index}"
when 'does_not_contain'
@filter_values["value_#{current_index}"] = "%#{filter_values(query_hash)}%"
"NOT LIKE :value_#{current_index}"
else
@filter_values["value_#{current_index}"] = filter_values(query_hash).to_s
"= :value_#{current_index}"
end
end
def filter_values(query_hash)
if query_hash['attribute_key'] == 'status'
query_hash['values'].map { |x| Conversation.statuses[x.to_sym] }
else
query_hash['values']
end
end
def set_count_for_all_conversations
[
@conversations.assigned_to(@user).count,
@conversations.unassigned.count,
@conversations.count
]
end
end

View file

@ -1,3 +1,10 @@
json.array! @conversations do |conversation|
json.partial! 'api/v1/models/conversation.json.jbuilder', conversation: conversation
json.meta do
json.mine_count @conversations_count[:mine_count]
json.unassigned_count @conversations_count[:unassigned_count]
json.all_count @conversations_count[:all_count]
end
json.payload do
json.array! @conversations do |conversation|
json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation
end
end

View file

@ -61,7 +61,7 @@ Rails.application.routes.draw do
collection do
get :meta
get :search
get :filter
post :filter
end
scope module: :conversations do
resources :messages, only: [:index, :create, :destroy]
@ -83,7 +83,7 @@ Rails.application.routes.draw do
collection do
get :active
get :search
get :filter
post :filter
post :import
end
member do

View file

@ -65,8 +65,8 @@
"attribute_type": "standard"
},
{
"attribute_key": "browser",
"attribute_name": "browser",
"attribute_key": "browser_language",
"attribute_name": "Browser Language",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],

View file

@ -0,0 +1,153 @@
{
"conversations": {
"status": {
"attribute_name": "Status",
"input_type": "multi_select",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to" ],
"attribute_type": "standard"
},
"assignee_id": {
"attribute_name": "Assignee Name",
"input_type": "search_box with name tags/plain text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"contact_id": {
"attribute_name": "Contact Name",
"input_type": "plain_text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"inbox_id": {
"attribute_name": "Inbox Name",
"input_type": "search_box",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"team_id": {
"attribute_name": "Team Name",
"input_type": "search_box",
"data_type": "number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"id": {
"attribute_name": "Conversation Identifier",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"campaign_id": {
"attribute_name": "Campaign Name",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"labels": {
"attribute_name": "Labels",
"input_type": "tags",
"data_type": "text",
"filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ],
"attribute_type": "standard"
},
"browser_language": {
"attribute_name": "Browser Language",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
"attribute_type": "additional_attributes"
},
"country_code": {
"attribute_name": "Country Name",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
},
"referer": {
"attribute_name": "Referer link",
"input_type": "textbox",
"data_type": "link",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
}
},
"contacts": {
"assignee_id": {
"attribute_name": "Assignee Name",
"input_type": "search_box with name tags/plain text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"contact_id": {
"attribute_name": "Contact Name",
"input_type": "plain_text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"inbox_id": {
"attribute_name": "Inbox Name",
"input_type": "search_box",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"team_id": {
"attribute_name": "Team Name",
"input_type": "search_box",
"data_type": "number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"id": {
"attribute_name": "Conversation Identifier",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"campaign_id": {
"attribute_name": "Campaign Name",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"labels": {
"attribute_name": "Labels",
"input_type": "tags",
"data_type": "text",
"filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ],
"attribute_type": "standard"
},
"browser_language": {
"attribute_name": "Browser Language",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
"attribute_type": "additional_attributes"
},
"country_code": {
"attribute_name": "Country Name",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
},
"referer": {
"attribute_name": "Referer link",
"input_type": "textbox",
"data_type": "link",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
}
}
}

View file

@ -233,10 +233,12 @@ RSpec.describe 'Contacts API', type: :request do
let!(:contact2) { create(:contact, :with_email, name: 'testcontact', account: account, email: 'test@test.com') }
it 'returns all contacts when query is empty' do
get "/api/v1/accounts/#{account.id}/contacts/filter",
params: { q: [] },
headers: admin.create_new_auth_token,
as: :json
post "/api/v1/accounts/#{account.id}/contacts/filter",
params: {
payload: []
},
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact2.email)

View file

@ -112,7 +112,7 @@ RSpec.describe 'Conversations API', type: :request do
describe 'GET /api/v1/accounts/{account.id}/conversations/filter' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/conversations/filter", params: { q: 'test' }
post "/api/v1/accounts/#{account.id}/conversations/filter", params: { q: 'test' }
expect(response).to have_http_status(:unauthorized)
end
@ -129,16 +129,15 @@ RSpec.describe 'Conversations API', type: :request do
end
it 'returns all conversations with empty query' do
get "/api/v1/accounts/#{account.id}/conversations/filter",
headers: agent.create_new_auth_token,
params: { q: 'test1' },
as: :json
post "/api/v1/accounts/#{account.id}/conversations/filter",
headers: agent.create_new_auth_token,
params: { payload: [] },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data.count).to eq(1)
expect(response_data[0][:messages][0][:content]).to include(Message.first.content)
expect(response_data.count).to eq(2)
end
end
end

View file

@ -0,0 +1,56 @@
require 'rails_helper'
describe ::Contacts::FilterService do
subject(:filter_service) { described_class }
let!(:account) { create(:account) }
let!(:user_1) { create(:user, account: account) }
let!(:user_2) { create(:user, account: account) }
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
let!(:contact) { create(:contact, account: account, additional_attributes: { 'browser_language': 'en' }) }
before do
create(:inbox_member, user: user_1, inbox: inbox)
create(:inbox_member, user: user_2, inbox: inbox)
create(:conversation, account: account, inbox: inbox, assignee: user_1, contact: contact)
create(:conversation, account: account, inbox: inbox)
Current.account = account
end
describe '#perform' do
context 'with query present' do
let!(:params) { { payload: [], page: 1 } }
let(:payload) do
[
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['en'],
query_operator: nil
}.with_indifferent_access
]
end
it 'filter contacts by additional_attributes' do
params[:payload] = payload
result = filter_service.new(params, user_1).perform
expect(result.length).to be 2
end
it 'filter conversations by tags' do
Contact.last.update_labels('support')
params[:payload] = [
{
attribute_key: 'labels',
filter_operator: 'equal_to',
values: [1],
query_operator: nil
}.with_indifferent_access
]
result = filter_service.new(params, user_1).perform
expect(result.length).to be 2
end
end
end
end

View file

@ -0,0 +1,76 @@
require 'rails_helper'
describe ::Conversations::FilterService do
subject(:filter_service) { described_class }
let!(:account) { create(:account) }
let!(:user_1) { create(:user, account: account) }
let!(:user_2) { create(:user, account: account) }
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
before do
create(:inbox_member, user: user_1, inbox: inbox)
create(:inbox_member, user: user_2, inbox: inbox)
create(:conversation, account: account, inbox: inbox, assignee: user_1)
create(:conversation, account: account, inbox: inbox, assignee: user_1,
status: 'pending', additional_attributes: { 'browser_language': 'en' })
create(:conversation, account: account, inbox: inbox, assignee: user_1,
status: 'pending', additional_attributes: { 'browser_language': 'en' })
create(:conversation, account: account, inbox: inbox, assignee: user_2)
# unassigned conversation
create(:conversation, account: account, inbox: inbox)
Current.account = account
end
describe '#perform' do
context 'with query present' do
let!(:params) { { payload: [], page: 1 } }
let(:payload) do
[
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['en'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: %w[open pending],
query_operator: nil
}.with_indifferent_access
]
end
it 'filter conversations by custom_attributes and status' do
params[:payload] = payload
result = filter_service.new(params, user_1).perform
conversations = Conversation.where("additional_attributes ->> 'browser_language' IN (?) AND status IN (?)", ['en'], [1, 2])
expect(result.length).to be conversations.count
end
it 'filter conversations by tags' do
Conversation.last.update_labels('support')
params[:payload] = [
{
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: [
user_1.id,
user_2.id
],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'labels',
filter_operator: 'equal_to',
values: [1],
query_operator: nil
}.with_indifferent_access
]
result = filter_service.new(params, user_1).perform
expect(result.length).to be 2
end
end
end
end