feat: Sort contacts via name, email, phone_number, last_activity_at (#1870)
This commit is contained in:
parent
368bab2553
commit
0e6cd699e8
24 changed files with 279 additions and 76 deletions
2
Gemfile
2
Gemfile
|
@ -107,6 +107,8 @@ gem 'maxminddb'
|
||||||
# to create db triggers
|
# to create db triggers
|
||||||
gem 'hairtrigger'
|
gem 'hairtrigger'
|
||||||
|
|
||||||
|
gem 'procore-sift'
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'annotate'
|
gem 'annotate'
|
||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
|
|
|
@ -353,6 +353,8 @@ GEM
|
||||||
parser (2.7.1.4)
|
parser (2.7.1.4)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
|
procore-sift (0.15.0)
|
||||||
|
rails (> 4.2.0)
|
||||||
pry (0.13.1)
|
pry (0.13.1)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
|
@ -649,6 +651,7 @@ DEPENDENCIES
|
||||||
mini_magick
|
mini_magick
|
||||||
mock_redis!
|
mock_redis!
|
||||||
pg
|
pg
|
||||||
|
procore-sift
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
pundit
|
pundit
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
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
|
RESULTS_PER_PAGE = 15
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
|
@ -68,7 +75,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
@resolved_contacts ||= Current.account.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, '']))
|
||||||
.order('LOWER(name)')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_current_page
|
def set_current_page
|
||||||
|
@ -76,8 +82,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contact_last_seen_at(contacts)
|
def fetch_contact_last_seen_at(contacts)
|
||||||
contacts.left_outer_joins(:conversations)
|
filtrate(contacts).left_outer_joins(:conversations)
|
||||||
.select('contacts.*, COUNT(conversations.id) as conversations_count, MAX(conversations.contact_last_seen_at) as last_seen_at')
|
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||||
.group('contacts.id')
|
.group('contacts.id')
|
||||||
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
||||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||||
|
|
|
@ -6,8 +6,8 @@ class ContactAPI extends ApiClient {
|
||||||
super('contacts', { accountScoped: true });
|
super('contacts', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get(page) {
|
get(page, sortAttr = 'name') {
|
||||||
return axios.get(`${this.url}?page=${page}`);
|
return axios.get(`${this.url}?page=${page}&sort=${sortAttr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConversations(contactId) {
|
getConversations(contactId) {
|
||||||
|
@ -18,8 +18,10 @@ class ContactAPI extends ApiClient {
|
||||||
return axios.get(`${this.url}/${contactId}/contactable_inboxes`);
|
return axios.get(`${this.url}/${contactId}/contactable_inboxes`);
|
||||||
}
|
}
|
||||||
|
|
||||||
search(search = '', page = 1) {
|
search(search = '', page = 1, sortAttr = 'name') {
|
||||||
return axios.get(`${this.url}/search?q=${search}&page=${page}`);
|
return axios.get(
|
||||||
|
`${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:table-data="tableData"
|
:table-data="tableData"
|
||||||
:border-around="false"
|
:border-around="false"
|
||||||
|
:sort-option="sortOption"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<empty-state
|
<empty-state
|
||||||
|
@ -57,16 +58,58 @@ export default {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
sortParam: {
|
||||||
|
type: String,
|
||||||
|
default: 'name',
|
||||||
|
},
|
||||||
|
sortOrder: {
|
||||||
|
type: String,
|
||||||
|
default: 'asc',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
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',
|
field: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.NAME'),
|
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.NAME'),
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
sortBy: this.sortConfig.name || '',
|
||||||
width: 300,
|
width: 300,
|
||||||
renderBodyCell: ({ row }) => (
|
renderBodyCell: ({ row }) => (
|
||||||
<woot-button
|
<woot-button
|
||||||
|
@ -98,6 +141,7 @@ export default {
|
||||||
key: 'email',
|
key: 'email',
|
||||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.EMAIL_ADDRESS'),
|
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.EMAIL_ADDRESS'),
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
sortBy: this.sortConfig.email || '',
|
||||||
width: 240,
|
width: 240,
|
||||||
renderBodyCell: ({ row }) => {
|
renderBodyCell: ({ row }) => {
|
||||||
if (row.email)
|
if (row.email)
|
||||||
|
@ -116,8 +160,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'phone',
|
field: 'phone_number',
|
||||||
key: 'phone',
|
key: 'phone_number',
|
||||||
|
sortBy: this.sortConfig.phone_number || '',
|
||||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.PHONE_NUMBER'),
|
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.PHONE_NUMBER'),
|
||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
|
@ -170,8 +215,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'lastSeen',
|
field: 'last_activity_at',
|
||||||
key: 'lastSeen',
|
key: 'last_activity_at',
|
||||||
|
sortBy: this.sortConfig.last_activity_at || '',
|
||||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.LAST_ACTIVITY'),
|
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.LAST_ACTIVITY'),
|
||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
|
@ -182,29 +228,23 @@ export default {
|
||||||
width: 150,
|
width: 150,
|
||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
};
|
|
||||||
},
|
},
|
||||||
computed: {
|
},
|
||||||
tableData() {
|
watch: {
|
||||||
if (this.isLoading) {
|
sortOrder() {
|
||||||
return [];
|
this.setSortConfig();
|
||||||
}
|
},
|
||||||
return this.contacts.map(item => {
|
sortParam() {
|
||||||
const additional = item.additional_attributes || {};
|
this.setSortConfig();
|
||||||
const { last_seen_at: lastSeenAt } = item;
|
},
|
||||||
return {
|
},
|
||||||
...item,
|
mounted() {
|
||||||
phone: item.phone_number || '---',
|
this.setSortConfig();
|
||||||
company: additional.company_name || '---',
|
},
|
||||||
location: additional.location || '---',
|
methods: {
|
||||||
profiles: additional.social_profiles || {},
|
setSortConfig() {
|
||||||
city: additional.city || '---',
|
this.sortConfig = { [this.sortParam]: this.sortOrder };
|
||||||
country: additional.country || '---',
|
|
||||||
conversationsCount: item.conversations_count || '---',
|
|
||||||
lastSeen: lastSeenAt ? this.dynamicTime(lastSeenAt) : '---',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -258,6 +298,9 @@ export default {
|
||||||
.ve-table-header-th {
|
.ve-table-header-th {
|
||||||
font-size: var(--font-size-mini) !important;
|
font-size: var(--font-size-mini) !important;
|
||||||
}
|
}
|
||||||
|
.ve-table-sort {
|
||||||
|
top: -4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.contacts--loader {
|
.contacts--loader {
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
:is-loading="uiFlags.isFetching"
|
:is-loading="uiFlags.isFetching"
|
||||||
:on-click-contact="openContactInfoPanel"
|
:on-click-contact="openContactInfoPanel"
|
||||||
:active-contact-id="selectedContactId"
|
:active-contact-id="selectedContactId"
|
||||||
|
:sort-config="sortConfig"
|
||||||
|
@on-sort-change="onSortChange"
|
||||||
/>
|
/>
|
||||||
<table-footer
|
<table-footer
|
||||||
:on-page-change="onPageChange"
|
:on-page-change="onPageChange"
|
||||||
|
@ -39,6 +41,8 @@ import ContactInfoPanel from './ContactInfoPanel';
|
||||||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
|
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
|
||||||
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
||||||
|
|
||||||
|
const DEFAULT_PAGE = 1;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ContactsHeader,
|
ContactsHeader,
|
||||||
|
@ -52,6 +56,7 @@ export default {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
showCreateModal: false,
|
showCreateModal: false,
|
||||||
selectedContactId: '',
|
selectedContactId: '',
|
||||||
|
sortConfig: { name: 'asc' },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -81,43 +86,63 @@ export default {
|
||||||
},
|
},
|
||||||
pageParameter() {
|
pageParameter() {
|
||||||
const selectedPageNumber = Number(this.$route.query?.page);
|
const selectedPageNumber = Number(this.$route.query?.page);
|
||||||
return !Number.isNaN(selectedPageNumber) && selectedPageNumber >= 1
|
return !Number.isNaN(selectedPageNumber) &&
|
||||||
|
selectedPageNumber >= DEFAULT_PAGE
|
||||||
? selectedPageNumber
|
? selectedPageNumber
|
||||||
: 1;
|
: DEFAULT_PAGE;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('contacts/get', { page: this.pageParameter });
|
this.fetchContacts(this.pageParameter);
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
onInputSearch(event) {
|
||||||
const newQuery = event.target.value;
|
const newQuery = event.target.value;
|
||||||
const refetchAllContacts = !!this.searchQuery && newQuery === '';
|
const refetchAllContacts = !!this.searchQuery && newQuery === '';
|
||||||
if (refetchAllContacts) {
|
|
||||||
this.$store.dispatch('contacts/get', { page: 1 });
|
|
||||||
}
|
|
||||||
this.searchQuery = newQuery;
|
this.searchQuery = newQuery;
|
||||||
|
if (refetchAllContacts) {
|
||||||
|
this.fetchContacts(DEFAULT_PAGE);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSearchSubmit() {
|
onSearchSubmit() {
|
||||||
this.selectedContactId = '';
|
this.selectedContactId = '';
|
||||||
if (this.searchQuery) {
|
if (this.searchQuery) {
|
||||||
this.$store.dispatch('contacts/search', {
|
this.fetchContacts(DEFAULT_PAGE);
|
||||||
search: this.searchQuery,
|
|
||||||
page: 1,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPageChange(page) {
|
onPageChange(page) {
|
||||||
this.selectedContactId = '';
|
this.selectedContactId = '';
|
||||||
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
|
this.fetchContacts(page);
|
||||||
if (this.searchQuery) {
|
|
||||||
this.$store.dispatch('contacts/search', {
|
|
||||||
search: this.searchQuery,
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.$store.dispatch('contacts/get', { page });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
openContactInfoPanel(contactId) {
|
openContactInfoPanel(contactId) {
|
||||||
this.selectedContactId = contactId;
|
this.selectedContactId = contactId;
|
||||||
|
@ -130,6 +155,10 @@ export default {
|
||||||
onToggleCreate() {
|
onToggleCreate() {
|
||||||
this.showCreateModal = !this.showCreateModal;
|
this.showCreateModal = !this.showCreateModal;
|
||||||
},
|
},
|
||||||
|
onSortChange(params) {
|
||||||
|
this.sortConfig = params;
|
||||||
|
this.fetchContacts(this.meta.currentPage);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -138,6 +167,7 @@ export default {
|
||||||
.contacts-page {
|
.contacts-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-wrap {
|
.left-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -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 }) => {
|
search: async ({ commit }, { search, page, sortAttr }) => {
|
||||||
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);
|
} = await ContactAPI.search(search, page, sortAttr);
|
||||||
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 } = {}) => {
|
get: async ({ commit }, { page = 1, sortAttr } = {}) => {
|
||||||
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);
|
} = await ContactAPI.get(page, sortAttr);
|
||||||
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 @@
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getContacts($state) {
|
getContacts($state) {
|
||||||
return Object.values($state.records);
|
return $state.sortOrder.map(contactId => $state.records[contactId]);
|
||||||
},
|
},
|
||||||
getUIFlags($state) {
|
getUIFlags($state) {
|
||||||
return $state.uiFlags;
|
return $state.uiFlags;
|
||||||
|
|
|
@ -14,6 +14,7 @@ const state = {
|
||||||
isFetchingInboxes: false,
|
isFetchingInboxes: false,
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
},
|
},
|
||||||
|
sortOrder: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const mutations = {
|
||||||
|
|
||||||
[types.CLEAR_CONTACTS]: $state => {
|
[types.CLEAR_CONTACTS]: $state => {
|
||||||
Vue.set($state, 'records', {});
|
Vue.set($state, 'records', {});
|
||||||
|
Vue.set($state, 'sortOrder', []);
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_CONTACT_META]: ($state, data) => {
|
[types.SET_CONTACT_META]: ($state, data) => {
|
||||||
|
@ -20,12 +21,14 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_CONTACTS]: ($state, data) => {
|
[types.SET_CONTACTS]: ($state, data) => {
|
||||||
data.forEach(contact => {
|
const sortOrder = data.map(contact => {
|
||||||
Vue.set($state.records, contact.id, {
|
Vue.set($state.records, contact.id, {
|
||||||
...($state.records[contact.id] || {}),
|
...($state.records[contact.id] || {}),
|
||||||
...contact,
|
...contact,
|
||||||
});
|
});
|
||||||
|
return contact.id;
|
||||||
});
|
});
|
||||||
|
$state.sortOrder = sortOrder;
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_CONTACT_ITEM]: ($state, data) => {
|
[types.SET_CONTACT_ITEM]: ($state, data) => {
|
||||||
|
@ -33,6 +36,10 @@ export const mutations = {
|
||||||
...($state.records[data.id] || {}),
|
...($state.records[data.id] || {}),
|
||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!$state.sortOrder.includes(data.id)) {
|
||||||
|
$state.sortOrder.push(data.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.EDIT_CONTACT]: ($state, data) => {
|
[types.EDIT_CONTACT]: ($state, data) => {
|
||||||
|
|
|
@ -6,9 +6,13 @@ const { getters } = Contacts;
|
||||||
describe('#getters', () => {
|
describe('#getters', () => {
|
||||||
it('getContacts', () => {
|
it('getContacts', () => {
|
||||||
const state = {
|
const state = {
|
||||||
records: { 1: contactList[0] },
|
records: { 1: contactList[0], 3: contactList[2] },
|
||||||
|
sortOrder: [3, 1],
|
||||||
};
|
};
|
||||||
expect(getters.getContacts(state)).toEqual([contactList[0]]);
|
expect(getters.getContacts(state)).toEqual([
|
||||||
|
contactList[2],
|
||||||
|
contactList[0],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getContact', () => {
|
it('getContact', () => {
|
||||||
|
|
|
@ -7,6 +7,7 @@ describe('#mutations', () => {
|
||||||
it('set contact records', () => {
|
it('set contact records', () => {
|
||||||
const state = { records: {} };
|
const state = { records: {} };
|
||||||
mutations[types.SET_CONTACTS](state, [
|
mutations[types.SET_CONTACTS](state, [
|
||||||
|
{ id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||||
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
]);
|
]);
|
||||||
expect(state.records).toEqual({
|
expect(state.records).toEqual({
|
||||||
|
@ -15,7 +16,13 @@ describe('#mutations', () => {
|
||||||
name: 'contact1',
|
name: 'contact1',
|
||||||
email: 'contact1@chatwoot.com',
|
email: 'contact1@chatwoot.com',
|
||||||
},
|
},
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
name: 'contact2',
|
||||||
|
email: 'contact2@chatwoot.com',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
expect(state.sortOrder).toEqual([2, 1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -25,6 +32,7 @@ describe('#mutations', () => {
|
||||||
records: {
|
records: {
|
||||||
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
},
|
},
|
||||||
|
sortOrder: [1],
|
||||||
};
|
};
|
||||||
mutations[types.SET_CONTACT_ITEM](state, {
|
mutations[types.SET_CONTACT_ITEM](state, {
|
||||||
id: 2,
|
id: 2,
|
||||||
|
@ -35,6 +43,7 @@ describe('#mutations', () => {
|
||||||
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
2: { id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
2: { id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||||
});
|
});
|
||||||
|
expect(state.sortOrder).toEqual([1, 2]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
# custom_attributes :jsonb
|
# custom_attributes :jsonb
|
||||||
# email :string
|
# email :string
|
||||||
# identifier :string
|
# identifier :string
|
||||||
|
# last_activity_at :datetime
|
||||||
# name :string
|
# name :string
|
||||||
# phone_number :string
|
# phone_number :string
|
||||||
# pubsub_token :string
|
# pubsub_token :string
|
||||||
|
|
|
@ -133,6 +133,11 @@ class Message < ApplicationRecord
|
||||||
dispatch_create_events
|
dispatch_create_events
|
||||||
send_reply
|
send_reply
|
||||||
execute_message_template_hooks
|
execute_message_template_hooks
|
||||||
|
update_contact_activity
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_contact_activity
|
||||||
|
sender.update(last_activity_at: DateTime.now) if sender&.is_a?(Contact)
|
||||||
end
|
end
|
||||||
|
|
||||||
def dispatch_create_events
|
def dispatch_create_events
|
||||||
|
|
|
@ -7,7 +7,7 @@ json.phone_number resource.phone_number
|
||||||
json.thumbnail resource.avatar_url
|
json.thumbnail resource.avatar_url
|
||||||
json.custom_attributes resource.custom_attributes
|
json.custom_attributes resource.custom_attributes
|
||||||
json.conversations_count resource.conversations_count if resource[:conversations_count].present?
|
json.conversations_count resource.conversations_count if resource[:conversations_count].present?
|
||||||
json.last_seen_at resource.last_seen_at.to_i if resource[:last_seen_at].present?
|
json.last_activity_at resource.last_activity_at.to_i if resource[:last_activity_at].present?
|
||||||
|
|
||||||
# we only want to output contact inbox when its /contacts endpoints
|
# we only want to output contact inbox when its /contacts endpoints
|
||||||
if defined?(with_contact_inboxes) && with_contact_inboxes.present?
|
if defined?(with_contact_inboxes) && with_contact_inboxes.present?
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
class AddLastActivityAtToContacts < ActiveRecord::Migration[6.0]
|
||||||
|
def up
|
||||||
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
|
add_column :contacts, :last_activity_at, :datetime, index: true, default: nil
|
||||||
|
Conversation.find_in_batches do |conversation_batch|
|
||||||
|
conversation_batch.each do |conversation|
|
||||||
|
contact = conversation.contact
|
||||||
|
if contact.last_activity_at.nil? || conversation.updated_at > contact.last_activity_at
|
||||||
|
contact.update_columns(last_activity_at: conversation.updated_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Contact.where(additional_attributes: nil).update_all(additional_attributes: {})
|
||||||
|
Contact.where(phone_number: '').update_all(phone_number: nil)
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :contacts, :last_activity_at, :datetime, index: true, default: nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -228,6 +228,7 @@ ActiveRecord::Schema.define(version: 2021_04_30_100138) do
|
||||||
t.jsonb "additional_attributes", default: {}
|
t.jsonb "additional_attributes", default: {}
|
||||||
t.string "identifier"
|
t.string "identifier"
|
||||||
t.jsonb "custom_attributes", default: {}
|
t.jsonb "custom_attributes", default: {}
|
||||||
|
t.datetime "last_activity_at"
|
||||||
t.index ["account_id"], name: "index_contacts_on_account_id"
|
t.index ["account_id"], name: "index_contacts_on_account_id"
|
||||||
t.index ["email", "account_id"], name: "uniq_email_per_account_contact", unique: true
|
t.index ["email", "account_id"], name: "uniq_email_per_account_contact", unique: true
|
||||||
t.index ["identifier", "account_id"], name: "uniq_identifier_per_account_contact", unique: true
|
t.index ["identifier", "account_id"], name: "uniq_identifier_per_account_contact", unique: true
|
||||||
|
|
|
@ -17,6 +17,10 @@ RSpec.describe Message, type: :model do
|
||||||
expect(message.created_at).to eq message.conversation.last_activity_at
|
expect(message.created_at).to eq message.conversation.last_activity_at
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'updates contact last_activity_at when created' do
|
||||||
|
expect { message.save! }.to(change { message.sender.last_activity_at })
|
||||||
|
end
|
||||||
|
|
||||||
it 'triggers ::MessageTemplates::HookExecutionService' do
|
it 'triggers ::MessageTemplates::HookExecutionService' do
|
||||||
hook_execution_service = double
|
hook_execution_service = double
|
||||||
allow(::MessageTemplates::HookExecutionService).to receive(:new).and_return(hook_execution_service)
|
allow(::MessageTemplates::HookExecutionService).to receive(:new).and_return(hook_execution_service)
|
||||||
|
|
15
swagger/parameters/contact_sort.yml
Normal file
15
swagger/parameters/contact_sort.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
in: query
|
||||||
|
name: sort
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- name
|
||||||
|
- email
|
||||||
|
- phone_number
|
||||||
|
- last_activity_at
|
||||||
|
- -name
|
||||||
|
- -email
|
||||||
|
- -phone_number
|
||||||
|
- -last_activity_at
|
||||||
|
required: false
|
||||||
|
description: The attribute by which list should be sorted
|
|
@ -4,7 +4,6 @@ account_id:
|
||||||
team_id:
|
team_id:
|
||||||
$ref: ./team_id.yml
|
$ref: ./team_id.yml
|
||||||
|
|
||||||
|
|
||||||
source_id:
|
source_id:
|
||||||
$ref: ./source_id.yml
|
$ref: ./source_id.yml
|
||||||
|
|
||||||
|
@ -14,3 +13,8 @@ conversation_id:
|
||||||
message_id:
|
message_id:
|
||||||
$ref: ./message_id.yml
|
$ref: ./message_id.yml
|
||||||
|
|
||||||
|
contact_sort_param:
|
||||||
|
$ref: ./contact_sort.yml
|
||||||
|
|
||||||
|
page:
|
||||||
|
$ref: ./page.yml
|
||||||
|
|
7
swagger/parameters/page.yml
Normal file
7
swagger/parameters/page.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
required: false
|
||||||
|
description: The page parameter
|
|
@ -2,12 +2,11 @@ get:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contact
|
||||||
operationId: contactList
|
operationId: contactList
|
||||||
description: Listing all the contacts with pagination
|
description: Listing all the contacts with pagination (Page size = 15)
|
||||||
summary: List Contacts
|
summary: List Contacts
|
||||||
parameters:
|
parameters:
|
||||||
- name: query_hash
|
- $ref: '#/parameters/contact_sort_param'
|
||||||
in: query
|
- $ref: '#/parameters/page'
|
||||||
type: string
|
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Success
|
description: Success
|
||||||
|
|
|
@ -2,12 +2,14 @@ get:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contact
|
||||||
operationId: contactSearch
|
operationId: contactSearch
|
||||||
description: Search the contacts using a search key, currently supports email search
|
description: Search the contacts using a search key, currently supports email search (Page size = 15)
|
||||||
summary: Search Contacts
|
summary: Search Contacts
|
||||||
parameters:
|
parameters:
|
||||||
- name: q
|
- name: q
|
||||||
in: query
|
in: query
|
||||||
type: string
|
type: string
|
||||||
|
- $ref: '#/parameters/contact_sort_param'
|
||||||
|
- $ref: '#/parameters/page'
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Success
|
description: Success
|
||||||
|
|
|
@ -887,13 +887,14 @@
|
||||||
"Contact"
|
"Contact"
|
||||||
],
|
],
|
||||||
"operationId": "contactList",
|
"operationId": "contactList",
|
||||||
"description": "Listing all the contacts with pagination",
|
"description": "Listing all the contacts with pagination (Page size = 15)",
|
||||||
"summary": "List Contacts",
|
"summary": "List Contacts",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "query_hash",
|
"$ref": "#/parameters/contact_sort_param"
|
||||||
"in": "query",
|
},
|
||||||
"type": "string"
|
{
|
||||||
|
"$ref": "#/parameters/page"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -1052,13 +1053,19 @@
|
||||||
"Contact"
|
"Contact"
|
||||||
],
|
],
|
||||||
"operationId": "contactSearch",
|
"operationId": "contactSearch",
|
||||||
"description": "Search the contacts using a search key, currently supports email search",
|
"description": "Search the contacts using a search key, currently supports email search (Page size = 15)",
|
||||||
"summary": "Search Contacts",
|
"summary": "Search Contacts",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "q",
|
"name": "q",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/parameters/contact_sort_param"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/parameters/page"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -2083,6 +2090,35 @@
|
||||||
},
|
},
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "The numeric ID of the message"
|
"description": "The numeric ID of the message"
|
||||||
|
},
|
||||||
|
"contact_sort_param": {
|
||||||
|
"in": "query",
|
||||||
|
"name": "sort",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"name",
|
||||||
|
"email",
|
||||||
|
"phone_number",
|
||||||
|
"last_activity_at",
|
||||||
|
"-name",
|
||||||
|
"-email",
|
||||||
|
"-phone_number",
|
||||||
|
"-last_activity_at"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"description": "The attribute by which list should be sorted"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"in": "query",
|
||||||
|
"name": "page",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"description": "The page parameter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue