inviteRowControl.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import {useCallback, useState} from 'react';
  2. import type {MultiValueProps} from 'react-select';
  3. import type {Theme} from '@emotion/react';
  4. import {useTheme} from '@emotion/react';
  5. import {Button} from 'sentry/components/button';
  6. import type {StylesConfig} from 'sentry/components/forms/controls/selectControl';
  7. import SelectControl from 'sentry/components/forms/controls/selectControl';
  8. import RoleSelectControl from 'sentry/components/roleSelectControl';
  9. import TeamSelector from 'sentry/components/teamSelector';
  10. import {IconClose} from 'sentry/icons/iconClose';
  11. import {t} from 'sentry/locale';
  12. import type {OrgRole, SelectValue} from 'sentry/types';
  13. import renderEmailValue from './renderEmailValue';
  14. import type {InviteStatus} from './types';
  15. type SelectOption = SelectValue<string>;
  16. type Props = {
  17. disableRemove: boolean;
  18. disabled: boolean;
  19. emails: string[];
  20. inviteStatus: InviteStatus;
  21. onChangeEmails: (emails: SelectOption[]) => void;
  22. onChangeRole: (role: SelectOption) => void;
  23. onChangeTeams: (teams: SelectOption[]) => void;
  24. onRemove: () => void;
  25. role: string;
  26. roleDisabledUnallowed: boolean;
  27. roleOptions: OrgRole[];
  28. teams: string[];
  29. className?: string;
  30. };
  31. function ValueComponent(
  32. props: MultiValueProps<SelectOption>,
  33. inviteStatus: Props['inviteStatus']
  34. ) {
  35. return renderEmailValue(inviteStatus[props.data.value], props);
  36. }
  37. function mapToOptions(values: string[]): SelectOption[] {
  38. return values.map(value => ({value, label: value}));
  39. }
  40. function InviteRowControl({
  41. className,
  42. disabled,
  43. emails,
  44. role,
  45. teams,
  46. roleOptions,
  47. roleDisabledUnallowed,
  48. inviteStatus,
  49. onRemove,
  50. onChangeEmails,
  51. onChangeRole,
  52. onChangeTeams,
  53. disableRemove,
  54. }: Props) {
  55. const [inputValue, setInputValue] = useState('');
  56. const theme = useTheme();
  57. const isTeamRolesAllowedForRole = useCallback<(roleId: string) => boolean>(
  58. roleId => {
  59. const roleOptionsMap = roleOptions.reduce(
  60. (rolesMap, roleOption) => ({...rolesMap, [roleOption.id]: roleOption}),
  61. {}
  62. );
  63. return roleOptionsMap[roleId]?.isTeamRolesAllowed ?? true;
  64. },
  65. [roleOptions]
  66. );
  67. const isTeamRolesAllowed = isTeamRolesAllowedForRole(role);
  68. const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
  69. switch (event.key) {
  70. case 'Enter':
  71. case ',':
  72. case ' ':
  73. onChangeEmails([...mapToOptions(emails), {label: inputValue, value: inputValue}]);
  74. setInputValue('');
  75. event.preventDefault();
  76. break;
  77. default:
  78. // do nothing.
  79. }
  80. };
  81. return (
  82. <li className={className}>
  83. <SelectControl
  84. aria-label={t('Email Addresses')}
  85. data-test-id="select-emails"
  86. disabled={disabled}
  87. placeholder={t('Enter one or more emails')}
  88. inputValue={inputValue}
  89. value={emails}
  90. components={{
  91. MultiValue: props => ValueComponent(props, inviteStatus),
  92. DropdownIndicator: () => null,
  93. }}
  94. options={mapToOptions(emails)}
  95. onBlur={(e: React.ChangeEvent<HTMLInputElement>) =>
  96. e.target.value &&
  97. onChangeEmails([
  98. ...mapToOptions(emails),
  99. {label: e.target.value, value: e.target.value},
  100. ])
  101. }
  102. styles={getStyles(theme, inviteStatus)}
  103. onInputChange={setInputValue}
  104. onKeyDown={handleKeyDown}
  105. onChange={onChangeEmails}
  106. multiple
  107. creatable
  108. clearable
  109. menuIsOpen={false}
  110. />
  111. <RoleSelectControl
  112. aria-label={t('Role')}
  113. data-test-id="select-role"
  114. disabled={disabled}
  115. value={role}
  116. roles={roleOptions}
  117. disableUnallowed={roleDisabledUnallowed}
  118. onChange={roleOption => {
  119. onChangeRole(roleOption);
  120. if (!isTeamRolesAllowedForRole(roleOption.value)) {
  121. onChangeTeams([]);
  122. }
  123. }}
  124. />
  125. <TeamSelector
  126. aria-label={t('Add to Team')}
  127. data-test-id="select-teams"
  128. disabled={isTeamRolesAllowed ? disabled : true}
  129. placeholder={isTeamRolesAllowed ? t('None') : t('Role cannot join teams')}
  130. value={isTeamRolesAllowed ? teams : []}
  131. onChange={onChangeTeams}
  132. useTeamDefaultIfOnlyOne
  133. multiple
  134. clearable
  135. />
  136. <Button
  137. borderless
  138. icon={<IconClose />}
  139. onClick={onRemove}
  140. disabled={disableRemove}
  141. aria-label={t('Remove')}
  142. />
  143. </li>
  144. );
  145. }
  146. /**
  147. * The email select control has custom selected item states as items
  148. * show their delivery status after the form is submitted.
  149. */
  150. function getStyles(theme: Theme, inviteStatus: Props['inviteStatus']): StylesConfig {
  151. return {
  152. multiValue: (provided, {data}: MultiValueProps<SelectOption>) => {
  153. const status = inviteStatus[data.value];
  154. return {
  155. ...provided,
  156. ...(status?.error
  157. ? {
  158. color: theme.red400,
  159. border: `1px solid ${theme.red300}`,
  160. backgroundColor: theme.red100,
  161. }
  162. : {}),
  163. };
  164. },
  165. multiValueLabel: (provided, {data}: MultiValueProps<SelectOption>) => {
  166. const status = inviteStatus[data.value];
  167. return {
  168. ...provided,
  169. pointerEvents: 'all',
  170. ...(status?.error ? {color: theme.red400} : {}),
  171. };
  172. },
  173. multiValueRemove: (provided, {data}: MultiValueProps<SelectOption>) => {
  174. const status = inviteStatus[data.value];
  175. return {
  176. ...provided,
  177. ...(status?.error
  178. ? {
  179. borderLeft: `1px solid ${theme.red300}`,
  180. ':hover': {backgroundColor: theme.red100, color: theme.red400},
  181. }
  182. : {}),
  183. };
  184. },
  185. };
  186. }
  187. export default InviteRowControl;