diff --git a/js/actions/piece_list_actions.js b/js/actions/piece_list_actions.js index 2a4464af..7ef9cb59 100644 --- a/js/actions/piece_list_actions.js +++ b/js/actions/piece_list_actions.js @@ -5,6 +5,7 @@ import Q from 'q'; import PieceListFetcher from '../fetchers/piece_list_fetcher'; + class PieceListActions { constructor() { this.generateActions( @@ -21,17 +22,16 @@ class PieceListActions { this.actions.updatePieceList({ page, pageSize, - search, orderBy, orderAsc, filterBy, + search: '', pieceList: [], pieceListCount: -1, unfilteredPieceListCount: -1 }); // afterwards, we can load the list - return Q.Promise((resolve, reject) => { PieceListFetcher .fetch(page, pageSize, search, orderBy, orderAsc, filterBy) diff --git a/js/components/ascribe_accordion_list/accordion_list.js b/js/components/ascribe_accordion_list/accordion_list.js index fe300702..c488a3d8 100644 --- a/js/components/ascribe_accordion_list/accordion_list.js +++ b/js/components/ascribe_accordion_list/accordion_list.js @@ -9,21 +9,49 @@ let AccordionList = React.createClass({ className: React.PropTypes.string, children: React.PropTypes.arrayOf(React.PropTypes.element).isRequired, loadingElement: React.PropTypes.element, - count: React.PropTypes.number + count: React.PropTypes.number, + itemList: React.PropTypes.arrayOf(React.PropTypes.object), + search: React.PropTypes.string, + searchFor: React.PropTypes.func }, - + + clearSearch() { + this.props.searchFor(''); + }, + render() { + const { search } = this.props; + if(this.props.itemList && this.props.itemList.length > 0) { return (
{this.props.children}
); - } else if(this.props.count === 0) { + } else if(this.props.count === 0 && !search) { return (
-

{getLangText('We could not find any works related to you...')}

-

{getLangText('To register one, click')} {getLangText('here')}!

+

+ {getLangText('We could not find any works related to you...')} +

+

+ {getLangText('To register one, click')} + {getLangText('here')}! +

+
+ ); + } else if(this.props.count === 0 && search) { + return ( +
+

+ {getLangText('We could not find any works related to you...')} +

+

+ {getLangText('You\'re filtering by the search keyword: \'%s\' ', search)} +

+

+ +

); } else { diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js index 0890db00..b7db7ce6 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js @@ -4,16 +4,16 @@ import React from 'react'; import PieceListToolbarFilterWidget from './piece_list_toolbar_filter_widget'; import PieceListToolbarOrderWidget from './piece_list_toolbar_order_widget'; +import SearchBar from '../search_bar'; + +import AppConstants from '../../constants/application_constants'; -import Input from 'react-bootstrap/lib/Input'; -import Glyphicon from 'react-bootstrap/lib/Glyphicon'; -import { getLangText } from '../../utils/lang_utils'; let PieceListToolbar = React.createClass({ - propTypes: { className: React.PropTypes.string, searchFor: React.PropTypes.func, + searchQuery: React.PropTypes.string, filterParams: React.PropTypes.arrayOf( React.PropTypes.shape({ label: React.PropTypes.string, @@ -39,11 +39,6 @@ let PieceListToolbar = React.createClass({ ]) }, - searchFor() { - let searchTerm = this.refs.search.getInputDOMNode().value; - this.props.searchFor(searchTerm); - }, - getFilterWidget(){ if (this.props.filterParams){ return ( @@ -55,6 +50,7 @@ let PieceListToolbar = React.createClass({ } return null; }, + getOrderWidget(){ if (this.props.orderParams){ return ( @@ -68,24 +64,21 @@ let PieceListToolbar = React.createClass({ }, render() { - let searchIcon = ; + const { className, children, searchFor, searchQuery } = this.props; return ( -
+
- {this.props.children} - - - + {children} + {this.getOrderWidget()} {this.getFilterWidget()} diff --git a/js/components/piece_list.js b/js/components/piece_list.js index 35dcaba0..05557418 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -157,6 +157,7 @@ let PieceList = React.createClass({ diff --git a/js/components/search_bar.js b/js/components/search_bar.js new file mode 100644 index 00000000..e8382489 --- /dev/null +++ b/js/components/search_bar.js @@ -0,0 +1,140 @@ +'use strict'; + +import React from 'react'; + +import Input from 'react-bootstrap/lib/Input'; +import Glyphicon from 'react-bootstrap/lib/Glyphicon'; +import { getLangText } from '../utils/lang_utils'; + + +const { func, string, number } = React.PropTypes; + +const SearchBar = React.createClass({ + propTypes: { + // a function that accepts a string as a search query and updates the + // propagated `searchQuery` after successfully retrieving the + // request from the server + searchFor: func.isRequired, + searchQuery: string.isRequired, + + className: string, + + // the number of milliseconds the component + // should wait before requesting search results from the server + threshold: number.isRequired + }, + + getInitialState() { + return { + timer: null, + searchQuery: '', + loading: false + }; + }, + + componentDidUpdate(prevProps) { + const searchQueryProps = this.props.searchQuery; + const searchQueryPrevProps = prevProps.searchQuery; + const searchQueryState = this.state.searchQuery; + const { loading } = this.state; + + /** + * 1. Condition: `loading` must be true, which implies that `evaluateTimer`, + * has already been called + * + * AND + * + * ( + * 2. Condition: `searchQueryProps` and `searchQueryState` are true and equal + * (which means that the search query has been propagated to the inner + * fetch method of `fetchPieceList`, which in turn means that a fetch + * has completed) + * + * OR + * + * 3. Condition: `searchQueryProps` and `searchQueryState` can be any value (`true` or + * `false`, as long as they're equal). This case only occurs when the user + * has entered a `searchQuery` and deletes the query in one go, reseting + * `searchQueryProps` to empty string ('' === false) again. + * ) + */ + + const firstCondition = !!loading; + const secondCondition = searchQueryProps && searchQueryState && searchQueryProps === searchQueryState; + const thirdCondition = !searchQueryPrevProps && searchQueryProps === searchQueryState; + + if(firstCondition && (secondCondition || thirdCondition)) { + this.setState({ loading: false }); + } + }, + + componentWillReceiveProps(nextProps) { + /** + * This enables the `PieceListStore` to override the state + * of that component in case someone is changing the `searchQuery` on + * another component. + * + * Like how it's being done in the 'Clear search' dialog. + */ + if(this.props.searchQuery !== nextProps.searchQuery || !this.state.searchQuery) { + this.setState({ searchQuery: nextProps.searchQuery }); + } + }, + + startTimer(searchQuery) { + const { timer } = this.state; + const { threshold } = this.props; + + // The timer waits for the specified threshold time in milliseconds + // and then calls `evaluateTimer`. + // If another letter has been called in the mean time (timespan < `threshold`), + // the present timer gets cleared and a new one is added to `this.state`. + // This means that `evaluateTimer`, will only be called when the threshold has actually + // passed, + clearTimeout(timer); // apparently `clearTimeout` can be called with null, without throwing errors + const newTimer = setTimeout(this.evaluateTimer(searchQuery), threshold); + + this.setState({ timer: newTimer }); + }, + + evaluateTimer(searchQuery) { + return () => { + this.setState({ timer: null, loading: true }, () => { + // search for the query + this.props.searchFor(searchQuery); + }); + }; + }, + + handleChange({ target: { value }}) { + // On each letter entry we're updating the state of the component + // and start a timer, which we're also pushing to the state + // of the component + this.startTimer(value); + this.setState({ searchQuery: value }); + }, + + render() { + let searchIcon = ; + const { className } = this.props; + const { loading, searchQuery } = this.state; + + if(loading) { + searchIcon = ; + } + + return ( + + + + ); + } +}); + + +export default SearchBar; \ No newline at end of file diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index ce893791..0fe5e210 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -78,7 +78,8 @@ let constants = { 'copyrightAssociations': ['ARS', 'DACS', 'Bildkunst', 'Pictoright', 'SODRAC', 'Copyright Agency/Viscopy', 'SAVA', 'Bildrecht GmbH', 'SABAM', 'AUTVIS', 'CREAIMAGEN', 'SONECA', 'Copydan', 'EAU', 'Kuvasto', 'GCA', 'HUNGART', 'IVARO', 'SIAE', 'JASPAR-SPDA', 'AKKA/LAA', 'LATGA-A', 'SOMAAP', 'ARTEGESTION', 'CARIER', 'BONO', 'APSAV', - 'SPA', 'GESTOR', 'VISaRTA', 'RAO', 'LITA', 'DALRO', 'VeGaP', 'BUS', 'ProLitteris', 'AGADU', 'AUTORARTE', 'BUBEDRA', 'BBDA', 'BCDA', 'BURIDA', 'ADAVIS', 'BSDA'] + 'SPA', 'GESTOR', 'VISaRTA', 'RAO', 'LITA', 'DALRO', 'VeGaP', 'BUS', 'ProLitteris', 'AGADU', 'AUTORARTE', 'BUBEDRA', 'BBDA', 'BCDA', 'BURIDA', 'ADAVIS', 'BSDA'], + 'searchThreshold': 500 }; export default constants;