selectAsyncControl.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import * as React from 'react';
  2. import debounce from 'lodash/debounce';
  3. import {addErrorMessage} from 'app/actionCreators/indicator';
  4. import {Client} from 'app/api';
  5. import {t} from 'app/locale';
  6. import handleXhrErrorResponse from 'app/utils/handleXhrErrorResponse';
  7. import SelectControl, {ControlProps} from './selectControl';
  8. type Result = {
  9. value: string;
  10. label: string;
  11. };
  12. type Props = {
  13. url: string;
  14. onResults: (data: any) => Result[]; // TODO(ts): Improve data type
  15. onQuery: (query: string | undefined) => {};
  16. } & Pick<ControlProps, 'value' | 'forwardedRef'>;
  17. type State = {
  18. query?: string;
  19. };
  20. /**
  21. * Performs an API request to `url` when menu is initially opened
  22. */
  23. class SelectAsyncControl extends React.Component<Props> {
  24. static defaultProps = {
  25. placeholder: '--',
  26. };
  27. constructor(props) {
  28. super(props);
  29. this.api = new Client();
  30. this.state = {
  31. query: '',
  32. };
  33. this.cache = {};
  34. }
  35. state: State = {};
  36. componentWillUnmount() {
  37. if (!this.api) {
  38. return;
  39. }
  40. this.api.clear();
  41. this.api = null;
  42. }
  43. api: Client | null;
  44. cache: Record<string, any>;
  45. doQuery = debounce(cb => {
  46. const {url, onQuery} = this.props;
  47. const {query} = this.state;
  48. if (!this.api) {
  49. return null;
  50. }
  51. return this.api
  52. .requestPromise(url, {
  53. query: typeof onQuery === 'function' ? onQuery(query) : {query},
  54. })
  55. .then(
  56. data => cb(null, data),
  57. err => cb(err)
  58. );
  59. }, 250);
  60. handleLoadOptions = () =>
  61. new Promise((resolve, reject) => {
  62. this.doQuery((err, result) => {
  63. if (err) {
  64. reject(err);
  65. } else {
  66. resolve(result);
  67. }
  68. });
  69. }).then(
  70. resp => {
  71. const {onResults} = this.props;
  72. return typeof onResults === 'function' ? onResults(resp) : resp;
  73. },
  74. err => {
  75. addErrorMessage(t('There was a problem with the request.'));
  76. handleXhrErrorResponse('SelectAsync failed')(err);
  77. // eslint-disable-next-line no-console
  78. console.error(err);
  79. }
  80. );
  81. handleInputChange = query => {
  82. this.setState({query});
  83. };
  84. render() {
  85. const {value, forwardedRef, ...props} = this.props;
  86. return (
  87. <SelectControl
  88. // The key is used as a way to force a reload of the options:
  89. // https://github.com/JedWatson/react-select/issues/1879#issuecomment-316871520
  90. key={value}
  91. ref={forwardedRef}
  92. value={value}
  93. defaultOptions
  94. loadOptions={this.handleLoadOptions}
  95. onInputChange={this.handleInputChange}
  96. async
  97. cache={this.cache}
  98. {...props}
  99. />
  100. );
  101. }
  102. }
  103. const forwardRef = (p, ref) => <SelectAsyncControl {...p} forwardedRef={ref} />;
  104. forwardRef.displayName = 'SelectAsyncControl';
  105. export default React.forwardRef(forwardRef);