asyncComponentSearchInput.tsx 4.1 KB

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