feat: Add the ability to save filters for contact (#3791)

This commit is contained in:
Sivin Varghese 2022-01-22 03:41:59 +05:30 committed by GitHub
parent 693f2531ab
commit 504fc24fb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 203 additions and 31 deletions

View file

@ -6,8 +6,8 @@ class CustomViewsAPI extends ApiClient {
super('custom_filters', { accountScoped: true }); super('custom_filters', { accountScoped: true });
} }
getCustomViews() { getCustomViewsByFilterType(type) {
return axios.get(this.url); return axios.get(`${this.url}?filter_type=${type}`);
} }
} }

View file

@ -88,12 +88,31 @@ export default {
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get', globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes', inboxes: 'inboxes/getInboxes',
customViews: 'customViews/getCustomViews',
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole', currentRole: 'getCurrentRole',
labels: 'labels/getLabelsOnSidebar', labels: 'labels/getLabelsOnSidebar',
teams: 'teams/getMyTeams', teams: 'teams/getMyTeams',
}), }),
activeCustomView() {
if (this.activePrimaryMenu.key === 'contacts') {
return 'contact';
}
if (this.activePrimaryMenu.key === 'conversations') {
return 'conversation';
}
return '';
},
customViews() {
return this.$store.getters['customViews/getCustomViewsByFilterType'](
this.activeCustomView
);
},
isConversationOrContactActive() {
return (
this.activePrimaryMenu.key === 'contacts' ||
this.activePrimaryMenu.key === 'conversations'
);
},
sideMenuConfig() { sideMenuConfig() {
return getSidebarItems(this.accountId); return getSidebarItems(this.accountId);
}, },
@ -121,16 +140,27 @@ export default {
return activePrimaryMenu; return activePrimaryMenu;
}, },
}, },
watch: {
activeCustomView() {
this.fetchCustomViews();
},
},
mounted() { mounted() {
this.$store.dispatch('labels/get'); this.$store.dispatch('labels/get');
this.$store.dispatch('inboxes/get'); this.$store.dispatch('inboxes/get');
this.$store.dispatch('customViews/get');
this.$store.dispatch('notifications/unReadCount'); this.$store.dispatch('notifications/unReadCount');
this.$store.dispatch('teams/get'); this.$store.dispatch('teams/get');
this.$store.dispatch('attributes/get'); this.$store.dispatch('attributes/get');
this.fetchCustomViews();
}, },
methods: { methods: {
fetchCustomViews() {
if (this.isConversationOrContactActive) {
this.$store.dispatch('customViews/get', this.activeCustomView);
}
},
toggleKeyShortcutModal() { toggleKeyShortcutModal() {
this.showShortcutModal = true; this.showShortcutModal = true;
}, },

View file

@ -5,6 +5,7 @@ const contacts = accountId => ({
routes: [ routes: [
'contacts_dashboard', 'contacts_dashboard',
'contact_profile_dashboard', 'contact_profile_dashboard',
'contacts_through_custom_view',
'contacts_labels_dashboard', 'contacts_labels_dashboard',
], ],
menuItems: [ menuItems: [

View file

@ -57,6 +57,9 @@ export default {
hasSecondaryMenu() { hasSecondaryMenu() {
return this.menuConfig.menuItems && this.menuConfig.menuItems.length; return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
}, },
contactCustomViews() {
return this.customViews.filter(view => view.filter_type === 'contact');
},
accessibleMenuItems() { accessibleMenuItems() {
if (!this.currentRole) { if (!this.currentRole) {
return []; return [];
@ -157,7 +160,7 @@ export default {
customViewsSection() { customViewsSection() {
return { return {
icon: 'folder', icon: 'folder',
label: 'CUSTOM_VIEWS', label: 'CUSTOM_VIEWS_FOLDER',
hasSubMenu: true, hasSubMenu: true,
key: 'custom_view', key: 'custom_view',
children: this.customViews children: this.customViews
@ -172,8 +175,27 @@ export default {
})), })),
}; };
}, },
contactCustomViewsSection() {
return {
icon: 'folder',
label: 'CUSTOM_VIEWS_SEGMENTS',
hasSubMenu: true,
key: 'custom_view',
children: this.customViews
.filter(view => view.filter_type === 'contact')
.map(view => ({
id: view.id,
label: view.name,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/contacts/custom_view/${view.id}`
),
})),
};
},
additionalSecondaryMenuItems() { additionalSecondaryMenuItems() {
let conversationMenuItems = [this.inboxSection, this.labelSection]; let conversationMenuItems = [this.inboxSection, this.labelSection];
let contactMenuItems = [this.contactLabelSection];
if (this.teams.length) { if (this.teams.length) {
conversationMenuItems = [this.teamSection, ...conversationMenuItems]; conversationMenuItems = [this.teamSection, ...conversationMenuItems];
} }
@ -183,9 +205,15 @@ export default {
...conversationMenuItems, ...conversationMenuItems,
]; ];
} }
if (this.contactCustomViews.length) {
contactMenuItems = [
this.contactCustomViewsSection,
...contactMenuItems,
];
}
return { return {
conversations: conversationMenuItems, conversations: conversationMenuItems,
contacts: [this.contactLabelSection], contacts: contactMenuItems,
}; };
}, },
}, },

View file

@ -179,6 +179,7 @@
"SEARCH_BUTTON": "Search", "SEARCH_BUTTON": "Search",
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts", "SEARCH_INPUT_PLACEHOLDER": "Search for contacts",
"FILTER_CONTACTS": "Filter", "FILTER_CONTACTS": "Filter",
"FILTER_CONTACTS_SAVE": "Save filter",
"LIST": { "LIST": {
"LOADING_MESSAGE": "Loading contacts...", "LOADING_MESSAGE": "Loading contacts...",
"404": "No contacts matches your search 🔍", "404": "No contacts matches your search 🔍",

View file

@ -149,7 +149,8 @@
"CUSTOM_ATTRIBUTES": "Custom Attributes", "CUSTOM_ATTRIBUTES": "Custom Attributes",
"AUTOMATION": "Automation", "AUTOMATION": "Automation",
"TEAMS": "Teams", "TEAMS": "Teams",
"CUSTOM_VIEWS": "Folders", "CUSTOM_VIEWS_FOLDER": "Folders",
"CUSTOM_VIEWS_SEGMENTS": "Segments",
"ALL_CONTACTS": "All Contacts", "ALL_CONTACTS": "All Contacts",
"TAGGED_WITH": "Tagged with", "TAGGED_WITH": "Tagged with",
"NEW_LABEL": "New label", "NEW_LABEL": "New label",

View file

@ -3,13 +3,15 @@
<div class="left-wrap" :class="wrapClas"> <div class="left-wrap" :class="wrapClas">
<contacts-header <contacts-header
:search-query="searchQuery" :search-query="searchQuery"
:custom-views-id="customViewsId"
:on-search-submit="onSearchSubmit" :on-search-submit="onSearchSubmit"
this-selected-contact-id="" this-selected-contact-id=""
:on-input-search="onInputSearch" :on-input-search="onInputSearch"
:on-toggle-create="onToggleCreate" :on-toggle-create="onToggleCreate"
:on-toggle-import="onToggleImport" :on-toggle-import="onToggleImport"
:on-toggle-filter="onToggleFilters" :on-toggle-filter="onToggleFilters"
:header-title="label" :header-title="pageTitle"
@on-toggle-save-filter="onToggleSaveFilters"
/> />
<contacts-table <contacts-table
:contacts="records" :contacts="records"
@ -26,6 +28,12 @@
:total-count="meta.count" :total-count="meta.count"
/> />
</div> </div>
<add-custom-views
v-if="showAddCustomViewsModal"
:custom-views-query="customViewsQuery"
:filter-type="filterType"
@close="onCloseAddCustomViewsModal"
/>
<contact-info-panel <contact-info-panel
v-if="showContactViewPane" v-if="showContactViewPane"
:contact="selectedContact" :contact="selectedContact"
@ -63,8 +71,10 @@ import ImportContacts from './ImportContacts.vue';
import ContactsAdvancedFilters from './ContactsAdvancedFilters.vue'; import ContactsAdvancedFilters from './ContactsAdvancedFilters.vue';
import contactFilterItems from '../contactFilterItems'; import contactFilterItems from '../contactFilterItems';
import filterQueryGenerator from '../../../../helper/filterQueryGenerator'; import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
const DEFAULT_PAGE = 1; const DEFAULT_PAGE = 1;
const FILTER_TYPE_CONTACT = 1;
export default { export default {
components: { components: {
@ -75,9 +85,14 @@ export default {
CreateContact, CreateContact,
ImportContacts, ImportContacts,
ContactsAdvancedFilters, ContactsAdvancedFilters,
AddCustomViews,
}, },
props: { props: {
label: { type: String, default: '' }, label: { type: String, default: '' },
customViewsId: {
type: [String, Number],
default: 0,
},
}, },
data() { data() {
return { return {
@ -93,6 +108,9 @@ export default {
`CONTACTS_FILTER.ATTRIBUTES.${filter.attributeI18nKey}` `CONTACTS_FILTER.ATTRIBUTES.${filter.attributeI18nKey}`
), ),
})), })),
customViewsQuery: {},
filterType: FILTER_TYPE_CONTACT,
showAddCustomViewsModal: false,
}; };
}, },
computed: { computed: {
@ -100,11 +118,28 @@ export default {
records: 'contacts/getContacts', records: 'contacts/getContacts',
uiFlags: 'contacts/getUIFlags', uiFlags: 'contacts/getUIFlags',
meta: 'contacts/getMeta', meta: 'contacts/getMeta',
customViews: 'customViews/getCustomViews',
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
}), }),
showEmptySearchResult() { showEmptySearchResult() {
const hasEmptyResults = !!this.searchQuery && this.records.length === 0; const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
return hasEmptyResults; return hasEmptyResults;
}, },
hasAppliedFilters() {
return this.getAppliedContactFilters.length;
},
hasActiveCustomViews() {
return this.activeCustomView && this.customViewsId !== 0;
},
pageTitle() {
if (this.hasActiveCustomViews) {
return this.activeCustomView.name;
}
if (this.label) {
return `#${this.label}`;
}
return this.$t('CONTACTS_PAGE.HEADER');
},
selectedContact() { selectedContact() {
if (this.selectedContactId) { if (this.selectedContactId) {
const contact = this.records.find( const contact = this.records.find(
@ -127,11 +162,28 @@ export default {
? selectedPageNumber ? selectedPageNumber
: DEFAULT_PAGE; : DEFAULT_PAGE;
}, },
activeCustomView() {
if (this.customViewsId) {
const [firstValue] = this.customViews.filter(
view => view.id === Number(this.customViewsId)
);
return firstValue;
}
return undefined;
},
}, },
watch: { watch: {
label() { label() {
this.fetchContacts(DEFAULT_PAGE); this.fetchContacts(DEFAULT_PAGE);
}, },
activeCustomView() {
if (this.hasActiveCustomViews) {
const payload = this.activeCustomView.query;
this.fetchSavedFilteredContact(payload);
} else {
this.fetchContacts(DEFAULT_PAGE);
}
},
}, },
mounted() { mounted() {
this.fetchContacts(this.pageParameter); this.fetchContacts(this.pageParameter);
@ -177,6 +229,14 @@ export default {
}); });
} }
}, },
fetchSavedFilteredContact(payload) {
if (this.hasAppliedFilters) {
this.clearFilters();
}
this.$store.dispatch('contacts/filter', {
queryPayload: payload,
});
},
onInputSearch(event) { onInputSearch(event) {
const newQuery = event.target.value; const newQuery = event.target.value;
const refetchAllContacts = !!this.searchQuery && newQuery === ''; const refetchAllContacts = !!this.searchQuery && newQuery === '';
@ -206,6 +266,12 @@ export default {
onToggleCreate() { onToggleCreate() {
this.showCreateModal = !this.showCreateModal; this.showCreateModal = !this.showCreateModal;
}, },
onToggleSaveFilters() {
this.showAddCustomViewsModal = true;
},
onCloseAddCustomViewsModal() {
this.showAddCustomViewsModal = false;
},
onToggleImport() { onToggleImport() {
this.showImportModal = !this.showImportModal; this.showImportModal = !this.showImportModal;
}, },
@ -218,6 +284,7 @@ export default {
}, },
onApplyFilter(payload) { onApplyFilter(payload) {
this.closeContactInfoPanel(); this.closeContactInfoPanel();
this.customViewsQuery = { payload };
this.$store.dispatch('contacts/filter', { this.$store.dispatch('contacts/filter', {
queryPayload: filterQueryGenerator(payload), queryPayload: filterQueryGenerator(payload),
}); });

View file

@ -3,7 +3,7 @@
<div class="table-actions-wrap"> <div class="table-actions-wrap">
<div class="left-aligned-wrap"> <div class="left-aligned-wrap">
<h1 class="page-title"> <h1 class="page-title">
{{ headerTitle ? `#${headerTitle}` : $t('CONTACTS_PAGE.HEADER') }} {{ headerTitle }}
</h1> </h1>
</div> </div>
<div class="right-aligned-wrap"> <div class="right-aligned-wrap">
@ -26,7 +26,7 @@
{{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }} {{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }}
</woot-button> </woot-button>
</div> </div>
<div class="filters__button-wrap"> <div v-if="!hasActiveCustomViews" class="filters__button-wrap">
<div v-if="hasAppliedFilters" class="filters__applied-indicator" /> <div v-if="hasAppliedFilters" class="filters__applied-indicator" />
<woot-button <woot-button
class="margin-right-small clear" class="margin-right-small clear"
@ -38,6 +38,16 @@
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS') }} {{ $t('CONTACTS_PAGE.FILTER_CONTACTS') }}
</woot-button> </woot-button>
</div> </div>
<woot-button
v-if="hasAppliedFilters && !hasActiveCustomViews"
class="margin-right-small clear"
color-scheme="alert"
variant="clear"
icon="save"
@click="onToggleCustomViewsModal"
>
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_SAVE') }}
</woot-button>
<woot-button <woot-button
class="margin-right-small clear" class="margin-right-small clear"
color-scheme="success" color-scheme="success"
@ -74,6 +84,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
customViewsId: {
type: [String, Number],
default: 0,
},
onInputSearch: { onInputSearch: {
type: Function, type: Function,
default: () => {}, default: () => {},
@ -111,6 +125,14 @@ export default {
hasAppliedFilters() { hasAppliedFilters() {
return this.getAppliedContactFilters.length; return this.getAppliedContactFilters.length;
}, },
hasActiveCustomViews() {
return this.customViewsId !== 0;
},
},
methods: {
onToggleCustomViewsModal() {
this.$emit('on-toggle-save-filter');
},
}, },
}; };
</script> </script>

View file

@ -10,6 +10,15 @@ export const routes = [
roles: ['administrator', 'agent'], roles: ['administrator', 'agent'],
component: ContactsView, component: ContactsView,
}, },
{
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
name: 'contacts_through_custom_view',
roles: ['administrator', 'agent'],
component: ContactsView,
props: route => {
return { customViewsId: route.params.id };
},
},
{ {
path: frontendURL('accounts/:accountId/labels/:label/contacts'), path: frontendURL('accounts/:accountId/labels/:label/contacts'),
name: 'contacts_labels_dashboard', name: 'contacts_labels_dashboard',

View file

@ -18,13 +18,18 @@ export const getters = {
getCustomViews(_state) { getCustomViews(_state) {
return _state.records; return _state.records;
}, },
getCustomViewsByFilterType: _state => filterType => {
return _state.records.filter(record => record.filter_type === filterType);
},
}; };
export const actions = { export const actions = {
get: async function getCustomViews({ commit }) { get: async function getCustomViews({ commit }, filterType) {
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true }); commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isFetching: true });
try { try {
const response = await CustomViewsAPI.getCustomViews(); const response = await CustomViewsAPI.getCustomViewsByFilterType(
filterType
);
commit(types.SET_CUSTOM_VIEW, response.data); commit(types.SET_CUSTOM_VIEW, response.data);
} catch (error) { } catch (error) {
// Ignore error // Ignore error

View file

@ -1,7 +1,7 @@
export default [ export default [
{ {
name: 'Custom view', name: 'Custom view',
filter_type: 'conversation', filter_type: 0,
query: { query: {
payload: [ payload: [
{ {
@ -21,20 +21,14 @@ export default [
}, },
{ {
name: 'Custom view 1', name: 'Custom view 1',
filter_type: 'conversation', filter_type: 1,
query: { query: {
payload: [ payload: [
{ {
attribute_key: 'browser_language', attribute_key: 'name',
filter_operator: 'equal_to', filter_operator: 'equal_to',
values: ['eng'], values: ['john doe'],
query_operator: 'or', query_operator: null,
},
{
attribute_key: 'campaign_id',
filter_operator: 'equal_to',
values: [15],
query_operator: 'and',
}, },
], ],
}, },

View file

@ -7,7 +7,7 @@ describe('#getters', () => {
expect(getters.getCustomViews(state)).toEqual([ expect(getters.getCustomViews(state)).toEqual([
{ {
name: 'Custom view', name: 'Custom view',
filter_type: 'conversation', filter_type: 0,
query: { query: {
payload: [ payload: [
{ {
@ -27,20 +27,34 @@ describe('#getters', () => {
}, },
{ {
name: 'Custom view 1', name: 'Custom view 1',
filter_type: 'conversation', filter_type: 1,
query: { query: {
payload: [ payload: [
{ {
attribute_key: 'browser_language', attribute_key: 'name',
filter_operator: 'equal_to', filter_operator: 'equal_to',
values: ['eng'], values: ['john doe'],
query_operator: 'or', query_operator: null,
}, },
],
},
},
]);
});
it('getCustomViewsByFilterType', () => {
const state = { records: customViewList };
expect(getters.getCustomViewsByFilterType(state)(1)).toEqual([
{ {
attribute_key: 'campaign_id', name: 'Custom view 1',
filter_type: 1,
query: {
payload: [
{
attribute_key: 'name',
filter_operator: 'equal_to', filter_operator: 'equal_to',
values: [15], values: ['john doe'],
query_operator: 'and', query_operator: null,
}, },
], ],
}, },