feat: Sort contacts via name, email, phone_number, last_activity_at (#1870)

This commit is contained in:
Pranav Raj S 2021-05-13 13:32:19 +05:30 committed by GitHub
parent 368bab2553
commit 0e6cd699e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 279 additions and 76 deletions

View file

@ -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'

View file

@ -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

View file

@ -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)

View file

@ -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}`
);
} }
} }

View file

@ -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 {

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -14,6 +14,7 @@ const state = {
isFetchingInboxes: false, isFetchingInboxes: false,
isUpdating: false, isUpdating: false,
}, },
sortOrder: [],
}; };
export default { export default {

View file

@ -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) => {

View file

@ -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', () => {

View file

@ -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]);
}); });
}); });

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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)

View 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

View file

@ -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

View file

@ -0,0 +1,7 @@
in: query
name: page
schema:
type: integer
default: 1
required: false
description: The page parameter

View file

@ -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

View file

@ -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

View file

@ -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"
} }
} }
} }