feat: Sort contacts via name, email, phone_number, last_activity_at (#1870)
This commit is contained in:
@@ -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) {
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:columns="columns"
|
||||
:table-data="tableData"
|
||||
:border-around="false"
|
||||
:sort-option="sortOption"
|
||||
/>
|
||||
|
||||
<empty-state
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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',
|
||||
},
|
||||
@@ -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',
|
||||
},
|
||||
@@ -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 };
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -258,6 +298,9 @@ export default {
|
||||
.ve-table-header-th {
|
||||
font-size: var(--font-size-mini) !important;
|
||||
}
|
||||
.ve-table-sort {
|
||||
top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.contacts--loader {
|
||||
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
@@ -52,6 +56,7 @@ export default {
|
||||
searchQuery: '',
|
||||
showCreateModal: false,
|
||||
selectedContactId: '',
|
||||
sortConfig: { name: 'asc' },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -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 === '';
|
||||
if (refetchAllContacts) {
|
||||
this.$store.dispatch('contacts/get', { page: 1 });
|
||||
}
|
||||
this.searchQuery = newQuery;
|
||||
if (refetchAllContacts) {
|
||||
this.fetchContacts(DEFAULT_PAGE);
|
||||
}
|
||||
},
|
||||
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;
|
||||
@@ -130,6 +155,10 @@ export default {
|
||||
onToggleCreate() {
|
||||
this.showCreateModal = !this.showCreateModal;
|
||||
},
|
||||
onSortChange(params) {
|
||||
this.sortConfig = params;
|
||||
this.fetchContacts(this.meta.currentPage);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -138,6 +167,7 @@ export default {
|
||||
.contacts-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const getters = {
|
||||
getContacts($state) {
|
||||
return Object.values($state.records);
|
||||
return $state.sortOrder.map(contactId => $state.records[contactId]);
|
||||
},
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
|
||||
@@ -14,6 +14,7 @@ const state = {
|
||||
isFetchingInboxes: false,
|
||||
isUpdating: false,
|
||||
},
|
||||
sortOrder: [],
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -11,6 +11,7 @@ export const mutations = {
|
||||
|
||||
[types.CLEAR_CONTACTS]: $state => {
|
||||
Vue.set($state, 'records', {});
|
||||
Vue.set($state, 'sortOrder', []);
|
||||
},
|
||||
|
||||
[types.SET_CONTACT_META]: ($state, data) => {
|
||||
@@ -20,12 +21,14 @@ export const mutations = {
|
||||
},
|
||||
|
||||
[types.SET_CONTACTS]: ($state, data) => {
|
||||
data.forEach(contact => {
|
||||
const sortOrder = data.map(contact => {
|
||||
Vue.set($state.records, contact.id, {
|
||||
...($state.records[contact.id] || {}),
|
||||
...contact,
|
||||
});
|
||||
return contact.id;
|
||||
});
|
||||
$state.sortOrder = sortOrder;
|
||||
},
|
||||
|
||||
[types.SET_CONTACT_ITEM]: ($state, data) => {
|
||||
@@ -33,6 +36,10 @@ export const mutations = {
|
||||
...($state.records[data.id] || {}),
|
||||
...data,
|
||||
});
|
||||
|
||||
if (!$state.sortOrder.includes(data.id)) {
|
||||
$state.sortOrder.push(data.id);
|
||||
}
|
||||
},
|
||||
|
||||
[types.EDIT_CONTACT]: ($state, data) => {
|
||||
|
||||
@@ -6,9 +6,13 @@ const { getters } = Contacts;
|
||||
describe('#getters', () => {
|
||||
it('getContacts', () => {
|
||||
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', () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ describe('#mutations', () => {
|
||||
it('set contact records', () => {
|
||||
const state = { records: {} };
|
||||
mutations[types.SET_CONTACTS](state, [
|
||||
{ id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||
]);
|
||||
expect(state.records).toEqual({
|
||||
@@ -15,7 +16,13 @@ describe('#mutations', () => {
|
||||
name: 'contact1',
|
||||
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: {
|
||||
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||
},
|
||||
sortOrder: [1],
|
||||
};
|
||||
mutations[types.SET_CONTACT_ITEM](state, {
|
||||
id: 2,
|
||||
@@ -35,6 +43,7 @@ describe('#mutations', () => {
|
||||
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||
2: { id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||
});
|
||||
expect(state.sortOrder).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user