feat: Add contact label filter (#2454)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
parent
50e4bb3e63
commit
6c49e58ff8
17 changed files with 201 additions and 70 deletions
|
@ -15,7 +15,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts_count = resolved_contacts.count
|
@contacts_count = resolved_contacts.count
|
||||||
@contacts = fetch_contact_last_seen_at(resolved_contacts)
|
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
|
@ -26,7 +26,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
search: "%#{params[:q]}%"
|
search: "%#{params[:q]}%"
|
||||||
)
|
)
|
||||||
@contacts_count = contacts.count
|
@contacts_count = contacts.count
|
||||||
@contacts = fetch_contact_last_seen_at(contacts)
|
@contacts = fetch_contacts_with_conversation_count(contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def import
|
def import
|
||||||
|
@ -72,17 +72,22 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# TODO: Move this to a finder class
|
||||||
def resolved_contacts
|
def resolved_contacts
|
||||||
@resolved_contacts ||= Current.account.contacts
|
return @resolved_contacts if @resolved_contacts
|
||||||
|
|
||||||
|
@resolved_contacts = Current.account.contacts
|
||||||
.where.not(email: [nil, ''])
|
.where.not(email: [nil, ''])
|
||||||
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||||
|
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
|
||||||
|
@resolved_contacts
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_current_page
|
def set_current_page
|
||||||
@current_page = params[:page] || 1
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contact_last_seen_at(contacts)
|
def fetch_contacts_with_conversation_count(contacts)
|
||||||
filtrate(contacts).left_outer_joins(:conversations)
|
filtrate(contacts).left_outer_joins(:conversations)
|
||||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||||
.group('contacts.id')
|
.group('contacts.id')
|
||||||
|
|
|
@ -1,13 +1,30 @@
|
||||||
/* global axios */
|
/* global axios */
|
||||||
import ApiClient from './ApiClient';
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
export const buildContactParams = (page, sortAttr, label, search) => {
|
||||||
|
let params = `page=${page}&sort=${sortAttr}`;
|
||||||
|
if (search) {
|
||||||
|
params = `${params}&q=${search}`;
|
||||||
|
}
|
||||||
|
if (label) {
|
||||||
|
params = `${params}&labels[]=${label}`;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
class ContactAPI extends ApiClient {
|
class ContactAPI extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('contacts', { accountScoped: true });
|
super('contacts', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get(page, sortAttr = 'name') {
|
get(page, sortAttr = 'name', label = '') {
|
||||||
return axios.get(`${this.url}?page=${page}&sort=${sortAttr}`);
|
let requestURL = `${this.url}?${buildContactParams(
|
||||||
|
page,
|
||||||
|
sortAttr,
|
||||||
|
label,
|
||||||
|
''
|
||||||
|
)}`;
|
||||||
|
return axios.get(requestURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConversations(contactId) {
|
getConversations(contactId) {
|
||||||
|
@ -26,10 +43,14 @@ class ContactAPI extends ApiClient {
|
||||||
return axios.post(`${this.url}/${contactId}/labels`, { labels });
|
return axios.post(`${this.url}/${contactId}/labels`, { labels });
|
||||||
}
|
}
|
||||||
|
|
||||||
search(search = '', page = 1, sortAttr = 'name') {
|
search(search = '', page = 1, sortAttr = 'name', label = '') {
|
||||||
return axios.get(
|
let requestURL = `${this.url}/search?${buildContactParams(
|
||||||
`${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}`
|
page,
|
||||||
);
|
sortAttr,
|
||||||
|
label,
|
||||||
|
search
|
||||||
|
)}`;
|
||||||
|
return axios.get(requestURL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import contactAPI from '../contacts';
|
import contactAPI, { buildContactParams } from '../contacts';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
import describeWithAPIMock from './apiSpecHelper';
|
||||||
|
|
||||||
|
@ -15,9 +15,9 @@ describe('#ContactsAPI', () => {
|
||||||
|
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
it('#get', () => {
|
it('#get', () => {
|
||||||
contactAPI.get(1, 'name');
|
contactAPI.get(1, 'name', 'customer-support');
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts?page=1&sort=name'
|
'/api/v1/contacts?page=1&sort=name&labels[]=customer-support'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,10 +54,22 @@ describe('#ContactsAPI', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#search', () => {
|
it('#search', () => {
|
||||||
contactAPI.search('leads', 1, 'date');
|
contactAPI.search('leads', 1, 'date', 'customer-support');
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/contacts/search?q=leads&page=1&sort=date'
|
'/api/v1/contacts/search?page=1&sort=date&q=leads&labels[]=customer-support'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#buildContactParams', () => {
|
||||||
|
it('returns correct string', () => {
|
||||||
|
expect(buildContactParams(1, 'name', '', '')).toBe('page=1&sort=name');
|
||||||
|
expect(buildContactParams(1, 'name', 'customer-support', '')).toBe(
|
||||||
|
'page=1&sort=name&labels[]=customer-support'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
buildContactParams(1, 'name', 'customer-support', 'message-content')
|
||||||
|
).toBe('page=1&sort=name&q=message-content&labels[]=customer-support');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -27,6 +27,13 @@
|
||||||
v-if="shouldShowSidebarItem"
|
v-if="shouldShowSidebarItem"
|
||||||
:key="labelSection.toState"
|
:key="labelSection.toState"
|
||||||
:menu-item="labelSection"
|
:menu-item="labelSection"
|
||||||
|
@add-label="showAddLabelPopup"
|
||||||
|
/>
|
||||||
|
<sidebar-item
|
||||||
|
v-if="showShowContactSideMenu"
|
||||||
|
:key="contactLabelSection.key"
|
||||||
|
:menu-item="contactLabelSection"
|
||||||
|
@add-label="showAddLabelPopup"
|
||||||
/>
|
/>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +64,10 @@
|
||||||
:show="showCreateAccountModal"
|
:show="showCreateAccountModal"
|
||||||
@close-account-create-modal="closeCreateAccountModal"
|
@close-account-create-modal="closeCreateAccountModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||||
|
<add-label-modal @close="hideAddLabelPopup" />
|
||||||
|
</woot-modal>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -74,6 +85,7 @@ import AgentDetails from './sidebarComponents/AgentDetails.vue';
|
||||||
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
||||||
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||||
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||||
|
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -84,6 +96,7 @@ export default {
|
||||||
OptionsMenu,
|
OptionsMenu,
|
||||||
AccountSelector,
|
AccountSelector,
|
||||||
AddAccountModal,
|
AddAccountModal,
|
||||||
|
AddLabelModal,
|
||||||
},
|
},
|
||||||
mixins: [adminMixin, alertMixin],
|
mixins: [adminMixin, alertMixin],
|
||||||
data() {
|
data() {
|
||||||
|
@ -91,6 +104,7 @@ export default {
|
||||||
showOptionsMenu: false,
|
showOptionsMenu: false,
|
||||||
showAccountModal: false,
|
showAccountModal: false,
|
||||||
showCreateAccountModal: false,
|
showCreateAccountModal: false,
|
||||||
|
showAddLabelModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -131,6 +145,9 @@ export default {
|
||||||
shouldShowSidebarItem() {
|
shouldShowSidebarItem() {
|
||||||
return this.sidemenuItems.common.routes.includes(this.currentRoute);
|
return this.sidemenuItems.common.routes.includes(this.currentRoute);
|
||||||
},
|
},
|
||||||
|
showShowContactSideMenu() {
|
||||||
|
return this.sidemenuItems.contacts.routes.includes(this.currentRoute);
|
||||||
|
},
|
||||||
shouldShowTeams() {
|
shouldShowTeams() {
|
||||||
return this.shouldShowSidebarItem && this.teams.length;
|
return this.shouldShowSidebarItem && this.teams.length;
|
||||||
},
|
},
|
||||||
|
@ -177,6 +194,29 @@ export default {
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
contactLabelSection() {
|
||||||
|
return {
|
||||||
|
icon: 'ion-pound',
|
||||||
|
label: 'TAGGED_WITH',
|
||||||
|
hasSubMenu: true,
|
||||||
|
key: 'label',
|
||||||
|
newLink: false,
|
||||||
|
cssClass: 'menu-title align-justify',
|
||||||
|
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||||
|
toStateName: 'labels_list',
|
||||||
|
showModalForNewItem: true,
|
||||||
|
modalName: 'AddLabel',
|
||||||
|
children: this.accountLabels.map(label => ({
|
||||||
|
id: label.id,
|
||||||
|
label: label.title,
|
||||||
|
color: label.color,
|
||||||
|
truncateLabel: true,
|
||||||
|
toState: frontendURL(
|
||||||
|
`accounts/${this.accountId}/labels/${label.title}/contacts`
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
teamSection() {
|
teamSection() {
|
||||||
return {
|
return {
|
||||||
icon: 'ion-ios-people',
|
icon: 'ion-ios-people',
|
||||||
|
@ -253,6 +293,12 @@ export default {
|
||||||
closeCreateAccountModal() {
|
closeCreateAccountModal() {
|
||||||
this.showCreateAccountModal = false;
|
this.showCreateAccountModal = false;
|
||||||
},
|
},
|
||||||
|
showAddLabelPopup() {
|
||||||
|
this.showAddLabelModal = true;
|
||||||
|
},
|
||||||
|
hideAddLabelPopup() {
|
||||||
|
this.showAddLabelModal = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -52,11 +52,6 @@
|
||||||
</a>
|
</a>
|
||||||
</router-link>
|
</router-link>
|
||||||
</ul>
|
</ul>
|
||||||
<add-label-modal
|
|
||||||
v-if="showAddLabel"
|
|
||||||
:show.sync="showAddLabel"
|
|
||||||
:on-close="hideAddLabelPopup"
|
|
||||||
/>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -66,17 +61,8 @@ import { mapGetters } from 'vuex';
|
||||||
import router from '../../routes';
|
import router from '../../routes';
|
||||||
import adminMixin from '../../mixins/isAdmin';
|
import adminMixin from '../../mixins/isAdmin';
|
||||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
AddLabelModal,
|
|
||||||
},
|
|
||||||
mixins: [adminMixin],
|
mixins: [adminMixin],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showAddLabel: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
menuItem: {
|
menuItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -127,19 +113,13 @@ export default {
|
||||||
router.push({ name: item.newLinkRouteName, params: { page: 'new' } });
|
router.push({ name: item.newLinkRouteName, params: { page: 'new' } });
|
||||||
} else if (item.showModalForNewItem) {
|
} else if (item.showModalForNewItem) {
|
||||||
if (item.modalName === 'AddLabel') {
|
if (item.modalName === 'AddLabel') {
|
||||||
this.showAddLabelPopup();
|
this.$emit('add-label');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showItem(item) {
|
showItem(item) {
|
||||||
return this.isAdmin && item.newLink !== undefined;
|
return this.isAdmin && item.newLink !== undefined;
|
||||||
},
|
},
|
||||||
showAddLabelPopup() {
|
|
||||||
this.showAddLabel = true;
|
|
||||||
},
|
|
||||||
hideAddLabelPopup() {
|
|
||||||
this.showAddLabel = false;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,8 +7,6 @@ export const getSidebarItems = accountId => ({
|
||||||
'inbox_dashboard',
|
'inbox_dashboard',
|
||||||
'inbox_conversation',
|
'inbox_conversation',
|
||||||
'conversation_through_inbox',
|
'conversation_through_inbox',
|
||||||
'contacts_dashboard',
|
|
||||||
'contacts_dashboard_manage',
|
|
||||||
'notifications_dashboard',
|
'notifications_dashboard',
|
||||||
'settings_account_reports',
|
'settings_account_reports',
|
||||||
'profile_settings',
|
'profile_settings',
|
||||||
|
@ -59,6 +57,29 @@ export const getSidebarItems = accountId => ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
contacts: {
|
||||||
|
routes: [
|
||||||
|
'contacts_dashboard',
|
||||||
|
'contacts_dashboard_manage',
|
||||||
|
'contacts_labels_dashboard',
|
||||||
|
],
|
||||||
|
menuItems: {
|
||||||
|
back: {
|
||||||
|
icon: 'ion-ios-arrow-back',
|
||||||
|
label: 'HOME',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toStateName: 'home',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||||
|
},
|
||||||
|
contacts: {
|
||||||
|
icon: 'ion-person',
|
||||||
|
label: 'ALL_CONTACTS',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||||
|
toStateName: 'contacts_dashboard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
routes: [
|
routes: [
|
||||||
'agent_list',
|
'agent_list',
|
||||||
|
|
|
@ -136,6 +136,7 @@
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"LOADING_MESSAGE": "Loading contacts...",
|
"LOADING_MESSAGE": "Loading contacts...",
|
||||||
"404": "No contacts matches your search 🔍",
|
"404": "No contacts matches your search 🔍",
|
||||||
|
"NO_CONTACTS": "There are no available contacts",
|
||||||
"TABLE_HEADER": {
|
"TABLE_HEADER": {
|
||||||
"NAME": "Name",
|
"NAME": "Name",
|
||||||
"PHONE_NUMBER": "Phone Number",
|
"PHONE_NUMBER": "Phone Number",
|
||||||
|
|
|
@ -135,7 +135,9 @@
|
||||||
"ACCOUNT_SETTINGS": "Account Settings",
|
"ACCOUNT_SETTINGS": "Account Settings",
|
||||||
"APPLICATIONS": "Applications",
|
"APPLICATIONS": "Applications",
|
||||||
"LABELS": "Labels",
|
"LABELS": "Labels",
|
||||||
"TEAMS": "Teams"
|
"TEAMS": "Teams",
|
||||||
|
"ALL_CONTACTS": "All Contacts",
|
||||||
|
"TAGGED_WITH": "Tagged with"
|
||||||
},
|
},
|
||||||
"CREATE_ACCOUNT": {
|
"CREATE_ACCOUNT": {
|
||||||
"NEW_ACCOUNT": "New Account",
|
"NEW_ACCOUNT": "New Account",
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
v-if="showSearchEmptyState"
|
v-if="showSearchEmptyState"
|
||||||
:title="$t('CONTACTS_PAGE.LIST.404')"
|
:title="$t('CONTACTS_PAGE.LIST.404')"
|
||||||
/>
|
/>
|
||||||
|
<empty-state
|
||||||
|
v-else-if="!isLoading && !contacts.length"
|
||||||
|
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
||||||
|
/>
|
||||||
<div v-if="isLoading" class="contacts--loader">
|
<div v-if="isLoading" class="contacts--loader">
|
||||||
<spinner />
|
<spinner />
|
||||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
this-selected-contact-id=""
|
this-selected-contact-id=""
|
||||||
:on-input-search="onInputSearch"
|
:on-input-search="onInputSearch"
|
||||||
:on-toggle-create="onToggleCreate"
|
:on-toggle-create="onToggleCreate"
|
||||||
|
:header-title="label"
|
||||||
/>
|
/>
|
||||||
<contacts-table
|
<contacts-table
|
||||||
:contacts="records"
|
:contacts="records"
|
||||||
|
@ -51,6 +52,9 @@ export default {
|
||||||
ContactInfoPanel,
|
ContactInfoPanel,
|
||||||
CreateContact,
|
CreateContact,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
label: { type: String, default: '' },
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
@ -92,6 +96,11 @@ export default {
|
||||||
: DEFAULT_PAGE;
|
: DEFAULT_PAGE;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
label() {
|
||||||
|
this.fetchContacts(DEFAULT_PAGE);
|
||||||
|
},
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchContacts(this.pageParameter);
|
this.fetchContacts(this.pageParameter);
|
||||||
},
|
},
|
||||||
|
@ -116,7 +125,11 @@ export default {
|
||||||
},
|
},
|
||||||
fetchContacts(page) {
|
fetchContacts(page) {
|
||||||
this.updatePageParam(page);
|
this.updatePageParam(page);
|
||||||
const requestParams = { page, sortAttr: this.getSortAttribute() };
|
const requestParams = {
|
||||||
|
page,
|
||||||
|
sortAttr: this.getSortAttribute(),
|
||||||
|
label: this.label,
|
||||||
|
};
|
||||||
if (!this.searchQuery) {
|
if (!this.searchQuery) {
|
||||||
this.$store.dispatch('contacts/get', requestParams);
|
this.$store.dispatch('contacts/get', requestParams);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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">
|
||||||
{{ $t('CONTACTS_PAGE.HEADER') }}
|
{{ headerTitle ? `#${headerTitle}` : $t('CONTACTS_PAGE.HEADER') }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-aligned-wrap">
|
<div class="right-aligned-wrap">
|
||||||
|
@ -42,6 +42,10 @@
|
||||||
export default {
|
export default {
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
|
headerTitle: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
searchQuery: {
|
searchQuery: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
|
|
@ -10,6 +10,15 @@ export const routes = [
|
||||||
roles: ['administrator', 'agent'],
|
roles: ['administrator', 'agent'],
|
||||||
component: ContactsView,
|
component: ContactsView,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
|
||||||
|
name: 'contacts_labels_dashboard',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
component: ContactsView,
|
||||||
|
props: route => {
|
||||||
|
return { label: route.params.label };
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/contacts/:contactId'),
|
path: frontendURL('accounts/:accountId/contacts/:contactId'),
|
||||||
name: 'contacts_dashboard_manage',
|
name: 'contacts_dashboard_manage',
|
||||||
|
|
|
@ -26,11 +26,10 @@ describe('validationMixin', () => {
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
localVue,
|
localVue,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { title: 'sales' };
|
||||||
title: 'sales',
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
wrapper.vm.$v.$touch();
|
||||||
expect(wrapper.vm.getLabelTitleErrorMessage).toBe('');
|
expect(wrapper.vm.getLabelTitleErrorMessage).toBe('');
|
||||||
});
|
});
|
||||||
it('it should return label required error message if empty name is passed', async () => {
|
it('it should return label required error message if empty name is passed', async () => {
|
||||||
|
@ -38,11 +37,10 @@ describe('validationMixin', () => {
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
localVue,
|
localVue,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { title: '' };
|
||||||
title: '',
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
wrapper.vm.$v.$touch();
|
||||||
expect(wrapper.vm.getLabelTitleErrorMessage).toBe('Label name is required');
|
expect(wrapper.vm.getLabelTitleErrorMessage).toBe('Label name is required');
|
||||||
});
|
});
|
||||||
it('it should return label minimum length error message if one charceter label name is passed', async () => {
|
it('it should return label minimum length error message if one charceter label name is passed', async () => {
|
||||||
|
@ -50,11 +48,10 @@ describe('validationMixin', () => {
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
localVue,
|
localVue,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { title: 's' };
|
||||||
title: 's',
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
wrapper.vm.$v.$touch();
|
||||||
expect(wrapper.vm.getLabelTitleErrorMessage).toBe(
|
expect(wrapper.vm.getLabelTitleErrorMessage).toBe(
|
||||||
'Minimum length 2 is required'
|
'Minimum length 2 is required'
|
||||||
);
|
);
|
||||||
|
@ -64,11 +61,10 @@ describe('validationMixin', () => {
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
localVue,
|
localVue,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { title: 'sales enquiry' };
|
||||||
title: 'sales enquiry',
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
wrapper.vm.$v.$touch();
|
||||||
expect(wrapper.vm.getLabelTitleErrorMessage).toBe(
|
expect(wrapper.vm.getLabelTitleErrorMessage).toBe(
|
||||||
'Only Alphabets, Numbers, Hyphen and Underscore are allowed'
|
'Only Alphabets, Numbers, Hyphen and Underscore are allowed'
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
getLabelTitleErrorMessage() {
|
getLabelTitleErrorMessage() {
|
||||||
if (!this.title) {
|
let errorMessage = '';
|
||||||
return this.$t('LABEL_MGMT.FORM.NAME.REQUIRED_ERROR');
|
if (!this.$v.title.$error) {
|
||||||
|
errorMessage = '';
|
||||||
|
} else if (!this.$v.title.required) {
|
||||||
|
errorMessage = this.$t('LABEL_MGMT.FORM.NAME.REQUIRED_ERROR');
|
||||||
|
} else if (!this.$v.title.minLength) {
|
||||||
|
errorMessage = this.$t('LABEL_MGMT.FORM.NAME.MINIMUM_LENGTH_ERROR');
|
||||||
|
} else if (!this.$v.title.validLabelCharacters) {
|
||||||
|
errorMessage = this.$t('LABEL_MGMT.FORM.NAME.VALID_ERROR');
|
||||||
}
|
}
|
||||||
if (!this.$v.title.minLength) {
|
return errorMessage;
|
||||||
return this.$t('LABEL_MGMT.FORM.NAME.MINIMUM_LENGTH_ERROR');
|
|
||||||
}
|
|
||||||
if (!this.$v.title.validLabelCharacters) {
|
|
||||||
return this.$t('LABEL_MGMT.FORM.NAME.VALID_ERROR');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,12 +6,12 @@ import types from '../../mutation-types';
|
||||||
import ContactAPI from '../../../api/contacts';
|
import ContactAPI from '../../../api/contacts';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
search: async ({ commit }, { search, page, sortAttr }) => {
|
search: async ({ commit }, { search, page, sortAttr, label }) => {
|
||||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { payload, meta },
|
data: { payload, meta },
|
||||||
} = await ContactAPI.search(search, page, sortAttr);
|
} = await ContactAPI.search(search, page, sortAttr, label);
|
||||||
commit(types.CLEAR_CONTACTS);
|
commit(types.CLEAR_CONTACTS);
|
||||||
commit(types.SET_CONTACTS, payload);
|
commit(types.SET_CONTACTS, payload);
|
||||||
commit(types.SET_CONTACT_META, meta);
|
commit(types.SET_CONTACT_META, meta);
|
||||||
|
@ -21,12 +21,12 @@ export const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
get: async ({ commit }, { page = 1, sortAttr } = {}) => {
|
get: async ({ commit }, { page = 1, sortAttr, label } = {}) => {
|
||||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { payload, meta },
|
data: { payload, meta },
|
||||||
} = await ContactAPI.get(page, sortAttr);
|
} = await ContactAPI.get(page, sortAttr, label);
|
||||||
commit(types.CLEAR_CONTACTS);
|
commit(types.CLEAR_CONTACTS);
|
||||||
commit(types.SET_CONTACTS, payload);
|
commit(types.SET_CONTACTS, payload);
|
||||||
commit(types.SET_CONTACT_META, meta);
|
commit(types.SET_CONTACT_META, meta);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module Redis::Config
|
module Redis::Config
|
||||||
DEFAULT_SENTINEL_PORT = '26379'.freeze
|
DEFAULT_SENTINEL_PORT ||= '26379'.freeze
|
||||||
SIDEKIQ_SIZE = 25
|
SIDEKIQ_SIZE ||= 25
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def app
|
def app
|
||||||
|
|
|
@ -40,6 +40,22 @@ RSpec.describe 'Contacts API', type: :request do
|
||||||
expect(response_body['payload'].first['conversations_count']).to eq(contact.conversations.count)
|
expect(response_body['payload'].first['conversations_count']).to eq(contact.conversations.count)
|
||||||
expect(response_body['payload'].first['last_seen_at']).present?
|
expect(response_body['payload'].first['last_seen_at']).present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'filters contacts based on label filter' do
|
||||||
|
contact_with_label1, contact_with_label2 = FactoryBot.create_list(:contact, 2, account: account)
|
||||||
|
contact_with_label1.update_labels(['label1'])
|
||||||
|
contact_with_label2.update_labels(['label2'])
|
||||||
|
|
||||||
|
get "/api/v1/accounts/#{account.id}/contacts",
|
||||||
|
params: { labels: %w[label1 label2] },
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
response_body = JSON.parse(response.body)
|
||||||
|
expect(response_body['meta']['count']).to eq(2)
|
||||||
|
expect(response_body['payload'].pluck('email')).to include(contact_with_label1.email, contact_with_label2.email)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue