selectAsyncControl.tsx 3.1 KB

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