teamSelect.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  4. import {Client} from 'app/api';
  5. import Button from 'app/components/button';
  6. import Confirm from 'app/components/confirm';
  7. import DropdownAutoComplete from 'app/components/dropdownAutoComplete';
  8. import {Item} from 'app/components/dropdownAutoComplete/types';
  9. import DropdownButton from 'app/components/dropdownButton';
  10. import Link from 'app/components/links/link';
  11. import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
  12. import {DEFAULT_DEBOUNCE_DURATION, TEAMS_PER_PAGE} from 'app/constants';
  13. import {IconSubtract} from 'app/icons';
  14. import {t} from 'app/locale';
  15. import space from 'app/styles/space';
  16. import {Organization, Team} from 'app/types';
  17. import withApi from 'app/utils/withApi';
  18. import EmptyMessage from 'app/views/settings/components/emptyMessage';
  19. type Props = {
  20. api: Client;
  21. organization: Organization;
  22. /**
  23. * Should button be disabled
  24. */
  25. disabled: boolean;
  26. /**
  27. * Teams that are already selected.
  28. */
  29. selectedTeams: string[];
  30. /**
  31. * callback when teams are added
  32. */
  33. onAddTeam: (team: Team) => void;
  34. /**
  35. * Callback when teams are removed
  36. */
  37. onRemoveTeam: (teamSlug: string) => void;
  38. /**
  39. * Optional menu header.
  40. */
  41. menuHeader?: React.ReactElement;
  42. /**
  43. * Message to display when the last team is removed
  44. * if empty no confirm will be displayed.
  45. */
  46. confirmLastTeamRemoveMessage?: string;
  47. };
  48. type State = {
  49. loading: boolean;
  50. teams: null | Team[];
  51. };
  52. class TeamSelect extends React.Component<Props, State> {
  53. state: State = {
  54. loading: true,
  55. teams: null,
  56. };
  57. componentDidMount() {
  58. this.fetchTeams();
  59. }
  60. fetchTeams = debounce(async (query?: string) => {
  61. const {api, organization} = this.props;
  62. const teams = await api.requestPromise(`/organizations/${organization.slug}/teams/`, {
  63. query: {query, per_page: TEAMS_PER_PAGE},
  64. });
  65. this.setState({teams, loading: false});
  66. }, DEFAULT_DEBOUNCE_DURATION);
  67. handleQueryUpdate = (event: React.ChangeEvent<HTMLInputElement>) => {
  68. this.setState({loading: true});
  69. this.fetchTeams(event.target.value);
  70. };
  71. handleAddTeam = (option: Item) => {
  72. const team = this.state.teams?.find(tm => tm.slug === option.value);
  73. if (team) {
  74. this.props.onAddTeam(team);
  75. }
  76. };
  77. handleRemove = (teamSlug: string) => {
  78. this.props.onRemoveTeam(teamSlug);
  79. };
  80. renderTeamAddDropDown() {
  81. const {disabled, selectedTeams, menuHeader} = this.props;
  82. const {teams} = this.state;
  83. const isDisabled = disabled;
  84. let options: Item[] = [];
  85. if (teams === null || teams.length === 0) {
  86. options = [];
  87. } else {
  88. options = teams
  89. .filter(team => !selectedTeams.includes(team.slug))
  90. .map((team, index) => ({
  91. index,
  92. value: team.slug,
  93. searchKey: team.slug,
  94. label: <TeamDropdownElement>#{team.slug}</TeamDropdownElement>,
  95. }));
  96. }
  97. return (
  98. <DropdownAutoComplete
  99. items={options}
  100. busyItemsStillVisible={this.state.loading}
  101. onChange={this.handleQueryUpdate}
  102. onSelect={this.handleAddTeam}
  103. emptyMessage={t('No teams')}
  104. menuHeader={menuHeader}
  105. disabled={isDisabled}
  106. alignMenu="right"
  107. >
  108. {({isOpen}) => (
  109. <DropdownButton
  110. aria-label={t('Add Team')}
  111. isOpen={isOpen}
  112. size="xsmall"
  113. disabled={isDisabled}
  114. >
  115. {t('Add Team')}
  116. </DropdownButton>
  117. )}
  118. </DropdownAutoComplete>
  119. );
  120. }
  121. renderBody() {
  122. const {
  123. organization,
  124. selectedTeams,
  125. disabled,
  126. confirmLastTeamRemoveMessage,
  127. } = this.props;
  128. if (selectedTeams.length === 0) {
  129. return <EmptyMessage>{t('No Teams assigned')}</EmptyMessage>;
  130. }
  131. const confirmMessage =
  132. selectedTeams.length === 1 && confirmLastTeamRemoveMessage
  133. ? confirmLastTeamRemoveMessage
  134. : null;
  135. return selectedTeams.map(team => (
  136. <TeamRow
  137. key={team}
  138. orgId={organization.slug}
  139. team={team}
  140. onRemove={this.handleRemove}
  141. disabled={disabled}
  142. confirmMessage={confirmMessage}
  143. />
  144. ));
  145. }
  146. render() {
  147. return (
  148. <Panel>
  149. <PanelHeader hasButtons>
  150. {t('Team')}
  151. {this.renderTeamAddDropDown()}
  152. </PanelHeader>
  153. <PanelBody>{this.renderBody()}</PanelBody>
  154. </Panel>
  155. );
  156. }
  157. }
  158. const TeamRow = props => {
  159. const {orgId, team, onRemove, disabled, confirmMessage} = props;
  160. return (
  161. <TeamPanelItem>
  162. <StyledLink to={`/settings/${orgId}/teams/${team}/`}>{`#${team}`}</StyledLink>
  163. <Confirm
  164. message={confirmMessage}
  165. bypass={!confirmMessage}
  166. onConfirm={() => onRemove(team)}
  167. disabled={disabled}
  168. >
  169. <Button
  170. size="xsmall"
  171. icon={<IconSubtract isCircled size="xs" />}
  172. disabled={disabled}
  173. >
  174. {t('Remove')}
  175. </Button>
  176. </Confirm>
  177. </TeamPanelItem>
  178. );
  179. };
  180. const TeamDropdownElement = styled('div')`
  181. padding: ${space(0.5)} 0px;
  182. text-transform: none;
  183. `;
  184. const TeamPanelItem = styled(PanelItem)`
  185. padding: ${space(2)};
  186. align-items: center;
  187. `;
  188. const StyledLink = styled(Link)`
  189. flex: 1;
  190. margin-right: ${space(1)};
  191. `;
  192. export default withApi(TeamSelect);