diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12e005ab8..f11f4b067 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
### Added
- Adjust marker icon on selecting a new facility on the vector tiles layer [#749](https://github.com/open-apparel-registry/open-apparel-registry/pull/749)
+- Fetch next page of facilities while scrolling through sidebar list [#750](https://github.com/open-apparel-registry/open-apparel-registry/pull/750)
### Changed
diff --git a/src/app/src/__tests__/utils.tests.js b/src/app/src/__tests__/utils.tests.js
index d2c5aa961..1f2f1b655 100644
--- a/src/app/src/__tests__/utils.tests.js
+++ b/src/app/src/__tests__/utils.tests.js
@@ -61,8 +61,6 @@ const {
makeUserProfileURL,
makeProfileRouteLink,
joinDataIntoCSVString,
- caseInsensitiveIncludes,
- sortFacilitiesAlphabeticallyByName,
updateListWithLabels,
makeSubmitFormOnEnterKeyPressFunction,
makeFacilityListItemsRetrieveCSVItemsURL,
@@ -75,6 +73,7 @@ const {
claimFacilityContactInfoStepIsValid,
claimFacilityFacilityInfoStepIsValid,
anyListItemMatchesAreInactive,
+ pluralizeResultsCount,
} = require('../util/util');
const {
@@ -88,6 +87,7 @@ const {
ENTER_KEY,
facilityListItemStatusChoicesEnum,
facilityListSummaryStatusMessages,
+ FACILITIES_REQUEST_PAGE_SIZE,
} = require('../util/constants');
it('creates a route for checking facility list items', () => {
@@ -140,8 +140,9 @@ it('creates an API URL for getting a single facility by OAR ID', () => {
it('creates an API URL for getting facilities with a query string', () => {
const qs = 'hello=world';
- const expectedMatch = '/api/facilities/?hello=world';
- expect(makeGetFacilitiesURLWithQueryString(qs)).toEqual(expectedMatch);
+ const expectedMatch = `/api/facilities/?hello=world&pageSize=${FACILITIES_REQUEST_PAGE_SIZE}`;
+ expect(makeGetFacilitiesURLWithQueryString(qs, FACILITIES_REQUEST_PAGE_SIZE))
+ .toEqual(expectedMatch);
});
it('gets the value from a React Select option object', () => {
@@ -877,83 +878,6 @@ it('joins a 2-d array into a correctly escaped CSV string', () => {
expect(joinDataIntoCSVString(escapedArray)).toBe(expectedEscapedArrayMatch);
});
-it('checks whether one string includes another regardless of char case', () => {
- const uppercaseTarget = 'HELLOWORLD';
- const lowercaseTest = 'world';
- const lowercaseTarget = 'helloworld';
- const uppercaseTest = 'WORLD';
- const uppercaseNonMatchTest = 'FOO';
- const lowercaseNonMatchTest = 'foo';
-
- expect(caseInsensitiveIncludes(uppercaseTarget, lowercaseTest)).toBe(true);
- expect(caseInsensitiveIncludes(lowercaseTarget, uppercaseTest)).toBe(true);
- expect(caseInsensitiveIncludes(lowercaseTarget, lowercaseTest)).toBe(true);
- expect(caseInsensitiveIncludes(uppercaseTarget, uppercaseTest)).toBe(true);
-
- expect(caseInsensitiveIncludes(uppercaseTarget, lowercaseNonMatchTest)).toBe(false);
- expect(caseInsensitiveIncludes(lowercaseTarget, uppercaseNonMatchTest)).toBe(false);
- expect(caseInsensitiveIncludes(lowercaseTarget, lowercaseNonMatchTest)).toBe(false);
- expect(caseInsensitiveIncludes(uppercaseTarget, uppercaseNonMatchTest)).toBe(false);
-});
-
-it('sorts an array of facilities alphabetically by name without mutating the input', () => {
- const inputData = [
- {
- properties: {
- name: 'hello World',
- },
- },
- {
- properties: {
- name: 'FOO',
- },
- },
- {
- properties: {
- name: 'Bar',
- },
- },
- {
- properties: {
- name: 'baz',
- },
- },
- ];
-
- const expectedSortedData = [
- {
- properties: {
- name: 'Bar',
- },
- },
- {
- properties: {
- name: 'baz',
- },
- },
- {
- properties: {
- name: 'FOO',
- },
- },
- {
- properties: {
- name: 'hello World',
- },
- },
- ];
-
- expect(isEqual(
- sortFacilitiesAlphabeticallyByName(inputData),
- expectedSortedData,
- )).toBe(true);
-
- expect(isEqual(
- inputData,
- expectedSortedData,
- )).toBe(false);
-});
-
it('updates a list of unlabeled values with the correct labels from a given source', () => {
const source = [
{
@@ -1348,3 +1272,11 @@ it('checks a facility list item to see whether any matches have been set to inac
expect(anyListItemMatchesAreInactive(listItemWithInactiveMatches)).toBe(true);
});
+
+it('pluralizes a results count correclty, returning null if count is undefined or null', () => {
+ expect(pluralizeResultsCount(undefined)).toBeNull();
+ expect(pluralizeResultsCount(null)).toBeNull();
+ expect(pluralizeResultsCount(1)).toBe('1 result');
+ expect(pluralizeResultsCount(0)).toBe('0 results');
+ expect(pluralizeResultsCount(200)).toBe('200 results');
+});
diff --git a/src/app/src/actions/facilities.js b/src/app/src/actions/facilities.js
index 1d7cd3c24..f3125dab9 100644
--- a/src/app/src/actions/facilities.js
+++ b/src/app/src/actions/facilities.js
@@ -6,6 +6,8 @@ import apiRequest from '../util/apiRequest';
import { fetchCurrentTileCacheKey } from './vectorTileLayer';
+import { FACILITIES_REQUEST_PAGE_SIZE } from '../util/constants';
+
import {
logErrorAndDispatchFailure,
makeGetFacilitiesURLWithQueryString,
@@ -24,7 +26,10 @@ export const failFetchSingleFacility = createAction('FAIL_FETCH_SINGLE_FACILITY'
export const completeFetchSingleFacility = createAction('COMPLETE_FETCH_SINGLE_FACILITY');
export const resetSingleFacility = createAction('RESET_SINGLE_FACILITY');
-export function fetchFacilities(pushNewRoute = noop) {
+export function fetchFacilities({
+ pageSize = FACILITIES_REQUEST_PAGE_SIZE,
+ pushNewRoute = noop,
+}) {
return (dispatch, getState) => {
dispatch(fetchCurrentTileCacheKey());
dispatch(startFetchFacilities());
@@ -36,7 +41,7 @@ export function fetchFacilities(pushNewRoute = noop) {
const qs = createQueryStringFromSearchFilters(filters);
return apiRequest
- .get(makeGetFacilitiesURLWithQueryString(qs))
+ .get(makeGetFacilitiesURLWithQueryString(qs, pageSize))
.then(({ data }) => {
const responseHasOnlyOneFacility = get(
data,
@@ -65,6 +70,37 @@ export function fetchFacilities(pushNewRoute = noop) {
};
}
+export const startFetchNextPageOfFacilities = createAction('START_FETCH_NEXT_PAGE_OF_FACILITIES');
+export const failFetchNextPageOfFacilities = createAction('FAIL_FETCH_NEXT_PAGE_OF_FACILITIES');
+export const completeFetchNextPageOfFacilities = createAction('COMPLETE_FETCH_NEXT_PAGE_OF_FACILITIES');
+
+export function fetchNextPageOfFacilities() {
+ return (dispatch, getState) => {
+ const {
+ facilities: {
+ facilities: {
+ nextPageURL,
+ },
+ },
+ } = getState();
+
+ if (!nextPageURL) {
+ return noop();
+ }
+
+ dispatch(startFetchNextPageOfFacilities());
+
+ return apiRequest
+ .get(nextPageURL)
+ .then(({ data }) => dispatch(completeFetchNextPageOfFacilities(data)))
+ .catch(err => dispatch(logErrorAndDispatchFailure(
+ err,
+ 'An error prevented fetching the next page of facilities',
+ failFetchNextPageOfFacilities,
+ )));
+ };
+}
+
export function fetchSingleFacility(oarID = null) {
return (dispatch) => {
dispatch(startFetchSingleFacility());
diff --git a/src/app/src/actions/ui.js b/src/app/src/actions/ui.js
index 58c41ce21..aa169d7e1 100644
--- a/src/app/src/actions/ui.js
+++ b/src/app/src/actions/ui.js
@@ -4,13 +4,13 @@ export const makeSidebarGuideTabActive = createAction('MAKE_SIDEBAR_GUIDE_TAB_AC
export const makeSidebarSearchTabActive = createAction('MAKE_SIDEBAR_SEARCH_TAB_ACTIVE');
export const makeSidebarFacilitiesTabActive = createAction('MAKE_SIDEBAR_FACILITIES_TAB_ACTIVE');
-export const updateSidebarFacilitiesTabTextFilter =
- createAction('UPDATE_SIDEBAR_FACILITIES_TAB_TEXT_FILTER');
-export const resetSidebarFacilitiesTabTextFilter =
- createAction('RESET_SIDEBAR_FACILITIES_TAB_TEXT_FILTER');
-
export const recordSearchTabResetButtonClick =
createAction('RECORD_SEARCH_TAB_RESET_BUTTON_CLICK');
export const reportWindowResize =
createAction('REPORT_WINDOW_RESIZE');
+
+export const updateSidebarFacilitiesTabTextFilter =
+ createAction('UPDATE_SIDEBAR_FACILITIES_TAB_TEXT_FILTER');
+export const resetSidebarFacilitiesTabTextFilter =
+ createAction('RESET_SIDEBAR_FACILITIES_TAB_TEXT_FILTER');
diff --git a/src/app/src/components/FilterSidebar.jsx b/src/app/src/components/FilterSidebar.jsx
index 44626c922..988a5b4cf 100644
--- a/src/app/src/components/FilterSidebar.jsx
+++ b/src/app/src/components/FilterSidebar.jsx
@@ -9,10 +9,13 @@ import { Route } from 'react-router-dom';
import FilterSidebarGuideTab from './FilterSidebarGuideTab';
import FilterSidebarSearchTab from './FilterSidebarSearchTab';
import FilterSidebarFacilitiesTab from './FilterSidebarFacilitiesTab';
+import NonVectorTileFilterSidebarFacilitiesTab from './NonVectorTileFilterSidebarFacilitiesTab';
+import FeatureFlag from './FeatureFlag';
import {
filterSidebarTabsEnum,
filterSidebarTabs,
+ VECTOR_TILE,
} from '../util/constants';
import {
@@ -136,7 +139,14 @@ class FilterSidebar extends Component {
// in its `mapDispatchToProps` function.
return ;
case filterSidebarTabsEnum.facilities:
- return ;
+ return (
+ }
+ >
+
+
+ );
default:
window.console.warn('invalid tab selection', activeFilterSidebarTab);
return null;
diff --git a/src/app/src/components/FilterSidebarFacilitiesTab.jsx b/src/app/src/components/FilterSidebarFacilitiesTab.jsx
index a7c5a9864..1c9607080 100644
--- a/src/app/src/components/FilterSidebarFacilitiesTab.jsx
+++ b/src/app/src/components/FilterSidebarFacilitiesTab.jsx
@@ -17,13 +17,12 @@ import get from 'lodash/get';
import { toast } from 'react-toastify';
import InfiniteAnyHeight from 'react-infinite-any-height';
-import ControlledTextInput from './ControlledTextInput';
-
import {
makeSidebarSearchTabActive,
- updateSidebarFacilitiesTabTextFilter,
} from '../actions/ui';
+import { fetchNextPageOfFacilities } from '../actions/facilities';
+
import { logDownload } from '../actions/logDownload';
import { facilityCollectionPropType } from '../util/propTypes';
@@ -35,17 +34,13 @@ import {
import {
makeFacilityDetailLink,
- getValueFromEvent,
- caseInsensitiveIncludes,
- sortFacilitiesAlphabeticallyByName,
+ pluralizeResultsCount,
} from '../util/util';
import COLOURS from '../util/COLOURS';
import { filterSidebarStyles } from '../util/styles';
-const SEARCH_TERM_INPUT = 'SEARCH_TERM_INPUT';
-
const facilitiesTabStyles = Object.freeze({
noResultsTextStyles: Object.freeze({
margin: '30px',
@@ -95,9 +90,9 @@ function FilterSidebarFacilitiesTab({
logDownloadError,
user,
returnToSearchTab,
- filterText,
- updateFilterText,
handleDownload,
+ fetchNextPage,
+ isInfiniteLoading,
}) {
const [loginRequiredDialogIsOpen, setLoginRequiredDialogIsOpen] = useState(false);
const [requestedDownload, setRequestedDownload] = useState(false);
@@ -188,27 +183,9 @@ function FilterSidebarFacilitiesTab({
);
}
- const filteredFacilities = filterText
- ? facilities
- .filter(({
- properties: {
- address,
- name,
- country_name: countryName,
- },
- }) => caseInsensitiveIncludes(address, filterText)
- || caseInsensitiveIncludes(name, filterText)
- || caseInsensitiveIncludes(countryName, filterText))
- : facilities;
-
- const orderedFacilities =
- sortFacilitiesAlphabeticallyByName(filteredFacilities);
-
const facilitiesCount = get(data, 'count', null);
- const headerDisplayString = facilitiesCount && (facilitiesCount !== filteredFacilities.length)
- ? `Displaying ${filteredFacilities.length} facilities of ${facilitiesCount} results`
- : `Displaying ${filteredFacilities.length} facilities`;
+ const headerDisplayString = pluralizeResultsCount(facilitiesCount);
const LoginLink = props => ;
const RegisterLink = props => ;
@@ -242,19 +219,6 @@ function FilterSidebarFacilitiesTab({
-
-
-
-
);
@@ -265,6 +229,15 @@ function FilterSidebarFacilitiesTab({
const resultListHeight = windowHeight - nonResultListComponentHeight;
+ const loadingElement = (facilities.length !== facilitiesCount) && (
+
+
+
+
+
+
+ );
+
return (
{listHeaderInsetComponent}
@@ -272,8 +245,12 @@ function FilterSidebarFacilitiesTab({
dispatch(makeSidebarSearchTabActive()),
- updateFilterText: e =>
- dispatch((updateSidebarFacilitiesTabTextFilter(getValueFromEvent(e)))),
handleDownload: () => dispatch(logDownload()),
+ fetchNextPage: () => dispatch(fetchNextPageOfFacilities()),
};
}
diff --git a/src/app/src/components/FilterSidebarSearchTab.jsx b/src/app/src/components/FilterSidebarSearchTab.jsx
index 69f2b97d9..7f85a14d0 100644
--- a/src/app/src/components/FilterSidebarSearchTab.jsx
+++ b/src/app/src/components/FilterSidebarSearchTab.jsx
@@ -6,6 +6,7 @@ import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';
import ReactSelect from 'react-select';
+import get from 'lodash/get';
import FacilitySidebarSearchTabFacilitiesCount from './FacilitySidebarSearchTabFacilitiesCount';
@@ -35,6 +36,8 @@ import {
makeSubmitFormOnEnterKeyPressFunction,
} from '../util/util';
+import { FACILITIES_REQUEST_PAGE_SIZE } from '../util/constants';
+
const filterSidebarSearchTabStyles = Object.freeze({
formStyle: Object.freeze({
width: '100%',
@@ -72,6 +75,7 @@ function FilterSidebarSearchTab({
facilities,
fetchingOptions,
submitFormOnEnterKeyPress,
+ vectorTileFlagIsActive,
}) {
if (fetchingOptions) {
return (
@@ -219,7 +223,7 @@ function FilterSidebarSearchTab({
color="primary"
className="margin-left-16 blue-background"
style={{ boxShadow: 'none' }}
- onClick={searchForFacilities}
+ onClick={() => searchForFacilities(vectorTileFlagIsActive)}
disabled={fetchingOptions}
>
Search
@@ -255,6 +259,7 @@ FilterSidebarSearchTab.propTypes = {
searchForFacilities: func.isRequired,
facilities: facilityCollectionPropType,
fetchingOptions: bool.isRequired,
+ vectorTileFlagIsActive: bool.isRequired,
};
function mapStateToProps({
@@ -284,8 +289,12 @@ function mapStateToProps({
fetching: fetchingFacilities,
},
},
+ featureFlags,
}) {
+ const vectorTileFlagIsActive = get(featureFlags, 'flags.vector_tile', false);
+
return {
+ vectorTileFlagIsActive,
contributorOptions,
contributorTypeOptions,
countryOptions,
@@ -316,7 +325,10 @@ function mapDispatchToProps(dispatch, {
dispatch(recordSearchTabResetButtonClick());
return dispatch(resetAllFilters());
},
- searchForFacilities: () => dispatch(fetchFacilities(push)),
+ searchForFacilities: vectorTilesAreActive => dispatch(fetchFacilities({
+ pageSize: vectorTilesAreActive ? FACILITIES_REQUEST_PAGE_SIZE : 500,
+ pushNewRoute: push,
+ })),
submitFormOnEnterKeyPress: makeSubmitFormOnEnterKeyPressFunction(
() => dispatch(fetchFacilities(push)),
),
diff --git a/src/app/src/components/NonVectorTileFilterSidebarFacilitiesTab.jsx b/src/app/src/components/NonVectorTileFilterSidebarFacilitiesTab.jsx
new file mode 100644
index 000000000..1d7e0ed64
--- /dev/null
+++ b/src/app/src/components/NonVectorTileFilterSidebarFacilitiesTab.jsx
@@ -0,0 +1,446 @@
+import React, { Fragment, useState, useEffect } from 'react';
+import { arrayOf, bool, func, number, string } from 'prop-types';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import Dialog from '@material-ui/core/Dialog';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogActions from '@material-ui/core/DialogActions';
+import Typography from '@material-ui/core/Typography';
+import Button from '@material-ui/core/Button';
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemText from '@material-ui/core/ListItemText';
+import Divider from '@material-ui/core/Divider';
+import get from 'lodash/get';
+import { toast } from 'react-toastify';
+import InfiniteAnyHeight from 'react-infinite-any-height';
+import includes from 'lodash/includes';
+import lowerCase from 'lodash/lowerCase';
+
+import ControlledTextInput from './ControlledTextInput';
+
+import {
+ makeSidebarSearchTabActive,
+ updateSidebarFacilitiesTabTextFilter,
+} from '../actions/ui';
+
+import { logDownload } from '../actions/logDownload';
+
+import { facilityCollectionPropType } from '../util/propTypes';
+
+import {
+ authLoginFormRoute,
+ authRegisterFormRoute,
+} from '../util/constants';
+
+import {
+ makeFacilityDetailLink,
+ getValueFromEvent,
+} from '../util/util';
+
+import COLOURS from '../util/COLOURS';
+
+import { filterSidebarStyles } from '../util/styles';
+
+const SEARCH_TERM_INPUT = 'SEARCH_TERM_INPUT';
+
+const caseInsensitiveIncludes = (target, test) =>
+ includes(lowerCase(target), lowerCase(test));
+
+const sortFacilitiesAlphabeticallyByName = data => data
+ .slice()
+ .sort((
+ {
+ properties: {
+ name: firstFacilityName,
+ },
+ },
+ {
+ properties: {
+ name: secondFacilityName,
+ },
+ },
+ ) => {
+ const a = lowerCase(firstFacilityName);
+ const b = lowerCase(secondFacilityName);
+
+ if (a === b) {
+ return 0;
+ }
+
+ return (a < b) ? -1 : 1;
+ });
+
+const facilitiesTabStyles = Object.freeze({
+ noResultsTextStyles: Object.freeze({
+ margin: '30px',
+ }),
+ linkStyles: Object.freeze({
+ display: 'flex',
+ textDecoration: 'none',
+ }),
+ listItemStyles: Object.freeze({
+ wordWrap: 'anywhere',
+ }),
+ listHeaderStyles: Object.freeze({
+ backgroundColor: COLOURS.WHITE,
+ padding: '0.25rem',
+ maxHeight: '130px',
+ }),
+ listStyles: Object.freeze({
+ /* overflowY: 'scroll',
+ position: 'fixed',
+ // sum heights of navbar, tab bar, panel header, and free text search control
+ top: 'calc(64px + 48px + 130px + 110px)',
+ bottom: '47px',
+ width: '33%', */
+ }),
+ listBottomPaddingStyles: Object.freeze({
+ height: '200px',
+ }),
+ titleRowStyles: Object.freeze({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: '6px 1rem',
+ }),
+ listHeaderTextSearchStyles: Object.freeze({
+ padding: '6px 1rem',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ }),
+});
+
+function NonVectorTileFilterSidebarFacilitiesTab({
+ fetching,
+ data,
+ error,
+ windowHeight,
+ logDownloadError,
+ user,
+ returnToSearchTab,
+ filterText,
+ updateFilterText,
+ handleDownload,
+}) {
+ const [loginRequiredDialogIsOpen, setLoginRequiredDialogIsOpen] = useState(false);
+ const [requestedDownload, setRequestedDownload] = useState(false);
+
+ useEffect(() => {
+ if (requestedDownload && logDownloadError) {
+ toast('A problem prevented downloading the facilities');
+ setRequestedDownload(false);
+ }
+ }, [logDownloadError, requestedDownload]);
+
+ if (fetching) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ An error prevented fetching facilities
+
+
+
+
+
+
+ );
+ }
+
+ const facilities = get(data, 'features', []);
+
+ if (!facilities.length) {
+ return (
+
+
+
+ No facilities to display
+
+
+
+
+
+
+ );
+ }
+
+ const filteredFacilities = filterText
+ ? facilities
+ .filter(({
+ properties: {
+ address,
+ name,
+ country_name: countryName,
+ },
+ }) => caseInsensitiveIncludes(address, filterText)
+ || caseInsensitiveIncludes(name, filterText)
+ || caseInsensitiveIncludes(countryName, filterText))
+ : facilities;
+
+ const orderedFacilities =
+ sortFacilitiesAlphabeticallyByName(filteredFacilities);
+
+ const facilitiesCount = get(data, 'count', null);
+
+ const headerDisplayString = facilitiesCount && (facilitiesCount !== filteredFacilities.length)
+ ? `Displaying ${filteredFacilities.length} facilities of ${facilitiesCount} results`
+ : `Displaying ${filteredFacilities.length} facilities`;
+
+ const LoginLink = props => ;
+ const RegisterLink = props => ;
+
+ const listHeaderInsetComponent = (
+
+
+
+ {headerDisplayString}
+
+
+
+
+
+
+
+
+ );
+
+ const nonResultListComponentHeight = (
+ Array.from(document.getElementsByClassName('results-height-subtract'))
+ .reduce((sum, x) => sum + x.offsetHeight, 0)
+ );
+
+ const resultListHeight = windowHeight - nonResultListComponentHeight;
+
+ return (
+
+ {listHeaderInsetComponent}
+
+
+ (
+
+
+
+
+
+
+
+ ))
+ }
+ />
+
+
+
+
+ );
+}
+
+NonVectorTileFilterSidebarFacilitiesTab.defaultProps = {
+ data: null,
+ error: null,
+ logDownloadError: null,
+};
+
+NonVectorTileFilterSidebarFacilitiesTab.propTypes = {
+ data: facilityCollectionPropType,
+ fetching: bool.isRequired,
+ error: arrayOf(string),
+ windowHeight: number.isRequired,
+ logDownloadError: arrayOf(string),
+ returnToSearchTab: func.isRequired,
+ filterText: string.isRequired,
+ updateFilterText: func.isRequired,
+ handleDownload: func.isRequired,
+};
+
+function mapStateToProps({
+ auth: {
+ user: {
+ user,
+ },
+ },
+ facilities: {
+ facilities: {
+ data,
+ error,
+ fetching,
+ },
+ },
+ ui: {
+ facilitiesSidebarTabSearch: {
+ filterText,
+ },
+ window: {
+ innerHeight: windowHeight,
+ },
+ },
+ logDownload: {
+ error: logDownloadError,
+ },
+}) {
+ return {
+ data,
+ error,
+ fetching,
+ filterText,
+ user,
+ logDownloadError,
+ windowHeight,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ returnToSearchTab: () => dispatch(makeSidebarSearchTabActive()),
+ updateFilterText: e =>
+ dispatch((updateSidebarFacilitiesTabTextFilter(getValueFromEvent(e)))),
+ handleDownload: () => dispatch(logDownload()),
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(NonVectorTileFilterSidebarFacilitiesTab);
diff --git a/src/app/src/reducers/FacilitiesReducer.js b/src/app/src/reducers/FacilitiesReducer.js
index b356a0003..4d01f6872 100644
--- a/src/app/src/reducers/FacilitiesReducer.js
+++ b/src/app/src/reducers/FacilitiesReducer.js
@@ -13,6 +13,9 @@ import {
failFetchSingleFacility,
completeFetchSingleFacility,
resetSingleFacility,
+ startFetchNextPageOfFacilities,
+ failFetchNextPageOfFacilities,
+ completeFetchNextPageOfFacilities,
} from '../actions/facilities';
import {
@@ -33,6 +36,8 @@ const initialState = Object.freeze({
data: null,
fetching: false,
error: null,
+ nextPageURL: null,
+ isInfiniteLoading: false,
}),
singleFacility: Object.freeze({
data: null,
@@ -100,6 +105,30 @@ export default createReducer({
fetching: { $set: false },
error: { $set: null },
data: { $set: payload },
+ nextPageURL: { $set: get(payload, 'next', null) },
+ },
+ }),
+ [startFetchNextPageOfFacilities]: state => update(state, {
+ facilities: {
+ isInfiniteLoading: { $set: true },
+ },
+ }),
+ [failFetchNextPageOfFacilities]: state => update(state, {
+ facilities: {
+ isInfiniteLoading: { $set: false },
+ },
+ }),
+ [completeFetchNextPageOfFacilities]: (state, payload) => update(state, {
+ facilities: {
+ fetching: { $set: false },
+ error: { $set: null },
+ data: {
+ features: {
+ $push: get(payload, 'features', []),
+ },
+ },
+ nextPageURL: { $set: get(payload, 'next', null) },
+ isInfiniteLoading: { $set: false },
},
}),
[resetFacilities]: state => update(state, {
diff --git a/src/app/src/util/constants.js b/src/app/src/util/constants.js
index 6ea5dd526..1bb70a392 100644
--- a/src/app/src/util/constants.js
+++ b/src/app/src/util/constants.js
@@ -1,5 +1,7 @@
export const OTHER = 'Other';
+export const FACILITIES_REQUEST_PAGE_SIZE = 25;
+
// This choices must be kept in sync with the identical list
// kept in the Django API's models.py file
export const contributorTypeOptions = Object.freeze([
diff --git a/src/app/src/util/util.js b/src/app/src/util/util.js
index 457ea380a..cfc10b61a 100644
--- a/src/app/src/util/util.js
+++ b/src/app/src/util/util.js
@@ -10,6 +10,7 @@ import negate from 'lodash/negate';
import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty';
import isNumber from 'lodash/isNumber';
+import isNil from 'lodash/isNil';
import values from 'lodash/values';
import flow from 'lodash/flow';
import noop from 'lodash/noop';
@@ -18,8 +19,6 @@ import startsWith from 'lodash/startsWith';
import head from 'lodash/head';
import replace from 'lodash/replace';
import trimEnd from 'lodash/trimEnd';
-import includes from 'lodash/includes';
-import lowerCase from 'lodash/lowerCase';
import range from 'lodash/range';
import ceil from 'lodash/ceil';
import toInteger from 'lodash/toInteger';
@@ -93,7 +92,7 @@ export const makeGetCountriesURL = () => '/api/countries/';
export const makeGetFacilitiesURL = () => '/api/facilities/';
export const makeGetFacilityByOARIdURL = oarId => `/api/facilities/${oarId}/`;
-export const makeGetFacilitiesURLWithQueryString = qs => `/api/facilities/?${qs}`;
+export const makeGetFacilitiesURLWithQueryString = (qs, pageSize) => `/api/facilities/?${qs}&pageSize=${pageSize}`;
export const makeClaimFacilityAPIURL = oarId => `/api/facilities/${oarId}/claim/`;
export const makeSplitFacilityAPIURL = oarID => `/api/facilities/${oarID}/split/`;
export const makePromoteFacilityMatchAPIURL = oarID => `/api/facilities/${oarID}/promote/`;
@@ -485,33 +484,6 @@ export const joinDataIntoCSVString = data => data
);
}, '');
-export const caseInsensitiveIncludes = (target, test) =>
- includes(lowerCase(target), lowerCase(test));
-
-export const sortFacilitiesAlphabeticallyByName = data => data
- .slice()
- .sort((
- {
- properties: {
- name: firstFacilityName,
- },
- },
- {
- properties: {
- name: secondFacilityName,
- },
- },
- ) => {
- const a = lowerCase(firstFacilityName);
- const b = lowerCase(secondFacilityName);
-
- if (a === b) {
- return 0;
- }
-
- return (a < b) ? -1 : 1;
- });
-
// Given a list where each item is like { label: 'ABCD', value: 123 }, and
// a payload which is a list of items like { label: '123', value: 123 },
// returns a list of items from the payload with their labels replaced with
@@ -632,3 +604,15 @@ export const claimFacilityFacilityInfoStepIsValid = ({
]);
export const anyListItemMatchesAreInactive = ({ matches }) => some(matches, ['is_active', false]);
+
+export const pluralizeResultsCount = (count) => {
+ if (isNil(count)) {
+ return null;
+ }
+
+ if (count === 1) {
+ return '1 result';
+ }
+
+ return `${count} results`;
+};
diff --git a/src/django/api/views.py b/src/django/api/views.py
index 7777cd566..890e6cc91 100644
--- a/src/django/api/views.py
+++ b/src/django/api/views.py
@@ -558,7 +558,8 @@ def list(self, request):
queryset = Facility \
.objects \
- .filter_by_query_params(request.query_params)
+ .filter_by_query_params(request.query_params) \
+ .order_by('name')
page_queryset = self.paginate_queryset(queryset)