From 3a6877b90616dfeeeffc42ea2b1f2d4c74ca619d Mon Sep 17 00:00:00 2001 From: "nrvikas@gmail.com" Date: Fri, 3 Jan 2020 15:20:58 +1100 Subject: [PATCH] [SDESK-4908] Paginate results in contacts selection in Event Form --- client/actions/contacts.js | 20 ++++-- client/actions/tests/contacts_test.js | 15 ++++- client/components/Contacts/ContactField.jsx | 3 - .../SelectListPopup.jsx | 63 ++++++++++++++++--- client/components/UI/SearchField/index.jsx | 2 + client/reducers/contacts.js | 20 +++++- client/selectors/general.js | 3 + 7 files changed, 107 insertions(+), 19 deletions(-) diff --git a/client/actions/contacts.js b/client/actions/contacts.js index db6e94fb0..d9a955fa1 100644 --- a/client/actions/contacts.js +++ b/client/actions/contacts.js @@ -8,7 +8,7 @@ import * as selectors from '../selectors'; * @param {string} contactType - Limit the query to a particular contact type * @returns {Array} Returns an array of contacts found */ -const getContacts = (searchText, searchFields = [], contactType = '') => ( +const getContacts = (searchText, searchFields = [], contactType = '', page = 1) => ( (dispatch, getState, {api}) => { const bool = { must: [], @@ -24,6 +24,7 @@ const getContacts = (searchText, searchFields = [], contactType = '') => ( default_field: 'first_name', fields: searchFields, query: searchText + '*', + default_operator: 'AND', }, }); } @@ -32,14 +33,21 @@ const getContacts = (searchText, searchFields = [], contactType = '') => ( bool.must.push({term: {contact_type: contactType}}); } + dispatch({type: 'LOADING_CONTACTS'}); + return api('contacts').query({ source: {query: {bool: bool}}, sort: '[("first_name", 1)]', + max_results: 200, + page: page, }) .then( (data) => dispatch( self.receiveContacts( - get(data, '_items', []) + get(data, '_items', []), + get(data, '_meta.total'), + get(data, '_meta.page') + ) ) ); @@ -160,12 +168,16 @@ const addContact = (newContact) => ({ * @param {Array} contacts - The contacts to add to the store * @returns {Array} Returns an array of the contacts provided */ -const receiveContacts = (contacts) => ( +const receiveContacts = (contacts, total, page) => ( (dispatch) => { if (get(contacts, 'length', 0) > 0) { dispatch({ type: 'RECEIVE_CONTACTS', - payload: contacts, + payload: { + contacts, + total, + page, + }, }); } return Promise.resolve(contacts); diff --git a/client/actions/tests/contacts_test.js b/client/actions/tests/contacts_test.js index d5e11c1c3..120f847db 100644 --- a/client/actions/tests/contacts_test.js +++ b/client/actions/tests/contacts_test.js @@ -40,6 +40,7 @@ describe('actions.contacts', () => { default_field: 'first_name', fields: [], query: 'bob*', + default_operator: 'AND', }, }], should: [ @@ -50,10 +51,12 @@ describe('actions.contacts', () => { }, }, sort: '[("first_name", 1)]', + max_results: 200, + page: 1, }]); expect(contactsApi.receiveContacts.callCount).toBe(1); - expect(contactsApi.receiveContacts.args[0]).toEqual([data.contacts.contacts]); + expect(contactsApi.receiveContacts.args[0][0]).toEqual(data.contacts.contacts); done(); }) .catch(done.fail); @@ -72,6 +75,7 @@ describe('actions.contacts', () => { default_field: 'first_name', fields: ['organisation'], query: 'bob*', + default_operator: 'AND', }, }], should: [ @@ -82,10 +86,12 @@ describe('actions.contacts', () => { }, }, sort: '[("first_name", 1)]', + max_results: 200, + page: 1, }]); expect(contactsApi.receiveContacts.callCount).toBe(1); - expect(contactsApi.receiveContacts.args[0]).toEqual([data.contacts.contacts]); + expect(contactsApi.receiveContacts.args[0][0]).toEqual(data.contacts.contacts); done(); }) .catch(done.fail); @@ -104,6 +110,7 @@ describe('actions.contacts', () => { default_field: 'first_name', fields: [], query: 'bob*', + default_operator: 'AND', }, }, { term: {contact_type: 'stringer'}, @@ -116,10 +123,12 @@ describe('actions.contacts', () => { }, }, sort: '[("first_name", 1)]', + max_results: 200, + page: 1, }]); expect(contactsApi.receiveContacts.callCount).toBe(1); - expect(contactsApi.receiveContacts.args[0]).toEqual([data.contacts.contacts]); + expect(contactsApi.receiveContacts.args[0][0]).toEqual(data.contacts.contacts); done(); }) .catch(done.fail); diff --git a/client/components/Contacts/ContactField.jsx b/client/components/Contacts/ContactField.jsx index 4e66a0cc1..d189ab95b 100644 --- a/client/components/Contacts/ContactField.jsx +++ b/client/components/Contacts/ContactField.jsx @@ -5,7 +5,6 @@ import {get} from 'lodash'; import * as selectors from '../../selectors'; import {ContactEditor, SelectSearchContactsField, ContactsPreviewList} from './index'; import * as actions from '../../actions'; -import {CONTACTS} from '../../constants'; import {gettext} from '../../utils/index'; @@ -154,7 +153,6 @@ ContactFieldComponent.propTypes = { PropTypes.arrayOf(PropTypes.string), PropTypes.string, ]), - searchContacts: PropTypes.func, fetchContacts: PropTypes.func, contacts: PropTypes.array, privileges: PropTypes.object, @@ -174,7 +172,6 @@ const mapStateToProps = (state) => ({ }); const mapDispatchToProps = (dispatch) => ({ - searchContacts: (text) => dispatch(actions.contacts.getContacts(text, CONTACTS.SEARCH_FIELDS)), addContact: (newContact) => dispatch(actions.contacts.addContact(newContact)), fetchContacts: (ids) => dispatch(actions.contacts.fetchContactsByIds(ids)), }); diff --git a/client/components/Contacts/SelectSearchContactsField/SelectListPopup.jsx b/client/components/Contacts/SelectSearchContactsField/SelectListPopup.jsx index b9258e207..5a02d13ce 100644 --- a/client/components/Contacts/SelectSearchContactsField/SelectListPopup.jsx +++ b/client/components/Contacts/SelectSearchContactsField/SelectListPopup.jsx @@ -5,6 +5,7 @@ import classNames from 'classnames'; import {get} from 'lodash'; import * as actions from '../../../actions'; +import {contactsLoading, contactsTotal, contactsPage} from '../../../selectors/general'; import {CONTACTS} from '../../../constants'; import {uiUtils, onEventCapture, gettext} from '../../../utils'; @@ -26,6 +27,8 @@ export class SelectListPopupComponent extends React.Component { activeOptionIndex: -1, openFilterList: false, filteredList: [], + options: [], + searchText: '', }; this.dom = { @@ -38,6 +41,13 @@ export class SelectListPopupComponent extends React.Component { this.onAdd = this.onAdd.bind(this); this.openSearchList = this.openSearchList.bind(this); this.filterSearchResults = this.filterSearchResults.bind(this); + this.handleScroll = this.handleScroll.bind(this); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.page < this.props.page && this.dom.listItems) { + this.dom.listItems.scrollTop = 0; + } } onKeyDown(event) { @@ -80,6 +90,21 @@ export class SelectListPopupComponent extends React.Component { uiUtils.scrollListItemIfNeeded(this.state.activeOptionIndex, this.dom.listItems); } + handleScroll(event) { + if (this.props.loading) { + return; + } + + const node = event.target; + const {total, page} = this.props; + + if (node && total > get(this.state.options, 'length', 0)) { + if (node.scrollTop + node.offsetHeight + 100 >= node.scrollHeight) { + this.getSearchResult(this.state.searchText, page + 1); + } + } + } + onAdd(event) { this.closeSearchList(); this.dom.searchField.resetSearch(); @@ -110,10 +135,15 @@ export class SelectListPopupComponent extends React.Component { const valueNoCase = (val || '').toLowerCase(); + if (valueNoCase === this.state.searchText) { + return; + } + this.getSearchResult(valueNoCase); this.setState({ search: true, openFilterList: true, + searchText: valueNoCase, }); } @@ -137,16 +167,20 @@ export class SelectListPopupComponent extends React.Component { filteredList: null, search: false, openFilterList: false, + options: [], + searchText: '', }); } } - getSearchResult(text) { - this.props.searchContacts(text, this.props.contactType) + getSearchResult(text, page = 1) { + this.props.searchContacts(text, this.props.contactType, page) .then((contacts) => { + const allContacts = page <= 1 ? contacts : [...this.state.options, ...contacts]; + this.setState({ - options: contacts, - filteredList: contacts.filter( + options: allContacts, + filteredList: allContacts.filter( (contact) => !this.props.value.find((contactId) => contactId === contact._id) ), }); @@ -164,6 +198,7 @@ export class SelectListPopupComponent extends React.Component { readOnly={this.props.readOnly} placeholder={this.props.placeholder || gettext('Search for a contact')} autoComplete={false} + name="searchFieldInput" /> {this.state.openFilterList && (
-
    this.dom.listItems = node}> +
      this.dom.listItems = node} + onScroll={this.handleScroll} > {get(this.state, 'filteredList.length', 0) > 0 && this.state.filteredList.map((opt, index) => (
    • ({ + loading: contactsLoading(state), + total: contactsTotal(state), + page: contactsPage(state), +}); + const mapDispatchToProps = (dispatch) => ({ - searchContacts: (text, contactType) => dispatch( - actions.contacts.getContacts(text, CONTACTS.SEARCH_FIELDS, contactType) + searchContacts: (text, contactType, page) => dispatch( + actions.contacts.getContacts(text, CONTACTS.SEARCH_FIELDS, contactType, page) ), }); export const SelectListPopup = connect( - null, + mapStateToProps, mapDispatchToProps )(SelectListPopupComponent); diff --git a/client/components/UI/SearchField/index.jsx b/client/components/UI/SearchField/index.jsx index 93197a125..0983e0662 100644 --- a/client/components/UI/SearchField/index.jsx +++ b/client/components/UI/SearchField/index.jsx @@ -76,6 +76,7 @@ export default class SearchField extends React.Component { onFocus={this.props.onFocus} disabled={this.props.readOnly} autoComplete={this.props.autoComplete ? 'on' : 'off'} + name={this.props.name} /> ); } @@ -90,6 +91,7 @@ SearchField.propTypes = { readOnly: PropTypes.bool, placeholder: PropTypes.string, autoComplete: PropTypes.bool, + name: PropTypes.string, }; SearchField.defaultProps = {autoComplete: true}; diff --git a/client/reducers/contacts.js b/client/reducers/contacts.js index 9891ae746..b8bfc4791 100644 --- a/client/reducers/contacts.js +++ b/client/reducers/contacts.js @@ -3,13 +3,31 @@ import {uniqBy} from 'lodash'; const initialState = {contacts: []}; const contacts = (state = initialState, action) => { + let newState; + switch (action.type) { case 'RECEIVE_CONTACTS': - return {contacts: uniqBy([...action.payload, ...state.contacts], '_id')}; + newState = { + contacts: uniqBy([...action.payload.contacts, ...state.contacts], '_id'), + loading: false, + }; + + if (action.payload.total || action.payload.page) { + newState.total = action.payload.total; + newState.page = action.payload.page; + } + + return newState; case 'ADD_CONTACT': return {contacts: uniqBy([action.payload, ...state.contacts], '_id')}; + case 'LOADING_CONTACTS': + return { + ...state, + loading: true, + }; + default: return state; } diff --git a/client/selectors/general.js b/client/selectors/general.js index 2397e43f2..c57efa57a 100644 --- a/client/selectors/general.js +++ b/client/selectors/general.js @@ -71,6 +71,9 @@ export const currentUserId = createSelector( export const files = (state) => get(state, 'files.files'); export const contacts = (state) => get(state, 'contacts.contacts') || []; +export const contactsLoading = (state) => get(state, 'contacts.loading', false); +export const contactsTotal = (state) => get(state, 'contacts.total', 0); +export const contactsPage = (state) => get(state, 'contacts.page', 1); export const contactsById = createSelector( [contacts],