2019-12-04 23:09:02 +07:00
|
|
|
import React, { useState, FormEvent, useCallback } from 'react';
|
2019-02-03 17:23:33 +07:00
|
|
|
import debounce from 'lodash/debounce';
|
2019-12-04 23:09:02 +07:00
|
|
|
import { RouteComponentProps, withRouter } from 'react-router';
|
|
|
|
import { SuggestionSelectedEventData } from 'react-autosuggest';
|
2019-02-03 17:23:33 +07:00
|
|
|
|
|
|
|
import AutoComplete from '../AutoComplete';
|
2019-08-25 22:39:15 +07:00
|
|
|
import { callSearch } from '../../utils/calls';
|
2019-02-03 17:23:33 +07:00
|
|
|
|
2019-12-04 23:09:02 +07:00
|
|
|
import SearchAdornment from './SearchAdornment';
|
2019-02-03 17:23:33 +07:00
|
|
|
|
|
|
|
const CONSTANTS = {
|
|
|
|
API_DELAY: 300,
|
|
|
|
PLACEHOLDER_TEXT: 'Search Packages',
|
|
|
|
ABORT_ERROR: 'AbortError',
|
|
|
|
};
|
|
|
|
|
2019-12-04 23:09:02 +07:00
|
|
|
const Search: React.FC<RouteComponentProps> = ({ history }) => {
|
|
|
|
const [suggestions, setSuggestions] = useState([]);
|
|
|
|
const [loaded, setLoaded] = useState(false);
|
|
|
|
const [search, setSearch] = useState('');
|
|
|
|
const [error, setError] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [requestList, setRequestList] = useState<Array<{ abort: () => void }>>([]);
|
2019-06-30 05:39:56 +07:00
|
|
|
|
2019-02-03 17:23:33 +07:00
|
|
|
/**
|
|
|
|
* Cancel all the requests which are in pending state.
|
|
|
|
*/
|
2019-12-04 23:09:02 +07:00
|
|
|
const cancelAllSearchRequests = useCallback(() => {
|
|
|
|
requestList.forEach(request => request.abort());
|
|
|
|
setRequestList([]);
|
|
|
|
}, [requestList, setRequestList]);
|
2019-02-03 17:23:33 +07:00
|
|
|
|
|
|
|
/**
|
2019-12-04 23:09:02 +07:00
|
|
|
* As user focuses out from input, we cancel all the request from requestList
|
|
|
|
* and set the API state parameters to default boolean values.
|
2019-02-03 17:23:33 +07:00
|
|
|
*/
|
2019-12-04 23:09:02 +07:00
|
|
|
const handleOnBlur = useCallback(
|
|
|
|
(event: FormEvent<HTMLInputElement>) => {
|
|
|
|
// stops event bubbling
|
|
|
|
event.stopPropagation();
|
|
|
|
setLoaded(false);
|
|
|
|
setLoading(false);
|
|
|
|
setError(false);
|
|
|
|
cancelAllSearchRequests();
|
|
|
|
},
|
|
|
|
[setLoaded, setLoading, cancelAllSearchRequests, setError]
|
|
|
|
);
|
2019-02-03 17:23:33 +07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* onChange method for the input element.
|
|
|
|
*/
|
2019-12-04 23:09:02 +07:00
|
|
|
const handleSearch = useCallback(
|
|
|
|
(event: FormEvent<HTMLInputElement>, { newValue, method }) => {
|
|
|
|
// stops event bubbling
|
|
|
|
event.stopPropagation();
|
|
|
|
if (method === 'type') {
|
|
|
|
const value = newValue.trim();
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
setError(false);
|
|
|
|
setSearch(value);
|
|
|
|
setLoaded(false);
|
|
|
|
/**
|
|
|
|
* A use case where User keeps adding and removing value in input field,
|
|
|
|
* so we cancel all the existing requests when input is empty.
|
|
|
|
*/
|
|
|
|
if (value.length === 0) {
|
|
|
|
cancelAllSearchRequests();
|
2019-02-03 17:23:33 +07:00
|
|
|
}
|
2019-12-04 23:09:02 +07:00
|
|
|
}
|
|
|
|
},
|
|
|
|
[cancelAllSearchRequests]
|
|
|
|
);
|
2019-02-03 17:23:33 +07:00
|
|
|
|
|
|
|
/**
|
2019-12-04 23:09:02 +07:00
|
|
|
* Cancel all the request from list and make request list empty.
|
2019-02-03 17:23:33 +07:00
|
|
|
*/
|
2019-12-04 23:09:02 +07:00
|
|
|
const handlePackagesClearRequested = useCallback(() => {
|
|
|
|
setSuggestions([]);
|
|
|
|
}, [setSuggestions]);
|
2019-02-03 17:23:33 +07:00
|
|
|
|
|
|
|
/**
|
2019-12-04 23:09:02 +07:00
|
|
|
* When an user select any package by clicking or pressing return key.
|
2019-02-03 17:23:33 +07:00
|
|
|
*/
|
2019-12-04 23:09:02 +07:00
|
|
|
const handleClickSearch = useCallback(
|
|
|
|
(
|
|
|
|
event: FormEvent<HTMLInputElement>,
|
|
|
|
{ suggestionValue, method }: SuggestionSelectedEventData<unknown>
|
|
|
|
): void | undefined => {
|
|
|
|
// stops event bubbling
|
|
|
|
event.stopPropagation();
|
|
|
|
switch (method) {
|
|
|
|
case 'click':
|
|
|
|
case 'enter':
|
|
|
|
setSearch('');
|
|
|
|
history.push(`/-/web/detail/${suggestionValue}`);
|
|
|
|
break;
|
2019-02-03 17:23:33 +07:00
|
|
|
}
|
2019-12-04 23:09:02 +07:00
|
|
|
},
|
|
|
|
[history]
|
|
|
|
);
|
2019-06-20 19:37:28 +07:00
|
|
|
|
2019-02-03 17:23:33 +07:00
|
|
|
/**
|
2019-12-04 23:09:02 +07:00
|
|
|
* Fetch packages from API.
|
|
|
|
* For AbortController see: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
|
2019-02-03 17:23:33 +07:00
|
|
|
*/
|
2019-12-04 23:09:02 +07:00
|
|
|
const handleFetchPackages = useCallback(
|
|
|
|
async ({ value }: { value: string }) => {
|
|
|
|
try {
|
|
|
|
const controller = new window.AbortController();
|
|
|
|
const signal = controller.signal;
|
|
|
|
// Keep track of search requests.
|
|
|
|
setRequestList([...requestList, controller]);
|
|
|
|
const suggestions = await callSearch(value, signal);
|
|
|
|
// @ts-ignore FIXME: Argument of type 'unknown' is not assignable to parameter of type 'SetStateAction<never[]>'
|
|
|
|
setSuggestions(suggestions);
|
|
|
|
setLoaded(true);
|
|
|
|
} catch (error) {
|
|
|
|
/**
|
|
|
|
* AbortError is not the API error.
|
|
|
|
* It means browser has cancelled the API request.
|
|
|
|
*/
|
|
|
|
if (error.name === CONSTANTS.ABORT_ERROR) {
|
|
|
|
setError(false);
|
|
|
|
setLoaded(false);
|
|
|
|
} else {
|
|
|
|
setError(true);
|
|
|
|
setLoaded(false);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
setLoading(false);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[requestList, setRequestList, setSuggestions, setLoaded, setError, setLoading]
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<AutoComplete
|
|
|
|
onBlur={handleOnBlur}
|
|
|
|
onChange={handleSearch}
|
|
|
|
onCleanSuggestions={handlePackagesClearRequested}
|
|
|
|
onClick={handleClickSearch}
|
|
|
|
onSuggestionsFetch={debounce(handleFetchPackages, CONSTANTS.API_DELAY)}
|
|
|
|
placeholder={CONSTANTS.PLACEHOLDER_TEXT}
|
|
|
|
startAdornment={<SearchAdornment />}
|
|
|
|
suggestions={suggestions}
|
|
|
|
suggestionsError={error}
|
|
|
|
suggestionsLoaded={loaded}
|
|
|
|
suggestionsLoading={loading}
|
|
|
|
value={search}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
2019-02-03 17:23:33 +07:00
|
|
|
|
|
|
|
export default withRouter(Search);
|