asyncComponentSearchInput.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import {Component} from 'react';
  2. // eslint-disable-next-line no-restricted-imports
  3. import {withRouter, WithRouterProps} from 'react-router';
  4. import styled from '@emotion/styled';
  5. import debounce from 'lodash/debounce';
  6. import {Client, ResponseMeta} from 'sentry/api';
  7. import Input from 'sentry/components/input';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {t} from 'sentry/locale';
  10. type RenderProps = {
  11. busy: boolean;
  12. defaultSearchBar: React.ReactNode;
  13. handleChange: (value: string) => void;
  14. value: string;
  15. };
  16. type DefaultProps = {
  17. /**
  18. * Placeholder text in the search input
  19. */
  20. placeholder: string;
  21. /**
  22. * Time in milliseconds to wait before firing off the request
  23. */
  24. debounceWait?: number; // optional, otherwise app/views/settings/organizationMembers/organizationMembersList.tsx L:191 is not happy
  25. };
  26. type Props = WithRouterProps &
  27. DefaultProps & {
  28. api: Client;
  29. onError: () => void;
  30. onSuccess: (data: object, resp: ResponseMeta | undefined) => void;
  31. /**
  32. * URL to make the search request to
  33. */
  34. url: string;
  35. /**
  36. * A render-prop child may be passed to handle custom rendering of the input.
  37. */
  38. children?: (otps: RenderProps) => React.ReactNode;
  39. className?: string;
  40. onSearchSubmit?: (query: string, event: React.FormEvent) => void;
  41. /**
  42. * Updates URL with search query in the URL param: `query`
  43. */
  44. updateRoute?: boolean;
  45. };
  46. type State = {
  47. busy: boolean;
  48. query: string;
  49. };
  50. /**
  51. * This is a search input that can be easily used in AsyncComponent/Views.
  52. *
  53. * It probably doesn't make too much sense outside of an AsyncComponent atm.
  54. */
  55. class AsyncComponentSearchInput extends Component<Props, State> {
  56. static defaultProps: DefaultProps = {
  57. placeholder: t('Search...'),
  58. debounceWait: 200,
  59. };
  60. state: State = {
  61. query: '',
  62. busy: false,
  63. };
  64. immediateQuery = async (searchQuery: string) => {
  65. const {location, api} = this.props;
  66. this.setState({busy: true});
  67. try {
  68. const [data, , resp] = await api.requestPromise(`${this.props.url}`, {
  69. includeAllArgs: true,
  70. method: 'GET',
  71. query: {...location.query, query: searchQuery},
  72. });
  73. // only update data if the request's query matches the current query
  74. if (this.state.query === searchQuery) {
  75. this.props.onSuccess(data, resp);
  76. }
  77. } catch {
  78. this.props.onError();
  79. }
  80. this.setState({busy: false});
  81. };
  82. query = debounce(this.immediateQuery, this.props.debounceWait);
  83. handleChange = (query: string) => {
  84. this.query(query);
  85. this.setState({query});
  86. };
  87. handleInputChange = (evt: React.ChangeEvent<HTMLInputElement>) =>
  88. this.handleChange(evt.target.value);
  89. /**
  90. * This is called when "Enter" (more specifically a form "submit" event) is pressed.
  91. */
  92. handleSearch = (evt: React.FormEvent<HTMLFormElement>) => {
  93. const {updateRoute, onSearchSubmit} = this.props;
  94. evt.preventDefault();
  95. // Update the URL to reflect search term.
  96. if (updateRoute) {
  97. const {router, location} = this.props;
  98. router.push({
  99. pathname: location.pathname,
  100. query: {
  101. query: this.state.query,
  102. },
  103. });
  104. }
  105. if (typeof onSearchSubmit !== 'function') {
  106. return;
  107. }
  108. onSearchSubmit(this.state.query, evt);
  109. };
  110. render() {
  111. const {placeholder, children, className} = this.props;
  112. const {busy, query} = this.state;
  113. const defaultSearchBar = (
  114. <Form onSubmit={this.handleSearch}>
  115. <Input
  116. value={query}
  117. onChange={this.handleInputChange}
  118. className={className}
  119. placeholder={placeholder}
  120. />
  121. {busy && <StyledLoadingIndicator size={18} hideMessage mini />}
  122. </Form>
  123. );
  124. return children === undefined
  125. ? defaultSearchBar
  126. : children({defaultSearchBar, busy, value: query, handleChange: this.handleChange});
  127. }
  128. }
  129. const StyledLoadingIndicator = styled(LoadingIndicator)`
  130. position: absolute;
  131. right: 25px;
  132. top: 50%;
  133. transform: translateY(-13px);
  134. `;
  135. const Form = styled('form')`
  136. position: relative;
  137. `;
  138. export default withRouter(AsyncComponentSearchInput);