Skip to content

Commit

Permalink
feat: Sort contacts via name, email, phone_number, last_activity_at (c…
Browse files Browse the repository at this point in the history
  • Loading branch information
pranavrajs authored May 13, 2021
1 parent 368bab2 commit 0e6cd69
Show file tree
Hide file tree
Showing 24 changed files with 278 additions and 75 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ gem 'maxminddb'
# to create db triggers
gem 'hairtrigger'

gem 'procore-sift'

group :development do
gem 'annotate'
gem 'bullet'
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ GEM
parser (2.7.1.4)
ast (~> 2.4.1)
pg (1.2.3)
procore-sift (0.15.0)
rails (> 4.2.0)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
Expand Down Expand Up @@ -649,6 +651,7 @@ DEPENDENCIES
mini_magick
mock_redis!
pg
procore-sift
pry-rails
puma
pundit
Expand Down
18 changes: 12 additions & 6 deletions app/controllers/api/v1/accounts/contacts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
include Sift

sort_on :email, type: :string
sort_on :name, type: :string
sort_on :phone_number, type: :string
sort_on :last_activity_at, type: :datetime

RESULTS_PER_PAGE = 15
protect_from_forgery with: :null_session

Expand Down Expand Up @@ -68,19 +75,18 @@ def resolved_contacts
@resolved_contacts ||= Current.account.contacts
.where.not(email: [nil, ''])
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
.order('LOWER(name)')
end

def set_current_page
@current_page = params[:page] || 1
end

def fetch_contact_last_seen_at(contacts)
contacts.left_outer_joins(:conversations)
.select('contacts.*, COUNT(conversations.id) as conversations_count, MAX(conversations.contact_last_seen_at) as last_seen_at')
.group('contacts.id')
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
.page(@current_page).per(RESULTS_PER_PAGE)
filtrate(contacts).left_outer_joins(:conversations)
.select('contacts.*, COUNT(conversations.id) as conversations_count')
.group('contacts.id')
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
.page(@current_page).per(RESULTS_PER_PAGE)
end

def build_contact_inbox
Expand Down
10 changes: 6 additions & 4 deletions app/javascript/dashboard/api/contacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class ContactAPI extends ApiClient {
super('contacts', { accountScoped: true });
}

get(page) {
return axios.get(`${this.url}?page=${page}`);
get(page, sortAttr = 'name') {
return axios.get(`${this.url}?page=${page}&sort=${sortAttr}`);
}

getConversations(contactId) {
Expand All @@ -18,8 +18,10 @@ class ContactAPI extends ApiClient {
return axios.get(`${this.url}/${contactId}/contactable_inboxes`);
}

search(search = '', page = 1) {
return axios.get(`${this.url}/search?q=${search}&page=${page}`);
search(search = '', page = 1, sortAttr = 'name') {
return axios.get(
`${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}`
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:columns="columns"
:table-data="tableData"
:border-around="false"
:sort-option="sortOption"
/>

<empty-state
Expand Down Expand Up @@ -57,16 +58,58 @@ export default {
type: [String, Number],
default: '',
},
sortParam: {
type: String,
default: 'name',
},
sortOrder: {
type: String,
default: 'asc',
},
},
data() {
return {
columns: [
sortConfig: {},
sortOption: {
sortAlways: true,
sortChange: params => this.$emit('on-sort-change', params),
},
};
},
computed: {
tableData() {
if (this.isLoading) {
return [];
}
return this.contacts.map(item => {
// Note: The attributes used here is in snake case
// as it simplier the sort attribute calculation
const additional = item.additional_attributes || {};
const { last_activity_at: lastActivityAt } = item;
return {
...item,
phone_number: item.phone_number || '---',
company: additional.company_name || '---',
location: additional.location || '---',
profiles: additional.social_profiles || {},
city: additional.city || '---',
country: additional.country || '---',
conversations_count: item.conversations_count || '---',
last_activity_at: lastActivityAt
? this.dynamicTime(lastActivityAt)
: '---',
};
});
},
columns() {
return [
{
field: 'name',
key: 'name',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.NAME'),
fixed: 'left',
align: 'left',
sortBy: this.sortConfig.name || '',
width: 300,
renderBodyCell: ({ row }) => (
<woot-button
Expand Down Expand Up @@ -98,6 +141,7 @@ export default {
key: 'email',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.EMAIL_ADDRESS'),
align: 'left',
sortBy: this.sortConfig.email || '',
width: 240,
renderBodyCell: ({ row }) => {
if (row.email)
Expand All @@ -116,8 +160,9 @@ export default {
},
},
{
field: 'phone',
key: 'phone',
field: 'phone_number',
key: 'phone_number',
sortBy: this.sortConfig.phone_number || '',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.PHONE_NUMBER'),
align: 'left',
},
Expand Down Expand Up @@ -170,8 +215,9 @@ export default {
},
},
{
field: 'lastSeen',
key: 'lastSeen',
field: 'last_activity_at',
key: 'last_activity_at',
sortBy: this.sortConfig.last_activity_at || '',
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.LAST_ACTIVITY'),
align: 'left',
},
Expand All @@ -182,29 +228,23 @@ export default {
width: 150,
align: 'left',
},
],
};
];
},
},
computed: {
tableData() {
if (this.isLoading) {
return [];
}
return this.contacts.map(item => {
const additional = item.additional_attributes || {};
const { last_seen_at: lastSeenAt } = item;
return {
...item,
phone: item.phone_number || '---',
company: additional.company_name || '---',
location: additional.location || '---',
profiles: additional.social_profiles || {},
city: additional.city || '---',
country: additional.country || '---',
conversationsCount: item.conversations_count || '---',
lastSeen: lastSeenAt ? this.dynamicTime(lastSeenAt) : '---',
};
});
watch: {
sortOrder() {
this.setSortConfig();
},
sortParam() {
this.setSortConfig();
},
},
mounted() {
this.setSortConfig();
},
methods: {
setSortConfig() {
this.sortConfig = { [this.sortParam]: this.sortOrder };
},
},
};
Expand Down Expand Up @@ -258,6 +298,9 @@ export default {
.ve-table-header-th {
font-size: var(--font-size-mini) !important;
}
.ve-table-sort {
top: -4px;
}
}
.contacts--loader {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
:is-loading="uiFlags.isFetching"
:on-click-contact="openContactInfoPanel"
:active-contact-id="selectedContactId"
:sort-config="sortConfig"
@on-sort-change="onSortChange"
/>
<table-footer
:on-page-change="onPageChange"
Expand All @@ -39,6 +41,8 @@ import ContactInfoPanel from './ContactInfoPanel';
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
import TableFooter from 'dashboard/components/widgets/TableFooter';
const DEFAULT_PAGE = 1;
export default {
components: {
ContactsHeader,
Expand All @@ -52,6 +56,7 @@ export default {
searchQuery: '',
showCreateModal: false,
selectedContactId: '',
sortConfig: { name: 'asc' },
};
},
computed: {
Expand Down Expand Up @@ -81,43 +86,63 @@ export default {
},
pageParameter() {
const selectedPageNumber = Number(this.$route.query?.page);
return !Number.isNaN(selectedPageNumber) && selectedPageNumber >= 1
return !Number.isNaN(selectedPageNumber) &&
selectedPageNumber >= DEFAULT_PAGE
? selectedPageNumber
: 1;
: DEFAULT_PAGE;
},
},
mounted() {
this.$store.dispatch('contacts/get', { page: this.pageParameter });
this.fetchContacts(this.pageParameter);
},
methods: {
updatePageParam(page) {
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
},
getSortAttribute() {
let sortAttr = Object.keys(this.sortConfig).reduce((acc, sortKey) => {
const sortOrder = this.sortConfig[sortKey];
if (sortOrder) {
const sortOrderSign = sortOrder === 'asc' ? '' : '-';
return `${sortOrderSign}${sortKey}`;
}
return acc;
}, '');
if (!sortAttr) {
this.sortConfig = { name: 'asc' };
sortAttr = 'name';
}
return sortAttr;
},
fetchContacts(page) {
this.updatePageParam(page);
const requestParams = { page, sortAttr: this.getSortAttribute() };
if (!this.searchQuery) {
this.$store.dispatch('contacts/get', requestParams);
} else {
this.$store.dispatch('contacts/search', {
search: this.searchQuery,
...requestParams,
});
}
},
onInputSearch(event) {
const newQuery = event.target.value;
const refetchAllContacts = !!this.searchQuery && newQuery === '';
this.searchQuery = newQuery;
if (refetchAllContacts) {
this.$store.dispatch('contacts/get', { page: 1 });
this.fetchContacts(DEFAULT_PAGE);
}
this.searchQuery = newQuery;
},
onSearchSubmit() {
this.selectedContactId = '';
if (this.searchQuery) {
this.$store.dispatch('contacts/search', {
search: this.searchQuery,
page: 1,
});
this.fetchContacts(DEFAULT_PAGE);
}
},
onPageChange(page) {
this.selectedContactId = '';
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
if (this.searchQuery) {
this.$store.dispatch('contacts/search', {
search: this.searchQuery,
page,
});
} else {
this.$store.dispatch('contacts/get', { page });
}
this.fetchContacts(page);
},
openContactInfoPanel(contactId) {
this.selectedContactId = contactId;
Expand All @@ -130,6 +155,10 @@ export default {
onToggleCreate() {
this.showCreateModal = !this.showCreateModal;
},
onSortChange(params) {
this.sortConfig = params;
this.fetchContacts(this.meta.currentPage);
},
},
};
</script>
Expand All @@ -138,6 +167,7 @@ export default {
.contacts-page {
width: 100%;
}
.left-wrap {
display: flex;
flex-direction: column;
Expand Down
8 changes: 4 additions & 4 deletions app/javascript/dashboard/store/modules/contacts/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import types from '../../mutation-types';
import ContactAPI from '../../../api/contacts';

export const actions = {
search: async ({ commit }, { search, page }) => {
search: async ({ commit }, { search, page, sortAttr }) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
} = await ContactAPI.search(search, page);
} = await ContactAPI.search(search, page, sortAttr);
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
Expand All @@ -21,12 +21,12 @@ export const actions = {
}
},

get: async ({ commit }, { page = 1 } = {}) => {
get: async ({ commit }, { page = 1, sortAttr } = {}) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
} = await ContactAPI.get(page);
} = await ContactAPI.get(page, sortAttr);
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
Expand Down
Loading

0 comments on commit 0e6cd69

Please sign in to comment.