Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Fetch next page of facilities via infinite scroll component #750

Merged
merged 2 commits into from
Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
91 changes: 11 additions & 80 deletions src/app/src/__tests__/utils.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ const {
makeUserProfileURL,
makeProfileRouteLink,
joinDataIntoCSVString,
caseInsensitiveIncludes,
sortFacilitiesAlphabeticallyByName,
updateListWithLabels,
makeSubmitFormOnEnterKeyPressFunction,
makeFacilityListItemsRetrieveCSVItemsURL,
Expand All @@ -75,6 +73,7 @@ const {
claimFacilityContactInfoStepIsValid,
claimFacilityFacilityInfoStepIsValid,
anyListItemMatchesAreInactive,
pluralizeResultsCount,
} = require('../util/util');

const {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -140,7 +140,7 @@ 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';
const expectedMatch = `/api/facilities/?hello=world&pageSize=${FACILITIES_REQUEST_PAGE_SIZE}`;
expect(makeGetFacilitiesURLWithQueryString(qs)).toEqual(expectedMatch);
});

Expand Down Expand Up @@ -877,83 +877,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 = [
{
Expand Down Expand Up @@ -1348,3 +1271,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');
});
31 changes: 31 additions & 0 deletions src/app/src/actions/facilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a benefit to return noop(); instead of the equivalent return;?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe they're basically equivalent insofar as each will make the return value undefined, but I think the explicit return noop() is a little more readable when scanning through the code.

}

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());
Expand Down
5 changes: 0 additions & 5 deletions src/app/src/actions/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ 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');

Expand Down
72 changes: 25 additions & 47 deletions src/app/src/components/FilterSidebarFacilitiesTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand Down Expand Up @@ -95,9 +90,9 @@ function FilterSidebarFacilitiesTab({
logDownloadError,
user,
returnToSearchTab,
filterText,
updateFilterText,
handleDownload,
fetchNextPage,
isInfiniteLoading,
}) {
const [loginRequiredDialogIsOpen, setLoginRequiredDialogIsOpen] = useState(false);
const [requestedDownload, setRequestedDownload] = useState(false);
Expand Down Expand Up @@ -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 => <Link to={authLoginFormRoute} {...props} />;
const RegisterLink = props => <Link to={authRegisterFormRoute} {...props} />;
Expand Down Expand Up @@ -242,19 +219,6 @@ function FilterSidebarFacilitiesTab({
</Button>
</div>
</Typography>
<div style={facilitiesTabStyles.listHeaderTextSearchStyles}>
<label
htmlFor={SEARCH_TERM_INPUT}
>
Filter results
</label>
<ControlledTextInput
id={SEARCH_TERM_INPUT}
value={filterText}
onChange={updateFilterText}
placeholder="Filter by name, address, or country"
/>
</div>
</div>
);

Expand All @@ -265,15 +229,28 @@ function FilterSidebarFacilitiesTab({

const resultListHeight = windowHeight - nonResultListComponentHeight;

const loadingElement = (facilities.length !== facilitiesCount) && (
<Fragment>
<Divider />
<ListItem style={facilitiesTabStyles.listItemStyles}>
<ListItemText primary="Loading more facilities..." />
</ListItem>
</Fragment>
);

return (
<Fragment>
{listHeaderInsetComponent}
<div style={filterSidebarStyles.controlPanelContentStyles}>
<List style={facilitiesTabStyles.listStyles}>
<InfiniteAnyHeight
containerHeight={resultListHeight}
infiniteLoadBeginEdgeOffset={100}
isInfiniteLoading={fetching || isInfiniteLoading}
onInfiniteLoad={fetchNextPage}
loadingSpinnerDelegate={loadingElement}
list={
orderedFacilities
facilities
.map(({
properties: {
address,
Expand Down Expand Up @@ -363,9 +340,9 @@ FilterSidebarFacilitiesTab.propTypes = {
windowHeight: number.isRequired,
logDownloadError: arrayOf(string),
returnToSearchTab: func.isRequired,
filterText: string.isRequired,
updateFilterText: func.isRequired,
handleDownload: func.isRequired,
fetchNextPage: func.isRequired,
isInfiniteLoading: bool.isRequired,
};

function mapStateToProps({
Expand All @@ -379,6 +356,7 @@ function mapStateToProps({
data,
error,
fetching,
isInfiniteLoading,
},
},
ui: {
Expand All @@ -401,15 +379,15 @@ function mapStateToProps({
user,
logDownloadError,
windowHeight,
isInfiniteLoading,
};
}

function mapDispatchToProps(dispatch) {
return {
returnToSearchTab: () => dispatch(makeSidebarSearchTabActive()),
updateFilterText: e =>
dispatch((updateSidebarFacilitiesTabTextFilter(getValueFromEvent(e)))),
handleDownload: () => dispatch(logDownload()),
fetchNextPage: () => dispatch(fetchNextPageOfFacilities()),
};
}

Expand Down