/frontend/src/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.js

https://github.com/NzbDrone/NzbDrone · JavaScript · 302 lines · 255 code · 39 blank · 8 comment · 14 complexity · 9ddb39437472ab6259d4bbef761496be MD5 · raw file

  1. import PropTypes from 'prop-types';
  2. import React, { Component } from 'react';
  3. import { Manager, Popper, Reference } from 'react-popper';
  4. import getUniqueElememtId from 'Utilities/getUniqueElementId';
  5. import { icons, kinds } from 'Helpers/Props';
  6. import Icon from 'Components/Icon';
  7. import Portal from 'Components/Portal';
  8. import FormInputButton from 'Components/Form/FormInputButton';
  9. import Link from 'Components/Link/Link';
  10. import LoadingIndicator from 'Components/Loading/LoadingIndicator';
  11. import TextInput from 'Components/Form/TextInput';
  12. import ImportSeriesSearchResultConnector from './ImportSeriesSearchResultConnector';
  13. import ImportSeriesTitle from './ImportSeriesTitle';
  14. import styles from './ImportSeriesSelectSeries.css';
  15. class ImportSeriesSelectSeries extends Component {
  16. //
  17. // Lifecycle
  18. constructor(props, context) {
  19. super(props, context);
  20. this._seriesLookupTimeout = null;
  21. this._scheduleUpdate = null;
  22. this._buttonId = getUniqueElememtId();
  23. this._contentId = getUniqueElememtId();
  24. this.state = {
  25. term: props.id,
  26. isOpen: false
  27. };
  28. }
  29. componentDidUpdate() {
  30. if (this._scheduleUpdate) {
  31. this._scheduleUpdate();
  32. }
  33. }
  34. //
  35. // Control
  36. _addListener() {
  37. window.addEventListener('click', this.onWindowClick);
  38. }
  39. _removeListener() {
  40. window.removeEventListener('click', this.onWindowClick);
  41. }
  42. //
  43. // Listeners
  44. onWindowClick = (event) => {
  45. const button = document.getElementById(this._buttonId);
  46. const content = document.getElementById(this._contentId);
  47. if (!button || !content) {
  48. return;
  49. }
  50. if (
  51. !button.contains(event.target) &&
  52. !content.contains(event.target) &&
  53. this.state.isOpen
  54. ) {
  55. this.setState({ isOpen: false });
  56. this._removeListener();
  57. }
  58. }
  59. onPress = () => {
  60. if (this.state.isOpen) {
  61. this._removeListener();
  62. } else {
  63. this._addListener();
  64. }
  65. this.setState({ isOpen: !this.state.isOpen });
  66. }
  67. onSearchInputChange = ({ value }) => {
  68. if (this._seriesLookupTimeout) {
  69. clearTimeout(this._seriesLookupTimeout);
  70. }
  71. this.setState({ term: value }, () => {
  72. this._seriesLookupTimeout = setTimeout(() => {
  73. this.props.onSearchInputChange(value);
  74. }, 200);
  75. });
  76. }
  77. onRefreshPress = () => {
  78. this.props.onSearchInputChange(this.state.term);
  79. }
  80. onSeriesSelect = (tvdbId) => {
  81. this.setState({ isOpen: false });
  82. this.props.onSeriesSelect(tvdbId);
  83. }
  84. //
  85. // Render
  86. render() {
  87. const {
  88. selectedSeries,
  89. isExistingSeries,
  90. isFetching,
  91. isPopulated,
  92. error,
  93. items,
  94. isQueued,
  95. isLookingUpSeries
  96. } = this.props;
  97. const errorMessage = error &&
  98. error.responseJSON &&
  99. error.responseJSON.message;
  100. return (
  101. <Manager>
  102. <Reference>
  103. {({ ref }) => (
  104. <div
  105. ref={ref}
  106. id={this._buttonId}
  107. >
  108. <Link
  109. ref={ref}
  110. className={styles.button}
  111. component="div"
  112. onPress={this.onPress}
  113. >
  114. {
  115. isLookingUpSeries && isQueued && !isPopulated ?
  116. <LoadingIndicator
  117. className={styles.loading}
  118. size={20}
  119. /> :
  120. null
  121. }
  122. {
  123. isPopulated && selectedSeries && isExistingSeries ?
  124. <Icon
  125. className={styles.warningIcon}
  126. name={icons.WARNING}
  127. kind={kinds.WARNING}
  128. /> :
  129. null
  130. }
  131. {
  132. isPopulated && selectedSeries ?
  133. <ImportSeriesTitle
  134. title={selectedSeries.title}
  135. year={selectedSeries.year}
  136. network={selectedSeries.network}
  137. isExistingSeries={isExistingSeries}
  138. /> :
  139. null
  140. }
  141. {
  142. isPopulated && !selectedSeries ?
  143. <div className={styles.noMatches}>
  144. <Icon
  145. className={styles.warningIcon}
  146. name={icons.WARNING}
  147. kind={kinds.WARNING}
  148. />
  149. No match found!
  150. </div> :
  151. null
  152. }
  153. {
  154. !isFetching && !!error ?
  155. <div>
  156. <Icon
  157. className={styles.warningIcon}
  158. title={errorMessage}
  159. name={icons.WARNING}
  160. kind={kinds.WARNING}
  161. />
  162. Search failed, please try again later.
  163. </div> :
  164. null
  165. }
  166. <div className={styles.dropdownArrowContainer}>
  167. <Icon
  168. name={icons.CARET_DOWN}
  169. />
  170. </div>
  171. </Link>
  172. </div>
  173. )}
  174. </Reference>
  175. <Portal>
  176. <Popper
  177. placement="bottom"
  178. modifiers={{
  179. preventOverflow: {
  180. boundariesElement: 'viewport'
  181. }
  182. }}
  183. >
  184. {({ ref, style, scheduleUpdate }) => {
  185. this._scheduleUpdate = scheduleUpdate;
  186. return (
  187. <div
  188. ref={ref}
  189. id={this._contentId}
  190. className={styles.contentContainer}
  191. style={style}
  192. >
  193. {
  194. this.state.isOpen ?
  195. <div className={styles.content}>
  196. <div className={styles.searchContainer}>
  197. <div className={styles.searchIconContainer}>
  198. <Icon name={icons.SEARCH} />
  199. </div>
  200. <TextInput
  201. className={styles.searchInput}
  202. name={`${name}_textInput`}
  203. value={this.state.term}
  204. onChange={this.onSearchInputChange}
  205. />
  206. <FormInputButton
  207. kind={kinds.DEFAULT}
  208. spinnerIcon={icons.REFRESH}
  209. canSpin={true}
  210. isSpinning={isFetching}
  211. onPress={this.onRefreshPress}
  212. >
  213. <Icon name={icons.REFRESH} />
  214. </FormInputButton>
  215. </div>
  216. <div className={styles.results}>
  217. {
  218. items.map((item) => {
  219. return (
  220. <ImportSeriesSearchResultConnector
  221. key={item.tvdbId}
  222. tvdbId={item.tvdbId}
  223. title={item.title}
  224. year={item.year}
  225. network={item.network}
  226. onPress={this.onSeriesSelect}
  227. />
  228. );
  229. })
  230. }
  231. </div>
  232. </div> :
  233. null
  234. }
  235. </div>
  236. );
  237. }}
  238. </Popper>
  239. </Portal>
  240. </Manager>
  241. );
  242. }
  243. }
  244. ImportSeriesSelectSeries.propTypes = {
  245. id: PropTypes.string.isRequired,
  246. selectedSeries: PropTypes.object,
  247. isExistingSeries: PropTypes.bool.isRequired,
  248. isFetching: PropTypes.bool.isRequired,
  249. isPopulated: PropTypes.bool.isRequired,
  250. error: PropTypes.object,
  251. items: PropTypes.arrayOf(PropTypes.object).isRequired,
  252. isQueued: PropTypes.bool.isRequired,
  253. isLookingUpSeries: PropTypes.bool.isRequired,
  254. onSearchInputChange: PropTypes.func.isRequired,
  255. onSeriesSelect: PropTypes.func.isRequired
  256. };
  257. ImportSeriesSelectSeries.defaultProps = {
  258. isFetching: true,
  259. isPopulated: false,
  260. items: [],
  261. isQueued: true
  262. };
  263. export default ImportSeriesSelectSeries;