feat: Add Advanced Conversation Filters (#3239)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Tejaswini <tejaswini@chatwoot.com>
This commit is contained in:
parent
bf96e8b68d
commit
c2333214af
30 changed files with 1778 additions and 78 deletions
|
@ -40,3 +40,6 @@ exclude_patterns:
|
|||
- "app/javascript/dashboard/i18n/locale"
|
||||
- "**/*.stories.js"
|
||||
- "stories/"
|
||||
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js"
|
||||
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/countries.js"
|
||||
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js"
|
||||
|
|
|
@ -19,6 +19,14 @@ class ConversationApi extends ApiClient {
|
|||
});
|
||||
}
|
||||
|
||||
filter(payload) {
|
||||
return axios.post(`${this.url}/filter`, payload.queryData, {
|
||||
params: {
|
||||
page: payload.page,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
search({ q }) {
|
||||
return axios.get(`${this.url}/search`, {
|
||||
params: {
|
||||
|
@ -54,7 +62,7 @@ class ConversationApi extends ApiClient {
|
|||
toggleTyping({ conversationId, status, isPrivate }) {
|
||||
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
||||
typing_status: status,
|
||||
is_private: isPrivate
|
||||
is_private: isPrivate,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ describe('#ConversationAPI', () => {
|
|||
expect(conversationAPI).toHaveProperty('unmute');
|
||||
expect(conversationAPI).toHaveProperty('meta');
|
||||
expect(conversationAPI).toHaveProperty('sendEmailTranscript');
|
||||
expect(conversationAPI).toHaveProperty('filter');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
|
@ -173,5 +174,41 @@ describe('#ConversationAPI', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#filter', () => {
|
||||
const payload = {
|
||||
page: 1,
|
||||
queryData: {
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
conversationAPI.filter(payload);
|
||||
expect(
|
||||
context.axiosMock.post
|
||||
).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/filter',
|
||||
payload.queryData,
|
||||
{ params: { page: payload.page } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
|
||||
.status--filter {
|
||||
@include padding($zero null $zero $space-normal);
|
||||
@include margin($space-smaller $space-slab $zero $zero);
|
||||
@include margin($zero);
|
||||
background-color: $color-background-light;
|
||||
border: 1px solid $color-border;
|
||||
float: right;
|
||||
|
|
|
@ -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,10 @@
|
|||
button {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
&.justify-content-end {
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-item {
|
||||
|
@ -97,7 +98,6 @@
|
|||
@include margin($zero);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modal-enter,
|
||||
|
|
|
@ -1,14 +1,39 @@
|
|||
<template>
|
||||
l<template>
|
||||
<div class="conversations-list-wrap">
|
||||
<slot></slot>
|
||||
<div class="chat-list__top">
|
||||
<div class="chat-list__top" :class="{ filter__applied: hasAppliedFilters }">
|
||||
<h1 class="page-title text-truncate" :title="pageTitle">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
<chat-filter @statusFilterChange="updateStatusType" />
|
||||
|
||||
<div class="filter--actions">
|
||||
<chat-filter
|
||||
v-if="!hasAppliedFilters"
|
||||
@statusFilterChange="updateStatusType"
|
||||
/>
|
||||
<woot-button
|
||||
v-else
|
||||
variant="clear"
|
||||
color-scheme="danger"
|
||||
class="btn-clear-filters"
|
||||
@click="resetAndFetchData"
|
||||
>
|
||||
{{ $t('FILTER.CLEAR_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.TOOLTIP_LABEL')"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class="btn-filter"
|
||||
@click="onToggleAdvanceFiltersModal"
|
||||
>
|
||||
<i class="icon ion-ios-settings-strong" />
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<chat-type-tabs
|
||||
v-if="!hasAppliedFilters"
|
||||
:items="assigneeTabItems"
|
||||
:active-tab="activeAssigneeTab"
|
||||
class="tab--chat-type"
|
||||
|
@ -37,7 +62,7 @@
|
|||
v-if="!hasCurrentPageEndReached && !chatListLoading"
|
||||
variant="clear"
|
||||
size="expanded"
|
||||
@click="fetchConversations"
|
||||
@click="loadMoreConversations"
|
||||
>
|
||||
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
|
||||
</woot-button>
|
||||
|
@ -53,6 +78,17 @@
|
|||
{{ $t('CHAT_LIST.EOF') }}
|
||||
</p>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show.sync="showAdvancedFilters"
|
||||
:on-close="onToggleAdvanceFiltersModal"
|
||||
>
|
||||
<conversation-advanced-filter
|
||||
v-if="showAdvancedFilters"
|
||||
:filter-types="advancedFilterTypes"
|
||||
:on-close="onToggleAdvanceFiltersModal"
|
||||
@applyFilter="onApplyFilter"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -60,12 +96,16 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
|
||||
import ChatFilter from './widgets/conversation/ChatFilter';
|
||||
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter';
|
||||
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,
|
||||
|
@ -76,6 +116,7 @@ export default {
|
|||
ChatTypeTabs,
|
||||
ConversationCard,
|
||||
ChatFilter,
|
||||
ConversationAdvancedFilter,
|
||||
},
|
||||
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
||||
props: {
|
||||
|
@ -96,6 +137,11 @@ export default {
|
|||
return {
|
||||
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||
showAdvancedFilters: false,
|
||||
advancedFilterTypes: advancedFilterTypes.map(filter => ({
|
||||
...filter,
|
||||
attributeName: this.$t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
|
||||
})),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -109,7 +155,11 @@ export default {
|
|||
currentUserID: 'getCurrentUserID',
|
||||
activeInbox: 'getSelectedInbox',
|
||||
conversationStats: 'conversationStats/getStats',
|
||||
appliedFilters: 'getAppliedFilters',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length;
|
||||
},
|
||||
assigneeTabItems() {
|
||||
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
|
||||
const count = this.conversationStats[item.COUNT_KEY] || 0;
|
||||
|
@ -131,9 +181,17 @@ export default {
|
|||
this.activeAssigneeTab
|
||||
);
|
||||
},
|
||||
currentPageFilterKey() {
|
||||
return this.hasAppliedFilters ? 'appliedFilters' : this.activeAssigneeTab;
|
||||
},
|
||||
currentFiltersPage() {
|
||||
return this.$store.getters['conversationPage/getCurrentPageFilter'](
|
||||
this.currentPageFilterKey
|
||||
);
|
||||
},
|
||||
hasCurrentPageEndReached() {
|
||||
return this.$store.getters['conversationPage/getHasEndReached'](
|
||||
this.activeAssigneeTab
|
||||
this.currentPageFilterKey
|
||||
);
|
||||
},
|
||||
conversationFilters() {
|
||||
|
@ -160,13 +218,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.hasAppliedFilters) {
|
||||
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 +260,17 @@ export default {
|
|||
});
|
||||
},
|
||||
methods: {
|
||||
onApplyFilter(payload) {
|
||||
if (this.$route.name !== 'home') {
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.fetchFilteredConversations(payload);
|
||||
},
|
||||
onToggleAdvanceFiltersModal() {
|
||||
this.showAdvancedFilters = !this.showAdvancedFilters;
|
||||
},
|
||||
getKeyboardListenerParams() {
|
||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||
'div.conversations-list div.conversation'
|
||||
|
@ -245,6 +318,7 @@ export default {
|
|||
resetAndFetchData() {
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.$store.dispatch('clearConversationFilters');
|
||||
this.fetchConversations();
|
||||
},
|
||||
fetchConversations() {
|
||||
|
@ -252,6 +326,23 @@ export default {
|
|||
.dispatch('fetchAllConversations', this.conversationFilters)
|
||||
.then(() => this.$emit('conversation-load'));
|
||||
},
|
||||
loadMoreConversations() {
|
||||
if (!this.hasAppliedFilters) {
|
||||
this.fetchConversations();
|
||||
} else {
|
||||
this.fetchFilteredConversations(this.appliedFilters);
|
||||
}
|
||||
},
|
||||
fetchFilteredConversations(payload) {
|
||||
let page = this.currentFiltersPage + 1;
|
||||
this.$store
|
||||
.dispatch('fetchFilteredConversations', {
|
||||
queryData: filterQueryGenerator(payload),
|
||||
page,
|
||||
})
|
||||
.then(() => this.$emit('conversation-load'));
|
||||
this.showAdvancedFilters = false;
|
||||
},
|
||||
updateAssigneeTab(selectedTab) {
|
||||
if (this.activeAssigneeTab !== selectedTab) {
|
||||
bus.$emit('clearSearchInput');
|
||||
|
@ -273,6 +364,7 @@ export default {
|
|||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/woot';
|
||||
|
||||
.spinner {
|
||||
margin-top: var(--space-normal);
|
||||
margin-bottom: var(--space-normal);
|
||||
|
@ -295,4 +387,23 @@ export default {
|
|||
flex-basis: 46rem;
|
||||
}
|
||||
}
|
||||
.filter--actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.btn-filter {
|
||||
cursor: pointer;
|
||||
i {
|
||||
font-size: var(--font-size-two);
|
||||
}
|
||||
}
|
||||
.btn-clear-filters {
|
||||
color: var(--r-500);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter__applied {
|
||||
padding: var(--space-slab) 0 !important;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,284 @@
|
|||
<template>
|
||||
<div class="column">
|
||||
<woot-modal-header :header-title="$t('FILTER.TITLE')">
|
||||
<p>{{ $t('FILTER.SUBTITLE') }}</p>
|
||||
</woot-modal-header>
|
||||
<div class="row modal-content">
|
||||
<div class="medium-12 columns">
|
||||
<filter-input-box
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
:key="i"
|
||||
v-model="appliedFilters[i]"
|
||||
: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"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
:v="$v.appliedFilters.$each[i]"
|
||||
@resetFilter="resetFilter(i, appliedFilters[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 w-full">
|
||||
<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 alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||
import filterInputBox from './components/FilterInput.vue';
|
||||
import languages from './advancedFilterItems/languages';
|
||||
import countries from './advancedFilterItems/countries';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
filterInputBox,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
filterTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
appliedFilters: {
|
||||
required,
|
||||
$each: {
|
||||
values: {
|
||||
required: requiredIf(prop => {
|
||||
let userInputRequired = true;
|
||||
if (
|
||||
prop.filter_operator !== 'is_present' ||
|
||||
prop.filter_operator !== 'is_not_present'
|
||||
)
|
||||
userInputRequired = false;
|
||||
return userInputRequired;
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
appliedFilters: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filterAttributes() {
|
||||
return this.filterTypes.map(type => {
|
||||
return {
|
||||
key: type.attributeKey,
|
||||
name: type.attributeName,
|
||||
attributeI18nKey: type.attributeI18nKey,
|
||||
};
|
||||
});
|
||||
},
|
||||
getAppliedFilters() {
|
||||
return this.$store.getters.getAppliedFilters;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('campaigns/get');
|
||||
if (this.getAppliedFilters.length) {
|
||||
this.appliedFilters = this.getAppliedFilters;
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
}
|
||||
},
|
||||
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) {
|
||||
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
switch (type) {
|
||||
case 'status':
|
||||
return [
|
||||
...Object.keys(statusFilters).map(status => {
|
||||
return {
|
||||
id: status,
|
||||
name: statusFilters[status].TEXT,
|
||||
};
|
||||
}),
|
||||
{
|
||||
id: 'all',
|
||||
name: this.$t('CHAT_LIST.FILTER_ALL'),
|
||||
},
|
||||
];
|
||||
case 'assignee_id':
|
||||
return this.$store.getters['agents/getAgents'];
|
||||
case 'contact':
|
||||
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.$store.dispatch(
|
||||
'setConversationFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
);
|
||||
this.$emit('applyFilter', this.appliedFilters);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
||||
filter => filter.attributeKey === currentFilter.attribute_key
|
||||
).filterOperators[0].value;
|
||||
this.appliedFilters[index].values = '';
|
||||
},
|
||||
showUserInput(operatorType) {
|
||||
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter-modal-content {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--space-half);
|
||||
padding: var(--space-one);
|
||||
}
|
||||
.filter--attributes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-one);
|
||||
}
|
||||
.filter--attribute_clearbtn {
|
||||
font-size: var(--space-two);
|
||||
margin-left: var(--space-one);
|
||||
cursor: pointer;
|
||||
}
|
||||
.filter--attributes_select {
|
||||
margin-bottom: var(--space-zero) !important;
|
||||
}
|
||||
|
||||
.filter--values_select {
|
||||
margin-bottom: var(--space-zero) !important;
|
||||
}
|
||||
|
||||
.padding-right-small {
|
||||
padding-right: var(--space-one);
|
||||
}
|
||||
.margin-right-small {
|
||||
margin-right: var(--space-three-fourths);
|
||||
}
|
||||
.append-filter-btn {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--space-one-fourths);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-woot);
|
||||
font-size: var(--font-size-one-and-half);
|
||||
padding: var(--space-one);
|
||||
height: var(----space-medium);
|
||||
cursor: pointer;
|
||||
}
|
||||
.filter-actions {
|
||||
margin: var(--space-two) var(--space-zero) var(--space-one) var(--space-zero);
|
||||
}
|
||||
.filter--attributes_input {
|
||||
margin-bottom: var(--space-zero) !important;
|
||||
}
|
||||
.filter--query_operator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
margin: var(--space-one) var(--space-zero);
|
||||
}
|
||||
.filter--query_operator_line {
|
||||
position: absolute;
|
||||
z-index: var(--z-index-low);
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.filter--query_operator_container {
|
||||
position: relative;
|
||||
z-index: var(--z-index-twenty);
|
||||
margin: var(--space-zero);
|
||||
}
|
||||
.filter--query_operator_select {
|
||||
width: 100%;
|
||||
margin-bottom: var(--space-zero) !important;
|
||||
border: none;
|
||||
padding: var(--space-zero) var(--space-three) var(--space-zero)
|
||||
var(--space-two);
|
||||
}
|
||||
</style>
|
|
@ -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;
|
|
@ -0,0 +1,230 @@
|
|||
const filterTypes = [
|
||||
{
|
||||
attributeKey: 'status',
|
||||
attributeI18nKey: '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',
|
||||
attributeI18nKey: 'ASSIGNEE_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'inbox_id',
|
||||
attributeI18nKey: 'INBOX_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'team_id',
|
||||
attributeI18nKey: 'TEAM_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'number',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'display_id',
|
||||
attributeI18nKey: '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',
|
||||
attributeI18nKey: 'CAMPAIGN_NAME',
|
||||
inputType: 'search_select',
|
||||
dataType: 'Number',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'labels',
|
||||
attributeI18nKey: 'LABELS',
|
||||
inputType: 'multi_select',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'browser_language',
|
||||
attributeI18nKey: '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',
|
||||
attributeI18nKey: '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',
|
||||
attributeI18nKey: '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;
|
|
@ -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;
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<div class="filters">
|
||||
<div class="filter--attributes">
|
||||
<select
|
||||
v-model="attribute_key"
|
||||
class="filter--attributes_select"
|
||||
@change="resetFilter()"
|
||||
>
|
||||
<option
|
||||
v-for="attribute in filterAttributes"
|
||||
:key="attribute.key"
|
||||
:value="attribute.key"
|
||||
>
|
||||
{{ $t(`FILTER.ATTRIBUTES.${attribute.attributeI18nKey}`) }}
|
||||
</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="columns padding-right-small"
|
||||
:class="showUserInput ? 'small-4' : 'small-12'"
|
||||
>
|
||||
<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 v-if="showUserInput" 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">
|
||||
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</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,
|
||||
},
|
||||
showUserInput: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
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');
|
||||
},
|
||||
resetFilter() {
|
||||
this.$emit('resetFilter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.multiselect {
|
||||
margin-bottom: var(--space-zero) !important;
|
||||
}
|
||||
.filter-error {
|
||||
color: var(--r-500);
|
||||
}
|
||||
</style>
|
18
app/javascript/dashboard/helper/filterQueryGenerator.js
Normal file
18
app/javascript/dashboard/helper/filterQueryGenerator.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
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 if (!item.values) {
|
||||
item.values = [];
|
||||
} else {
|
||||
item.values = [item.values];
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
return { payload };
|
||||
};
|
||||
|
||||
export default generatePayload;
|
|
@ -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);
|
||||
});
|
||||
});
|
37
app/javascript/dashboard/i18n/locale/en/advancedFilters.json
Normal file
37
app/javascript/dashboard/i18n/locale/en/advancedFilters.json
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"FILTER": {
|
||||
"TITLE": "Filter Conversations",
|
||||
"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",
|
||||
"CLEAR_BUTTON_LABEL": "Clear Filters",
|
||||
"EMPTY_VALUE_ERROR": "Value is required",
|
||||
"TOOLTIP_LABEL": "Filter conversations",
|
||||
"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"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"STATUS": "Status",
|
||||
"ASSIGNEE_NAME": "Assignee Name",
|
||||
"INBOX_NAME": "Inbox Name",
|
||||
"TEAM_NAME": "Team Name",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation Identifier",
|
||||
"CAMPAIGN_NAME": "Campaign Name",
|
||||
"LABELS": "Labels",
|
||||
"BROWSER_LANGUAGE": "Browser Language",
|
||||
"COUNTRY_NAME": "Country Name",
|
||||
"REFERER_LINK": "Referer link"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
import { default as _automation } from './automation.json';
|
||||
|
||||
export default {
|
||||
|
@ -41,5 +42,6 @@ export default {
|
|||
..._settings,
|
||||
..._signup,
|
||||
..._teamsSettings,
|
||||
..._advancedFilters,
|
||||
..._automation,
|
||||
};
|
||||
|
|
|
@ -19,6 +19,9 @@ export const getters = {
|
|||
record => record.campaign_type === campaignType
|
||||
);
|
||||
},
|
||||
getAllCampaigns: _state => {
|
||||
return _state.records;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
|
|
@ -6,6 +6,7 @@ const state = {
|
|||
me: 0,
|
||||
unassigned: 0,
|
||||
all: 0,
|
||||
appliedFilters: 0,
|
||||
},
|
||||
hasEndReached: {
|
||||
me: false,
|
||||
|
@ -54,12 +55,14 @@ export const mutations = {
|
|||
me: 0,
|
||||
unassigned: 0,
|
||||
all: 0,
|
||||
appliedFilters: 0,
|
||||
};
|
||||
|
||||
$state.hasEndReached = {
|
||||
me: false,
|
||||
unassigned: false,
|
||||
all: false,
|
||||
appliedFilters: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,6 +5,42 @@ import MessageApi from '../../../api/inbox/message';
|
|||
import { MESSAGE_STATUS, MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { createPendingMessage } from 'dashboard/helper/commons';
|
||||
|
||||
const setPageFilter = ({ dispatch, filter, page, markEndReached }) => {
|
||||
dispatch('conversationPage/setCurrentPage', { filter, page }, { root: true });
|
||||
if (markEndReached) {
|
||||
dispatch('conversationPage/setEndReached', { filter }, { root: true });
|
||||
}
|
||||
};
|
||||
|
||||
const setContacts = (commit, chatList) => {
|
||||
commit(
|
||||
`contacts/${types.SET_CONTACTS}`,
|
||||
chatList.map(chat => chat.meta.sender)
|
||||
);
|
||||
};
|
||||
|
||||
const buildConversationList = (
|
||||
context,
|
||||
requestPayload,
|
||||
responseData,
|
||||
filterType
|
||||
) => {
|
||||
const { payload: conversationList, meta: metaData } = responseData;
|
||||
context.commit(types.SET_ALL_CONVERSATION, conversationList);
|
||||
context.dispatch('conversationStats/set', metaData);
|
||||
context.dispatch(
|
||||
'conversationLabels/setBulkConversationLabels',
|
||||
conversationList
|
||||
);
|
||||
context.commit(types.CLEAR_LIST_LOADING_STATUS);
|
||||
setContacts(context.commit, conversationList);
|
||||
setPageFilter({
|
||||
dispatch: context.dispatch,
|
||||
filter: filterType,
|
||||
page: requestPayload.page,
|
||||
markEndReached: !conversationList.length,
|
||||
});
|
||||
};
|
||||
// actions
|
||||
const actions = {
|
||||
getConversation: async ({ commit }, conversationId) => {
|
||||
|
@ -20,30 +56,30 @@ const actions = {
|
|||
fetchAllConversations: async ({ commit, dispatch }, params) => {
|
||||
commit(types.SET_LIST_LOADING_STATUS);
|
||||
try {
|
||||
const response = await ConversationApi.get(params);
|
||||
const {
|
||||
data: { payload: chatList, meta: metaData },
|
||||
} = response.data;
|
||||
commit(types.SET_ALL_CONVERSATION, chatList);
|
||||
dispatch('conversationStats/set', metaData);
|
||||
dispatch('conversationLabels/setBulkConversationLabels', chatList);
|
||||
commit(types.CLEAR_LIST_LOADING_STATUS);
|
||||
commit(
|
||||
`contacts/${types.SET_CONTACTS}`,
|
||||
chatList.map(chat => chat.meta.sender)
|
||||
data: { data },
|
||||
} = await ConversationApi.get(params);
|
||||
buildConversationList(
|
||||
{ commit, dispatch },
|
||||
params,
|
||||
data,
|
||||
params.assigneeType
|
||||
);
|
||||
dispatch(
|
||||
'conversationPage/setCurrentPage',
|
||||
{ filter: params.assigneeType, page: params.page },
|
||||
{ root: true }
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
}
|
||||
},
|
||||
|
||||
fetchFilteredConversations: async ({ commit, dispatch }, params) => {
|
||||
commit(types.SET_LIST_LOADING_STATUS);
|
||||
try {
|
||||
const { data } = await ConversationApi.filter(params);
|
||||
buildConversationList(
|
||||
{ commit, dispatch },
|
||||
params,
|
||||
data,
|
||||
'appliedFilters'
|
||||
);
|
||||
if (!chatList.length) {
|
||||
dispatch(
|
||||
'conversationPage/setEndReached',
|
||||
{ filter: params.assigneeType },
|
||||
{ root: true }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
}
|
||||
|
@ -198,12 +234,15 @@ const actions = {
|
|||
},
|
||||
|
||||
addConversation({ commit, state, dispatch }, conversation) {
|
||||
const { currentInbox } = state;
|
||||
const { currentInbox, appliedFilters } = state;
|
||||
const {
|
||||
inbox_id: inboxId,
|
||||
meta: { sender },
|
||||
} = conversation;
|
||||
if (!currentInbox || Number(currentInbox) === inboxId) {
|
||||
const hasAppliedFilters = !!appliedFilters.length;
|
||||
const isMatchingInboxFilter =
|
||||
!currentInbox || Number(currentInbox) === inboxId;
|
||||
if (!hasAppliedFilters && isMatchingInboxFilter) {
|
||||
commit(types.ADD_CONVERSATION, conversation);
|
||||
dispatch('contacts/setContact', sender);
|
||||
}
|
||||
|
@ -288,6 +327,14 @@ const actions = {
|
|||
// Handle error
|
||||
}
|
||||
},
|
||||
|
||||
setConversationFilters({ commit }, data) {
|
||||
commit(types.SET_CONVERSATION_FILTERS, data);
|
||||
},
|
||||
|
||||
clearConversationFilters({ commit }) {
|
||||
commit(types.CLEAR_CONVERSATION_FILTERS);
|
||||
},
|
||||
};
|
||||
|
||||
export default actions;
|
||||
|
|
|
@ -31,6 +31,9 @@ const getters = {
|
|||
return isChatMine;
|
||||
});
|
||||
},
|
||||
getAppliedFilters: _state => {
|
||||
return _state.appliedFilters;
|
||||
},
|
||||
getUnAssignedChats: _state => activeFilters => {
|
||||
return _state.allConversations.filter(conversation => {
|
||||
const isUnAssigned = !conversation.meta.assignee;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import * as types from '../../mutation-types';
|
||||
import types from '../../mutation-types';
|
||||
import getters, { getSelectedChatConversation } from './getters';
|
||||
import actions from './actions';
|
||||
import { findPendingMessageIndex } from './helpers';
|
||||
|
@ -11,11 +11,12 @@ const state = {
|
|||
chatStatusFilter: wootConstants.STATUS_TYPE.OPEN,
|
||||
currentInbox: null,
|
||||
selectedChatId: null,
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
// mutations
|
||||
export const mutations = {
|
||||
[types.default.SET_ALL_CONVERSATION](_state, conversationList) {
|
||||
[types.SET_ALL_CONVERSATION](_state, conversationList) {
|
||||
const newAllConversations = [..._state.allConversations];
|
||||
conversationList.forEach(conversation => {
|
||||
const indexInCurrentList = newAllConversations.findIndex(
|
||||
|
@ -27,47 +28,47 @@ export const mutations = {
|
|||
});
|
||||
_state.allConversations = newAllConversations;
|
||||
},
|
||||
[types.default.EMPTY_ALL_CONVERSATION](_state) {
|
||||
[types.EMPTY_ALL_CONVERSATION](_state) {
|
||||
_state.allConversations = [];
|
||||
_state.selectedChatId = null;
|
||||
},
|
||||
[types.default.SET_ALL_MESSAGES_LOADED](_state) {
|
||||
[types.SET_ALL_MESSAGES_LOADED](_state) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat, 'allMessagesLoaded', true);
|
||||
},
|
||||
|
||||
[types.default.CLEAR_ALL_MESSAGES_LOADED](_state) {
|
||||
[types.CLEAR_ALL_MESSAGES_LOADED](_state) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat, 'allMessagesLoaded', false);
|
||||
},
|
||||
[types.default.CLEAR_CURRENT_CHAT_WINDOW](_state) {
|
||||
[types.CLEAR_CURRENT_CHAT_WINDOW](_state) {
|
||||
_state.selectedChatId = null;
|
||||
},
|
||||
|
||||
[types.default.SET_PREVIOUS_CONVERSATIONS](_state, { id, data }) {
|
||||
[types.SET_PREVIOUS_CONVERSATIONS](_state, { id, data }) {
|
||||
if (data.length) {
|
||||
const [chat] = _state.allConversations.filter(c => c.id === id);
|
||||
chat.messages.unshift(...data);
|
||||
}
|
||||
},
|
||||
|
||||
[types.default.SET_CURRENT_CHAT_WINDOW](_state, activeChat) {
|
||||
[types.SET_CURRENT_CHAT_WINDOW](_state, activeChat) {
|
||||
if (activeChat) {
|
||||
_state.selectedChatId = activeChat.id;
|
||||
}
|
||||
},
|
||||
|
||||
[types.default.ASSIGN_AGENT](_state, assignee) {
|
||||
[types.ASSIGN_AGENT](_state, assignee) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat.meta, 'assignee', assignee);
|
||||
},
|
||||
|
||||
[types.default.ASSIGN_TEAM](_state, team) {
|
||||
[types.ASSIGN_TEAM](_state, team) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat.meta, 'team', team);
|
||||
},
|
||||
|
||||
[types.default.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES](
|
||||
[types.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES](
|
||||
_state,
|
||||
custom_attributes
|
||||
) {
|
||||
|
@ -75,7 +76,7 @@ export const mutations = {
|
|||
Vue.set(chat, 'custom_attributes', custom_attributes);
|
||||
},
|
||||
|
||||
[types.default.CHANGE_CONVERSATION_STATUS](
|
||||
[types.CHANGE_CONVERSATION_STATUS](
|
||||
_state,
|
||||
{ conversationId, status, snoozedUntil }
|
||||
) {
|
||||
|
@ -85,17 +86,17 @@ export const mutations = {
|
|||
Vue.set(conversation, 'status', status);
|
||||
},
|
||||
|
||||
[types.default.MUTE_CONVERSATION](_state) {
|
||||
[types.MUTE_CONVERSATION](_state) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat, 'muted', true);
|
||||
},
|
||||
|
||||
[types.default.UNMUTE_CONVERSATION](_state) {
|
||||
[types.UNMUTE_CONVERSATION](_state) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat, 'muted', false);
|
||||
},
|
||||
|
||||
[types.default.ADD_MESSAGE]({ allConversations, selectedChatId }, message) {
|
||||
[types.ADD_MESSAGE]({ allConversations, selectedChatId }, message) {
|
||||
const { conversation_id: conversationId } = message;
|
||||
const [chat] = getSelectedChatConversation({
|
||||
allConversations,
|
||||
|
@ -115,11 +116,11 @@ export const mutations = {
|
|||
}
|
||||
},
|
||||
|
||||
[types.default.ADD_CONVERSATION](_state, conversation) {
|
||||
[types.ADD_CONVERSATION](_state, conversation) {
|
||||
_state.allConversations.push(conversation);
|
||||
},
|
||||
|
||||
[types.default.UPDATE_CONVERSATION](_state, conversation) {
|
||||
[types.UPDATE_CONVERSATION](_state, conversation) {
|
||||
const { allConversations } = _state;
|
||||
const currentConversationIndex = allConversations.findIndex(
|
||||
c => c.id === conversation.id
|
||||
|
@ -139,32 +140,32 @@ export const mutations = {
|
|||
}
|
||||
},
|
||||
|
||||
[types.default.SET_LIST_LOADING_STATUS](_state) {
|
||||
[types.SET_LIST_LOADING_STATUS](_state) {
|
||||
_state.listLoadingStatus = true;
|
||||
},
|
||||
|
||||
[types.default.CLEAR_LIST_LOADING_STATUS](_state) {
|
||||
[types.CLEAR_LIST_LOADING_STATUS](_state) {
|
||||
_state.listLoadingStatus = false;
|
||||
},
|
||||
|
||||
[types.default.MARK_MESSAGE_READ](_state, { id, lastSeen }) {
|
||||
[types.MARK_MESSAGE_READ](_state, { id, lastSeen }) {
|
||||
const [chat] = _state.allConversations.filter(c => c.id === id);
|
||||
if (chat) {
|
||||
chat.agent_last_seen_at = lastSeen;
|
||||
}
|
||||
},
|
||||
|
||||
[types.default.CHANGE_CHAT_STATUS_FILTER](_state, data) {
|
||||
[types.CHANGE_CHAT_STATUS_FILTER](_state, data) {
|
||||
_state.chatStatusFilter = data;
|
||||
},
|
||||
|
||||
// Update assignee on action cable message
|
||||
[types.default.UPDATE_ASSIGNEE](_state, payload) {
|
||||
[types.UPDATE_ASSIGNEE](_state, payload) {
|
||||
const [chat] = _state.allConversations.filter(c => c.id === payload.id);
|
||||
Vue.set(chat.meta, 'assignee', payload.assignee);
|
||||
},
|
||||
|
||||
[types.default.UPDATE_CONVERSATION_CONTACT](
|
||||
[types.UPDATE_CONVERSATION_CONTACT](
|
||||
_state,
|
||||
{ conversationId, ...payload }
|
||||
) {
|
||||
|
@ -174,11 +175,11 @@ export const mutations = {
|
|||
}
|
||||
},
|
||||
|
||||
[types.default.SET_ACTIVE_INBOX](_state, inboxId) {
|
||||
[types.SET_ACTIVE_INBOX](_state, inboxId) {
|
||||
_state.currentInbox = inboxId ? parseInt(inboxId, 10) : null;
|
||||
},
|
||||
|
||||
[types.default.SET_CONVERSATION_CAN_REPLY](
|
||||
[types.SET_CONVERSATION_CAN_REPLY](
|
||||
_state,
|
||||
{ conversationId, canReply }
|
||||
) {
|
||||
|
@ -188,12 +189,20 @@ export const mutations = {
|
|||
}
|
||||
},
|
||||
|
||||
[types.default.CLEAR_CONTACT_CONVERSATIONS](_state, contactId) {
|
||||
[types.CLEAR_CONTACT_CONVERSATIONS](_state, contactId) {
|
||||
const chats = _state.allConversations.filter(
|
||||
c => c.meta.sender.id !== contactId
|
||||
);
|
||||
Vue.set(_state, 'allConversations', chats);
|
||||
},
|
||||
|
||||
[types.SET_CONVERSATION_FILTERS](_state, data) {
|
||||
_state.appliedFilters = data;
|
||||
},
|
||||
|
||||
[types.CLEAR_CONVERSATION_FILTERS](_state) {
|
||||
_state.appliedFilters = [];
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -58,6 +58,11 @@ describe('#getters', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('get all campaigns', () => {
|
||||
const state = { records: campaigns };
|
||||
expect(getters.getAllCampaigns(state)).toEqual(campaigns);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
|
|
|
@ -23,8 +23,13 @@ describe('#mutations', () => {
|
|||
};
|
||||
mutations[types.default.CLEAR_CONVERSATION_PAGE](state);
|
||||
expect(state).toEqual({
|
||||
currentPage: { me: 0, unassigned: 0, all: 0 },
|
||||
hasEndReached: { me: false, unassigned: false, all: false },
|
||||
currentPage: { me: 0, unassigned: 0, all: 0, appliedFilters: 0 },
|
||||
hasEndReached: {
|
||||
me: false,
|
||||
unassigned: false,
|
||||
all: false,
|
||||
appliedFilters: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
import axios from 'axios';
|
||||
import actions from '../../conversations/actions';
|
||||
import types from '../../../mutation-types';
|
||||
const dataToSend = {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
query_operator: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
import { dataReceived } from './testConversationResponse';
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
@ -73,7 +84,30 @@ describe('#actions', () => {
|
|||
inbox_id: 2,
|
||||
};
|
||||
actions.addConversation(
|
||||
{ commit, dispatch, state: { currentInbox: 1 } },
|
||||
{
|
||||
commit,
|
||||
dispatch,
|
||||
state: { currentInbox: 1, appliedFilters: [] },
|
||||
},
|
||||
conversation
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('doesnot send mutation if conversation filters are applied', () => {
|
||||
const conversation = {
|
||||
id: 1,
|
||||
messages: [],
|
||||
meta: { sender: { id: 1, name: 'john-doe' } },
|
||||
inbox_id: 1,
|
||||
};
|
||||
actions.addConversation(
|
||||
{
|
||||
commit,
|
||||
dispatch,
|
||||
state: { currentInbox: 1, appliedFilters: [{ id: 'random-filter' }] },
|
||||
},
|
||||
conversation
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
|
@ -88,7 +122,11 @@ describe('#actions', () => {
|
|||
inbox_id: 1,
|
||||
};
|
||||
actions.addConversation(
|
||||
{ commit, dispatch, state: { currentInbox: 1 } },
|
||||
{
|
||||
commit,
|
||||
dispatch,
|
||||
state: { currentInbox: 1, appliedFilters: [] },
|
||||
},
|
||||
conversation
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
|
@ -112,7 +150,10 @@ describe('#actions', () => {
|
|||
meta: { sender: { id: 1, name: 'john-doe' } },
|
||||
inbox_id: 1,
|
||||
};
|
||||
actions.addConversation({ commit, dispatch, state: {} }, conversation);
|
||||
actions.addConversation(
|
||||
{ commit, dispatch, state: { appliedFilters: [] } },
|
||||
conversation
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.ADD_CONVERSATION, conversation],
|
||||
]);
|
||||
|
@ -262,6 +303,44 @@ describe('#actions', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchFilteredConversations', () => {
|
||||
it('fetches filtered conversations with a mock commit', async () => {
|
||||
axios.post.mockResolvedValue({
|
||||
data: dataReceived,
|
||||
});
|
||||
await actions.fetchFilteredConversations({ commit }, dataToSend);
|
||||
expect(commit).toHaveBeenCalledTimes(2);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_LIST_LOADING_STATUS'],
|
||||
['SET_ALL_CONVERSATION', dataReceived.payload],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setConversationFilter', () => {
|
||||
it('commits the correct mutation and sets filter state', () => {
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'snoozed', name: 'Snoozed' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
actions.setConversationFilters({ commit }, filters);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONVERSATION_FILTERS, filters],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clearConversationFilter', () => {
|
||||
it('commits the correct mutation and clears filter state', () => {
|
||||
actions.clearConversationFilters({ commit });
|
||||
expect(commit.mock.calls).toEqual([[types.CLEAR_CONVERSATION_FILTERS]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteMessage', () => {
|
||||
|
|
|
@ -114,4 +114,21 @@ describe('#getters', () => {
|
|||
expect(getters.getConversationById(state)(1)).toEqual({ id: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAppliedFilters', () => {
|
||||
it('getAppliedFilters', () => {
|
||||
const filtersList = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'snoozed', name: 'Snoozed' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
const state = {
|
||||
appliedFilters: filtersList,
|
||||
};
|
||||
expect(getters.getAppliedFilters(state)).toEqual(filtersList);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -201,4 +201,43 @@ describe('#mutations', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_CONVERSATION_FILTERS', () => {
|
||||
it('set conversation filter', () => {
|
||||
const appliedFilters = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'snoozed', name: 'Snoozed' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
mutations[types.SET_CONVERSATION_FILTERS](appliedFilters);
|
||||
expect(appliedFilters).toEqual([
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'snoozed', name: 'Snoozed' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_CONVERSATION_FILTERS', () => {
|
||||
it('clears applied conversation filters', () => {
|
||||
const state = {
|
||||
appliedFilters: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'snoozed', name: 'Snoozed' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
};
|
||||
mutations[types.CLEAR_CONVERSATION_FILTERS](state);
|
||||
expect(state.appliedFilters).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
export const dataReceived = {
|
||||
meta: {
|
||||
mine_count: 3,
|
||||
unassigned_count: 0,
|
||||
all_count: 4,
|
||||
},
|
||||
payload: [
|
||||
{
|
||||
meta: {
|
||||
sender: {
|
||||
additional_attributes: {},
|
||||
availability_status: 'offline',
|
||||
email: null,
|
||||
id: 40,
|
||||
name: 'damp-field-834',
|
||||
phone_number: null,
|
||||
identifier: null,
|
||||
thumbnail: '',
|
||||
custom_attributes: {},
|
||||
last_activity_at: 1635764106,
|
||||
},
|
||||
channel: 'Channel::WebWidget',
|
||||
assignee: {
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@acme.inc',
|
||||
available_name: 'John',
|
||||
name: 'John',
|
||||
role: 'administrator',
|
||||
thumbnail:
|
||||
'http://0.0.0.0:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--318d40b3d34e02760df9f4ea0c5c89d1f590dda4/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/profile-pic.png',
|
||||
},
|
||||
},
|
||||
id: 10,
|
||||
messages: [
|
||||
{
|
||||
id: 85,
|
||||
content: 'Ok',
|
||||
account_id: 1,
|
||||
inbox_id: 6,
|
||||
conversation_id: 10,
|
||||
message_type: 1,
|
||||
created_at: 1635764265,
|
||||
updated_at: '2021-11-01T10:57:45.790Z',
|
||||
private: false,
|
||||
status: 'sent',
|
||||
source_id: null,
|
||||
content_type: 'text',
|
||||
content_attributes: {},
|
||||
sender_type: 'User',
|
||||
sender_id: 1,
|
||||
external_source_ids: {},
|
||||
sender: {
|
||||
id: 1,
|
||||
name: 'John',
|
||||
available_name: 'John',
|
||||
avatar_url:
|
||||
'http://0.0.0.0:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--318d40b3d34e02760df9f4ea0c5c89d1f590dda4/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/profile-pic.png',
|
||||
type: 'user',
|
||||
availability_status: 'online',
|
||||
thumbnail:
|
||||
'http://0.0.0.0:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--318d40b3d34e02760df9f4ea0c5c89d1f590dda4/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/profile-pic.png',
|
||||
},
|
||||
},
|
||||
],
|
||||
account_id: 1,
|
||||
additional_attributes: {
|
||||
browser: {
|
||||
device_name: 'Unknown',
|
||||
browser_name: 'Chrome',
|
||||
platform_name: 'macOS',
|
||||
browser_version: '95.0.4638.54',
|
||||
platform_version: '10.15.7',
|
||||
},
|
||||
referer: 'http://localhost:3000/widget_tests',
|
||||
initiated_at: {
|
||||
timestamp: 'Mon Nov 01 2021 16:25:06 GMT+0530 (India Standard Time)',
|
||||
},
|
||||
},
|
||||
agent_last_seen_at: 1635846359,
|
||||
assignee_last_seen_at: 1635846359,
|
||||
can_reply: true,
|
||||
contact_last_seen_at: 1635764265,
|
||||
custom_attributes: {},
|
||||
inbox_id: 6,
|
||||
labels: [],
|
||||
muted: false,
|
||||
snoozed_until: null,
|
||||
status: 'open',
|
||||
timestamp: 1635764265,
|
||||
unread_count: 0,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -19,6 +19,8 @@ export default {
|
|||
UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE',
|
||||
UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT',
|
||||
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
|
||||
SET_CONVERSATION_FILTERS: 'SET_CONVERSATION_FILTERS',
|
||||
CLEAR_CONVERSATION_FILTERS: 'CLEAR_CONVERSATION_FILTERS',
|
||||
|
||||
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
|
||||
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
:root {
|
||||
--font-size-nano: 0.8rem;
|
||||
--font-size-micro: 1.0rem;
|
||||
--font-size-micro: 1rem;
|
||||
--font-size-mini: 1.2rem;
|
||||
--font-size-small: 1.4rem;
|
||||
--font-size-one-and-half: 1.5rem;
|
||||
--font-size-default: 1.6rem;
|
||||
--font-size-medium: 1.8rem;
|
||||
--font-size-two: 2rem;
|
||||
--font-size-large: 2.2rem;
|
||||
--font-size-big: 2.4rem;
|
||||
--font-size-bigger: 3.0rem;
|
||||
--font-size-bigger: 3rem;
|
||||
--font-size-mega: 3.4rem;
|
||||
--font-size-giga: 4.0rem;
|
||||
--font-size-giga: 4rem;
|
||||
}
|
||||
|
|
|
@ -2,17 +2,21 @@
|
|||
// spaces
|
||||
--space-zero: 0;
|
||||
--space-micro: 0.2rem;
|
||||
--space-one-fourths: 0.25rem;
|
||||
--space-smaller: 0.4rem;
|
||||
--space-half: 0.5rem;
|
||||
--space-three-fourths: 0.75rem;
|
||||
--space-small: 0.8rem;
|
||||
--space-one: 1rem;
|
||||
--space-slab: 1.2rem;
|
||||
--space-normal: 1.6rem;
|
||||
--space-two: 2.0rem;
|
||||
--space-two: 2rem;
|
||||
--space-medium: 2.4rem;
|
||||
--space-three: 3rem;
|
||||
--space-large: 3.2rem;
|
||||
--space-larger: 4.8rem;
|
||||
--space-jumbo: 6.4rem;
|
||||
--space-mega: 10.0rem;
|
||||
--space-mega: 10rem;
|
||||
|
||||
--space-minus-micro: -0.2rem;
|
||||
--space-minus-smaller: -0.4rem;
|
||||
|
@ -20,10 +24,10 @@
|
|||
--space-minus-one: -1rem;
|
||||
--space-minus-slab: -1.2rem;
|
||||
--space-minus-normal: -1.6rem;
|
||||
--space-minus-two: -2.0rem;
|
||||
--space-minus-two: -2rem;
|
||||
--space-minus-medium: -2.4rem;
|
||||
--space-minus-large: -3.2rem;
|
||||
--space-minus-larger: -4.8rem;
|
||||
--space-minus-jumbo: -6.4rem;
|
||||
--space-minus-mega: -10.0rem;
|
||||
--space-minus-mega: -10rem;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
--z-index-minus: -1;
|
||||
--z-index-zero: 0;
|
||||
--z-index-low: 10;
|
||||
--z-index-twenty: 20;
|
||||
--z-index-normal: 100;
|
||||
--z-index-high: 1000;
|
||||
--z-index-higher: 2000;
|
||||
|
|
Loading…
Reference in a new issue