teamSelect.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  4. import Button from 'app/components/button';
  5. import Confirm from 'app/components/confirm';
  6. import DropdownAutoComplete from 'app/components/dropdownAutoComplete';
  7. import {Item} from 'app/components/dropdownAutoComplete/types';
  8. import DropdownButton from 'app/components/dropdownButton';
  9. import TeamBadge from 'app/components/idBadge/teamBadge';
  10. import Link from 'app/components/links/link';
  11. import LoadingIndicator from 'app/components/loadingIndicator';
  12. import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
  13. import {DEFAULT_DEBOUNCE_DURATION} from 'app/constants';
  14. import {IconSubtract} from 'app/icons';
  15. import {t} from 'app/locale';
  16. import space from 'app/styles/space';
  17. import {Organization, Team} from 'app/types';
  18. import useTeams from 'app/utils/useTeams';
  19. import EmptyMessage from 'app/views/settings/components/emptyMessage';
  20. type Props = {
  21. organization: Organization;
  22. /**
  23. * Should button be disabled
  24. */
  25. disabled: boolean;
  26. /**
  27. * Teams that are already selected.
  28. */
  29. selectedTeams: Team[];
  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. * Used to determine whether we should show a loading state while waiting for teams
  49. */
  50. loadingTeams?: boolean;
  51. };
  52. function TeamSelect({
  53. disabled,
  54. selectedTeams,
  55. menuHeader,
  56. organization,
  57. onAddTeam,
  58. onRemoveTeam,
  59. confirmLastTeamRemoveMessage,
  60. loadingTeams,
  61. }: Props) {
  62. const {teams, onSearch, fetching} = useTeams();
  63. const handleAddTeam = (option: Item) => {
  64. const team = teams.find(tm => tm.slug === option.value);
  65. if (team) {
  66. onAddTeam(team);
  67. }
  68. };
  69. const renderBody = () => {
  70. if (selectedTeams.length === 0) {
  71. return <EmptyMessage>{t('No Teams assigned')}</EmptyMessage>;
  72. }
  73. const confirmMessage =
  74. selectedTeams.length === 1 && confirmLastTeamRemoveMessage
  75. ? confirmLastTeamRemoveMessage
  76. : null;
  77. return selectedTeams.map(team => (
  78. <TeamRow
  79. key={team.slug}
  80. orgId={organization.slug}
  81. team={team}
  82. onRemove={slug => onRemoveTeam(slug)}
  83. disabled={disabled}
  84. confirmMessage={confirmMessage}
  85. />
  86. ));
  87. };
  88. // Only show options that aren't selected in the dropdown
  89. const options = teams
  90. .filter(team => !selectedTeams.some(selectedTeam => selectedTeam.slug === team.slug))
  91. .map((team, index) => ({
  92. index,
  93. value: team.slug,
  94. searchKey: team.slug,
  95. label: <DropdownTeamBadge avatarSize={18} team={team} />,
  96. }));
  97. return (
  98. <Panel>
  99. <PanelHeader hasButtons>
  100. {t('Team')}
  101. <DropdownAutoComplete
  102. items={options}
  103. busyItemsStillVisible={fetching}
  104. onChange={debounce<(e: React.ChangeEvent<HTMLInputElement>) => void>(
  105. e => onSearch(e.target.value),
  106. DEFAULT_DEBOUNCE_DURATION
  107. )}
  108. onSelect={handleAddTeam}
  109. emptyMessage={t('No teams')}
  110. menuHeader={menuHeader}
  111. disabled={disabled}
  112. alignMenu="right"
  113. >
  114. {({isOpen}) => (
  115. <DropdownButton
  116. aria-label={t('Add Team')}
  117. isOpen={isOpen}
  118. size="xsmall"
  119. disabled={disabled}
  120. >
  121. {t('Add Team')}
  122. </DropdownButton>
  123. )}
  124. </DropdownAutoComplete>
  125. </PanelHeader>
  126. <PanelBody>{loadingTeams ? <LoadingIndicator /> : renderBody()}</PanelBody>
  127. </Panel>
  128. );
  129. }
  130. type TeamRowProps = {
  131. orgId: string;
  132. team: Team;
  133. onRemove: Props['onRemoveTeam'];
  134. disabled: boolean;
  135. confirmMessage: string | null;
  136. };
  137. const TeamRow = ({orgId, team, onRemove, disabled, confirmMessage}: TeamRowProps) => (
  138. <TeamPanelItem>
  139. <StyledLink to={`/settings/${orgId}/teams/${team.slug}/`}>
  140. <TeamBadge team={team} />
  141. </StyledLink>
  142. <Confirm
  143. message={confirmMessage}
  144. bypass={!confirmMessage}
  145. onConfirm={() => onRemove(team.slug)}
  146. disabled={disabled}
  147. >
  148. <Button
  149. size="xsmall"
  150. icon={<IconSubtract isCircled size="xs" />}
  151. disabled={disabled}
  152. >
  153. {t('Remove')}
  154. </Button>
  155. </Confirm>
  156. </TeamPanelItem>
  157. );
  158. const DropdownTeamBadge = styled(TeamBadge)`
  159. font-weight: normal;
  160. font-size: ${p => p.theme.fontSizeMedium};
  161. text-transform: none;
  162. `;
  163. const TeamPanelItem = styled(PanelItem)`
  164. padding: ${space(2)};
  165. align-items: center;
  166. `;
  167. const StyledLink = styled(Link)`
  168. flex: 1;
  169. margin-right: ${space(1)};
  170. `;
  171. export default TeamSelect;