teamSelect.tsx 4.8 KB

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