inviteRowControl.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import {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 handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
  58. switch (event.key) {
  59. case 'Enter':
  60. case ',':
  61. case ' ':
  62. onChangeEmails([...mapToOptions(emails), {label: inputValue, value: inputValue}]);
  63. setInputValue('');
  64. event.preventDefault();
  65. break;
  66. default:
  67. // do nothing.
  68. }
  69. };
  70. return (
  71. <li className={className}>
  72. <SelectControl
  73. aria-label={t('Email Addresses')}
  74. data-test-id="select-emails"
  75. disabled={disabled}
  76. placeholder={t('Enter one or more emails')}
  77. inputValue={inputValue}
  78. value={emails}
  79. components={{
  80. MultiValue: props => ValueComponent(props, inviteStatus),
  81. DropdownIndicator: () => null,
  82. }}
  83. options={mapToOptions(emails)}
  84. onBlur={(e: React.ChangeEvent<HTMLInputElement>) =>
  85. e.target.value &&
  86. onChangeEmails([
  87. ...mapToOptions(emails),
  88. {label: e.target.value, value: e.target.value},
  89. ])
  90. }
  91. styles={getStyles(theme, inviteStatus)}
  92. onInputChange={setInputValue}
  93. onKeyDown={handleKeyDown}
  94. onChange={onChangeEmails}
  95. multiple
  96. creatable
  97. clearable
  98. menuIsOpen={false}
  99. />
  100. <RoleSelectControl
  101. aria-label={t('Role')}
  102. data-test-id="select-role"
  103. disabled={disabled}
  104. value={role}
  105. roles={roleOptions}
  106. disableUnallowed={roleDisabledUnallowed}
  107. onChange={onChangeRole}
  108. />
  109. <TeamSelector
  110. aria-label={t('Add to Team')}
  111. data-test-id="select-teams"
  112. disabled={disabled}
  113. placeholder={t('None')}
  114. value={teams}
  115. onChange={onChangeTeams}
  116. useTeamDefaultIfOnlyOne
  117. multiple
  118. clearable
  119. />
  120. <Button
  121. borderless
  122. icon={<IconClose />}
  123. onClick={onRemove}
  124. disabled={disableRemove}
  125. aria-label={t('Remove')}
  126. />
  127. </li>
  128. );
  129. }
  130. /**
  131. * The email select control has custom selected item states as items
  132. * show their delivery status after the form is submitted.
  133. */
  134. function getStyles(theme: Theme, inviteStatus: Props['inviteStatus']): StylesConfig {
  135. return {
  136. multiValue: (provided, {data}: MultiValueProps<SelectOption>) => {
  137. const status = inviteStatus[data.value];
  138. return {
  139. ...provided,
  140. ...(status?.error
  141. ? {
  142. color: theme.red400,
  143. border: `1px solid ${theme.red300}`,
  144. backgroundColor: theme.red100,
  145. }
  146. : {}),
  147. };
  148. },
  149. multiValueLabel: (provided, {data}: MultiValueProps<SelectOption>) => {
  150. const status = inviteStatus[data.value];
  151. return {
  152. ...provided,
  153. pointerEvents: 'all',
  154. ...(status?.error ? {color: theme.red400} : {}),
  155. };
  156. },
  157. multiValueRemove: (provided, {data}: MultiValueProps<SelectOption>) => {
  158. const status = inviteStatus[data.value];
  159. return {
  160. ...provided,
  161. ...(status?.error
  162. ? {
  163. borderLeft: `1px solid ${theme.red300}`,
  164. ':hover': {backgroundColor: theme.red100, color: theme.red400},
  165. }
  166. : {}),
  167. };
  168. },
  169. };
  170. }
  171. export default InviteRowControl;