diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb
index 46f6b83c7..3e7b50bc2 100644
--- a/app/controllers/api/v1/accounts/contacts_controller.rb
+++ b/app/controllers/api/v1/accounts/contacts_controller.rb
@@ -15,7 +15,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def index
@contacts_count = resolved_contacts.count
- @contacts = fetch_contact_last_seen_at(resolved_contacts)
+ @contacts = fetch_contacts_with_conversation_count(resolved_contacts)
end
def search
@@ -26,7 +26,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
search: "%#{params[:q]}%"
)
@contacts_count = contacts.count
- @contacts = fetch_contact_last_seen_at(contacts)
+ @contacts = fetch_contacts_with_conversation_count(contacts)
end
def import
@@ -72,17 +72,22 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
private
+ # TODO: Move this to a finder class
def resolved_contacts
- @resolved_contacts ||= Current.account.contacts
- .where.not(email: [nil, ''])
- .or(Current.account.contacts.where.not(phone_number: [nil, '']))
+ return @resolved_contacts if @resolved_contacts
+
+ @resolved_contacts = Current.account.contacts
+ .where.not(email: [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
def set_current_page
@current_page = params[:page] || 1
end
- def fetch_contact_last_seen_at(contacts)
+ def fetch_contacts_with_conversation_count(contacts)
filtrate(contacts).left_outer_joins(:conversations)
.select('contacts.*, COUNT(conversations.id) as conversations_count')
.group('contacts.id')
diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js
index cc8448873..443a5b8dd 100644
--- a/app/javascript/dashboard/api/contacts.js
+++ b/app/javascript/dashboard/api/contacts.js
@@ -1,13 +1,30 @@
/* global axios */
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 {
constructor() {
super('contacts', { accountScoped: true });
}
- get(page, sortAttr = 'name') {
- return axios.get(`${this.url}?page=${page}&sort=${sortAttr}`);
+ get(page, sortAttr = 'name', label = '') {
+ let requestURL = `${this.url}?${buildContactParams(
+ page,
+ sortAttr,
+ label,
+ ''
+ )}`;
+ return axios.get(requestURL);
}
getConversations(contactId) {
@@ -26,10 +43,14 @@ class ContactAPI extends ApiClient {
return axios.post(`${this.url}/${contactId}/labels`, { labels });
}
- search(search = '', page = 1, sortAttr = 'name') {
- return axios.get(
- `${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}`
- );
+ search(search = '', page = 1, sortAttr = 'name', label = '') {
+ let requestURL = `${this.url}/search?${buildContactParams(
+ page,
+ sortAttr,
+ label,
+ search
+ )}`;
+ return axios.get(requestURL);
}
}
diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js
index a7080a634..08f6ace03 100644
--- a/app/javascript/dashboard/api/specs/contacts.spec.js
+++ b/app/javascript/dashboard/api/specs/contacts.spec.js
@@ -1,4 +1,4 @@
-import contactAPI from '../contacts';
+import contactAPI, { buildContactParams } from '../contacts';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
@@ -15,9 +15,9 @@ describe('#ContactsAPI', () => {
describeWithAPIMock('API calls', context => {
it('#get', () => {
- contactAPI.get(1, 'name');
+ contactAPI.get(1, 'name', 'customer-support');
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', () => {
- contactAPI.search('leads', 1, 'date');
+ contactAPI.search('leads', 1, 'date', 'customer-support');
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');
+ });
+});
diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue
index 996494789..61405f6cd 100644
--- a/app/javascript/dashboard/components/layout/Sidebar.vue
+++ b/app/javascript/dashboard/components/layout/Sidebar.vue
@@ -27,6 +27,13 @@
v-if="shouldShowSidebarItem"
:key="labelSection.toState"
:menu-item="labelSection"
+ @add-label="showAddLabelPopup"
+ />
+
@@ -57,6 +64,10 @@
:show="showCreateAccountModal"
@close-account-create-modal="closeCreateAccountModal"
/>
+
+
+
+
@@ -74,6 +85,7 @@ import AgentDetails from './sidebarComponents/AgentDetails.vue';
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
import AccountSelector from './sidebarComponents/AccountSelector.vue';
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
+import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
export default {
components: {
@@ -84,6 +96,7 @@ export default {
OptionsMenu,
AccountSelector,
AddAccountModal,
+ AddLabelModal,
},
mixins: [adminMixin, alertMixin],
data() {
@@ -91,6 +104,7 @@ export default {
showOptionsMenu: false,
showAccountModal: false,
showCreateAccountModal: false,
+ showAddLabelModal: false,
};
},
@@ -131,6 +145,9 @@ export default {
shouldShowSidebarItem() {
return this.sidemenuItems.common.routes.includes(this.currentRoute);
},
+ showShowContactSideMenu() {
+ return this.sidemenuItems.contacts.routes.includes(this.currentRoute);
+ },
shouldShowTeams() {
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() {
return {
icon: 'ion-ios-people',
@@ -253,6 +293,12 @@ export default {
closeCreateAccountModal() {
this.showCreateAccountModal = false;
},
+ showAddLabelPopup() {
+ this.showAddLabelModal = true;
+ },
+ hideAddLabelPopup() {
+ this.showAddLabelModal = false;
+ },
},
};
diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue
index 810155bef..bd4a00487 100644
--- a/app/javascript/dashboard/components/layout/SidebarItem.vue
+++ b/app/javascript/dashboard/components/layout/SidebarItem.vue
@@ -52,11 +52,6 @@
-
@@ -66,17 +61,8 @@ import { mapGetters } from 'vuex';
import router from '../../routes';
import adminMixin from '../../mixins/isAdmin';
import { getInboxClassByType } from 'dashboard/helper/inbox';
-import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
export default {
- components: {
- AddLabelModal,
- },
mixins: [adminMixin],
- data() {
- return {
- showAddLabel: false,
- };
- },
props: {
menuItem: {
type: Object,
@@ -127,19 +113,13 @@ export default {
router.push({ name: item.newLinkRouteName, params: { page: 'new' } });
} else if (item.showModalForNewItem) {
if (item.modalName === 'AddLabel') {
- this.showAddLabelPopup();
+ this.$emit('add-label');
}
}
},
showItem(item) {
return this.isAdmin && item.newLink !== undefined;
},
- showAddLabelPopup() {
- this.showAddLabel = true;
- },
- hideAddLabelPopup() {
- this.showAddLabel = false;
- },
},
};
diff --git a/app/javascript/dashboard/i18n/default-sidebar.js b/app/javascript/dashboard/i18n/default-sidebar.js
index 8bb78f61b..e5c8b79c5 100644
--- a/app/javascript/dashboard/i18n/default-sidebar.js
+++ b/app/javascript/dashboard/i18n/default-sidebar.js
@@ -7,8 +7,6 @@ export const getSidebarItems = accountId => ({
'inbox_dashboard',
'inbox_conversation',
'conversation_through_inbox',
- 'contacts_dashboard',
- 'contacts_dashboard_manage',
'notifications_dashboard',
'settings_account_reports',
'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: {
routes: [
'agent_list',
diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json
index 84e6303fc..24bcbc27e 100644
--- a/app/javascript/dashboard/i18n/locale/en/contact.json
+++ b/app/javascript/dashboard/i18n/locale/en/contact.json
@@ -136,6 +136,7 @@
"LIST": {
"LOADING_MESSAGE": "Loading contacts...",
"404": "No contacts matches your search 🔍",
+ "NO_CONTACTS": "There are no available contacts",
"TABLE_HEADER": {
"NAME": "Name",
"PHONE_NUMBER": "Phone Number",
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index c7bb0cb6d..509cc07f5 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -135,7 +135,9 @@
"ACCOUNT_SETTINGS": "Account Settings",
"APPLICATIONS": "Applications",
"LABELS": "Labels",
- "TEAMS": "Teams"
+ "TEAMS": "Teams",
+ "ALL_CONTACTS": "All Contacts",
+ "TAGGED_WITH": "Tagged with"
},
"CREATE_ACCOUNT": {
"NEW_ACCOUNT": "New Account",
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsTable.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsTable.vue
index 67faaac22..6f83cf0bd 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsTable.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsTable.vue
@@ -14,6 +14,10 @@
v-if="showSearchEmptyState"
:title="$t('CONTACTS_PAGE.LIST.404')"
/>
+
{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
index 7874302ae..ada19502e 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
@@ -7,6 +7,7 @@
this-selected-contact-id=""
:on-input-search="onInputSearch"
:on-toggle-create="onToggleCreate"
+ :header-title="label"
/>
- {{ $t('CONTACTS_PAGE.HEADER') }}
+ {{ headerTitle ? `#${headerTitle}` : $t('CONTACTS_PAGE.HEADER') }}
@@ -42,6 +42,10 @@
export default {
components: {},
props: {
+ headerTitle: {
+ type: String,
+ default: '',
+ },
searchQuery: {
type: String,
default: '',
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/routes.js b/app/javascript/dashboard/routes/dashboard/contacts/routes.js
index f5ef5ad14..b8b129435 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/routes.js
+++ b/app/javascript/dashboard/routes/dashboard/contacts/routes.js
@@ -10,6 +10,15 @@ export const routes = [
roles: ['administrator', 'agent'],
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'),
name: 'contacts_dashboard_manage',
diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/specs/validationMixin.spec.js b/app/javascript/dashboard/routes/dashboard/settings/labels/specs/validationMixin.spec.js
index ee02f674e..8e1628bdb 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/labels/specs/validationMixin.spec.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/labels/specs/validationMixin.spec.js
@@ -26,11 +26,10 @@ describe('validationMixin', () => {
i18n: i18nConfig,
localVue,
data() {
- return {
- title: 'sales',
- };
+ return { title: 'sales' };
},
});
+ wrapper.vm.$v.$touch();
expect(wrapper.vm.getLabelTitleErrorMessage).toBe('');
});
it('it should return label required error message if empty name is passed', async () => {
@@ -38,11 +37,10 @@ describe('validationMixin', () => {
i18n: i18nConfig,
localVue,
data() {
- return {
- title: '',
- };
+ return { title: '' };
},
});
+ wrapper.vm.$v.$touch();
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 () => {
@@ -50,11 +48,10 @@ describe('validationMixin', () => {
i18n: i18nConfig,
localVue,
data() {
- return {
- title: 's',
- };
+ return { title: 's' };
},
});
+ wrapper.vm.$v.$touch();
expect(wrapper.vm.getLabelTitleErrorMessage).toBe(
'Minimum length 2 is required'
);
@@ -64,11 +61,10 @@ describe('validationMixin', () => {
i18n: i18nConfig,
localVue,
data() {
- return {
- title: 'sales enquiry',
- };
+ return { title: 'sales enquiry' };
},
});
+ wrapper.vm.$v.$touch();
expect(wrapper.vm.getLabelTitleErrorMessage).toBe(
'Only Alphabets, Numbers, Hyphen and Underscore are allowed'
);
diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/validationMixin.js b/app/javascript/dashboard/routes/dashboard/settings/labels/validationMixin.js
index ca2a0b777..9f11646a6 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/labels/validationMixin.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/labels/validationMixin.js
@@ -1,16 +1,17 @@
export default {
computed: {
getLabelTitleErrorMessage() {
- if (!this.title) {
- return this.$t('LABEL_MGMT.FORM.NAME.REQUIRED_ERROR');
+ let errorMessage = '';
+ 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 this.$t('LABEL_MGMT.FORM.NAME.MINIMUM_LENGTH_ERROR');
- }
- if (!this.$v.title.validLabelCharacters) {
- return this.$t('LABEL_MGMT.FORM.NAME.VALID_ERROR');
- }
- return '';
+ return errorMessage;
},
},
};
diff --git a/app/javascript/dashboard/store/modules/contacts/actions.js b/app/javascript/dashboard/store/modules/contacts/actions.js
index 0e5e3a8df..09761c443 100644
--- a/app/javascript/dashboard/store/modules/contacts/actions.js
+++ b/app/javascript/dashboard/store/modules/contacts/actions.js
@@ -6,12 +6,12 @@ import types from '../../mutation-types';
import ContactAPI from '../../../api/contacts';
export const actions = {
- search: async ({ commit }, { search, page, sortAttr }) => {
+ search: async ({ commit }, { search, page, sortAttr, label }) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
- } = await ContactAPI.search(search, page, sortAttr);
+ } = await ContactAPI.search(search, page, sortAttr, label);
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
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 });
try {
const {
data: { payload, meta },
- } = await ContactAPI.get(page, sortAttr);
+ } = await ContactAPI.get(page, sortAttr, label);
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
diff --git a/lib/redis/config.rb b/lib/redis/config.rb
index 35ea3e617..f6027d1f6 100644
--- a/lib/redis/config.rb
+++ b/lib/redis/config.rb
@@ -1,6 +1,6 @@
module Redis::Config
- DEFAULT_SENTINEL_PORT = '26379'.freeze
- SIDEKIQ_SIZE = 25
+ DEFAULT_SENTINEL_PORT ||= '26379'.freeze
+ SIDEKIQ_SIZE ||= 25
class << self
def app
diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
index 12b58c90f..0ebe6b8a0 100644
--- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
@@ -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['last_seen_at']).present?
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