You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UsersSelectSearch.tsx 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import * as React from 'react';
  21. import { debounce } from 'lodash';
  22. import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
  23. import Select from 'sonar-ui-common/components/controls/Select';
  24. import Avatar from '../../../components/ui/Avatar';
  25. interface Option {
  26. login: string;
  27. name: string;
  28. avatar?: string;
  29. }
  30. interface Props {
  31. autoFocus?: boolean;
  32. excludedUsers: string[];
  33. handleValueChange: (option: Option) => void;
  34. searchUsers: (query: string, ps: number) => Promise<{ users: Option[] }>;
  35. selectedUser?: Option;
  36. }
  37. interface State {
  38. isLoading: boolean;
  39. search: string;
  40. searchResult: Option[];
  41. }
  42. const LIST_SIZE = 10;
  43. const AVATAR_SIZE = 16;
  44. export default class UsersSelectSearch extends React.PureComponent<Props, State> {
  45. mounted = false;
  46. constructor(props: Props) {
  47. super(props);
  48. this.handleSearch = debounce(this.handleSearch, 250);
  49. this.state = { searchResult: [], isLoading: false, search: '' };
  50. }
  51. componentDidMount() {
  52. this.mounted = true;
  53. this.handleSearch(this.state.search);
  54. }
  55. componentDidUpdate(prevProps: Props) {
  56. if (this.props.excludedUsers !== prevProps.excludedUsers) {
  57. this.handleSearch(this.state.search);
  58. }
  59. }
  60. componentWillUnmount() {
  61. this.mounted = false;
  62. }
  63. filterSearchResult = ({ users }: { users: Option[] }) =>
  64. users.filter(user => !this.props.excludedUsers.includes(user.login)).slice(0, LIST_SIZE);
  65. filterOptions = (options: Option[]) => {
  66. return options; // We don't filter anything, this is done by the WS
  67. };
  68. handleSearch = (search: string) => {
  69. this.props
  70. .searchUsers(search, Math.min(this.props.excludedUsers.length + LIST_SIZE, 500))
  71. .then(this.filterSearchResult)
  72. .then(
  73. searchResult => {
  74. if (this.mounted) {
  75. this.setState({ isLoading: false, searchResult });
  76. }
  77. },
  78. () => {
  79. if (this.mounted) {
  80. this.setState({ isLoading: false, searchResult: [] });
  81. }
  82. }
  83. );
  84. };
  85. handleInputChange = (search: string) => {
  86. if (search == null || search.length === 1) {
  87. this.setState({ search });
  88. } else {
  89. this.setState({ isLoading: true, search });
  90. this.handleSearch(search);
  91. }
  92. };
  93. render() {
  94. const noResult =
  95. this.state.search.length === 1
  96. ? translateWithParameters('select2.tooShort', 2)
  97. : translate('no_results');
  98. return (
  99. <Select
  100. autoFocus={this.props.autoFocus}
  101. className="Select-big"
  102. clearable={false}
  103. filterOptions={this.filterOptions}
  104. isLoading={this.state.isLoading}
  105. labelKey="name"
  106. noResultsText={noResult}
  107. onChange={this.props.handleValueChange}
  108. onInputChange={this.handleInputChange}
  109. optionComponent={UsersSelectSearchOption}
  110. options={this.state.searchResult}
  111. placeholder=""
  112. searchable={true}
  113. value={this.props.selectedUser}
  114. valueComponent={UsersSelectSearchValue}
  115. valueKey="login"
  116. />
  117. );
  118. }
  119. }
  120. interface OptionProps {
  121. children?: React.ReactNode;
  122. className?: string;
  123. isFocused?: boolean;
  124. onFocus: (option: Option, evt: React.MouseEvent<HTMLDivElement>) => void;
  125. onSelect: (option: Option, evt: React.MouseEvent<HTMLDivElement>) => void;
  126. option: Option;
  127. }
  128. export class UsersSelectSearchOption extends React.PureComponent<OptionProps> {
  129. handleMouseDown = (evt: React.MouseEvent<HTMLDivElement>) => {
  130. evt.preventDefault();
  131. evt.stopPropagation();
  132. this.props.onSelect(this.props.option, evt);
  133. };
  134. handleMouseEnter = (evt: React.MouseEvent<HTMLDivElement>) => {
  135. this.props.onFocus(this.props.option, evt);
  136. };
  137. handleMouseMove = (evt: React.MouseEvent<HTMLDivElement>) => {
  138. if (this.props.isFocused) {
  139. return;
  140. }
  141. this.props.onFocus(this.props.option, evt);
  142. };
  143. render() {
  144. const { option } = this.props;
  145. return (
  146. <div
  147. className={this.props.className}
  148. onMouseDown={this.handleMouseDown}
  149. onMouseEnter={this.handleMouseEnter}
  150. onMouseMove={this.handleMouseMove}
  151. role="listitem"
  152. title={option.name}>
  153. <Avatar hash={option.avatar} name={option.name} size={AVATAR_SIZE} />
  154. <strong className="spacer-left">{this.props.children}</strong>
  155. <span className="note little-spacer-left">{option.login}</span>
  156. </div>
  157. );
  158. }
  159. }
  160. interface ValueProps {
  161. value?: Option;
  162. children?: React.ReactNode;
  163. }
  164. export function UsersSelectSearchValue({ children, value }: ValueProps) {
  165. return (
  166. <div className="Select-value" title={value ? value.name : ''}>
  167. {value && value.login && (
  168. <div className="Select-value-label">
  169. <Avatar hash={value.avatar} name={value.name} size={AVATAR_SIZE} />
  170. <strong className="spacer-left">{children}</strong>
  171. <span className="note little-spacer-left">{value.login}</span>
  172. </div>
  173. )}
  174. </div>
  175. );
  176. }