accountEmails.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import type {RequestOptions} from 'sentry/api';
  5. import {Button} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import {AlertLink} from 'sentry/components/core/alert/alertLink';
  8. import {Tag} from 'sentry/components/core/badge/tag';
  9. import type {FormProps} from 'sentry/components/forms/form';
  10. import Form from 'sentry/components/forms/form';
  11. import JsonForm from 'sentry/components/forms/jsonForm';
  12. import LoadingError from 'sentry/components/loadingError';
  13. import LoadingIndicator from 'sentry/components/loadingIndicator';
  14. import Panel from 'sentry/components/panels/panel';
  15. import PanelBody from 'sentry/components/panels/panelBody';
  16. import PanelHeader from 'sentry/components/panels/panelHeader';
  17. import PanelItem from 'sentry/components/panels/panelItem';
  18. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  19. import accountEmailsFields from 'sentry/data/forms/accountEmails';
  20. import {IconDelete, IconStack} from 'sentry/icons';
  21. import {t} from 'sentry/locale';
  22. import {space} from 'sentry/styles/space';
  23. import type {UserEmail} from 'sentry/types/user';
  24. import type {ApiQueryKey} from 'sentry/utils/queryClient';
  25. import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
  26. import useApi from 'sentry/utils/useApi';
  27. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  28. const ENDPOINT = '/users/me/emails/';
  29. function AccountEmails() {
  30. const queryClient = useQueryClient();
  31. const handleSubmitSuccess: FormProps['onSubmitSuccess'] = (response, model, id) => {
  32. queryClient.invalidateQueries({queryKey: makeEmailsEndpointKey()});
  33. if (id !== undefined) {
  34. model.setValue(id, '');
  35. }
  36. if (response?.detail) {
  37. addSuccessMessage(response.detail);
  38. }
  39. };
  40. const handleSubmitError: FormProps['onSubmitError'] = (error, _model, _id) => {
  41. const errorMessage = error?.responseJSON?.detail;
  42. if (errorMessage) {
  43. addErrorMessage(errorMessage);
  44. }
  45. };
  46. return (
  47. <Fragment>
  48. <SentryDocumentTitle title={t('Emails')} />
  49. <SettingsPageHeader title={t('Email Addresses')} />
  50. <EmailAddresses />
  51. <Form
  52. apiMethod="POST"
  53. apiEndpoint={ENDPOINT}
  54. saveOnBlur
  55. allowUndo={false}
  56. onSubmitSuccess={handleSubmitSuccess}
  57. onSubmitError={handleSubmitError}
  58. >
  59. <JsonForm forms={accountEmailsFields} />
  60. </Form>
  61. <AlertLink.Container>
  62. <AlertLink
  63. to="/settings/account/notifications"
  64. trailingItems={<IconStack />}
  65. type="info"
  66. >
  67. {t('Want to change how many emails you get? Use the notifications panel.')}
  68. </AlertLink>
  69. </AlertLink.Container>
  70. </Fragment>
  71. );
  72. }
  73. export default AccountEmails;
  74. function makeEmailsEndpointKey(): ApiQueryKey {
  75. return [ENDPOINT];
  76. }
  77. export function EmailAddresses() {
  78. const api = useApi();
  79. const [isUpdating, setIsUpdating] = useState(false);
  80. const {
  81. data: emails = [],
  82. isPending,
  83. isError,
  84. refetch,
  85. } = useApiQuery<UserEmail[]>(makeEmailsEndpointKey(), {staleTime: 0, gcTime: 0});
  86. if (isPending || isUpdating) {
  87. return (
  88. <Panel>
  89. <PanelHeader>{t('Email Addresses')}</PanelHeader>
  90. <PanelBody>
  91. <LoadingIndicator />
  92. </PanelBody>
  93. </Panel>
  94. );
  95. }
  96. if (isError) {
  97. return <LoadingError onRetry={refetch} />;
  98. }
  99. function doApiCall(endpoint: string, requestParams: RequestOptions) {
  100. setIsUpdating(true);
  101. api
  102. .requestPromise(endpoint, requestParams)
  103. .catch(err => {
  104. if (err?.responseJSON?.email) {
  105. addErrorMessage(err.responseJSON.email);
  106. }
  107. })
  108. .finally(() => {
  109. refetch();
  110. setIsUpdating(false);
  111. });
  112. }
  113. const handleSetPrimary = (email: string) => {
  114. doApiCall(ENDPOINT, {
  115. method: 'PUT',
  116. data: {email},
  117. });
  118. };
  119. const handleRemove = (email: string) => {
  120. doApiCall(ENDPOINT, {
  121. method: 'DELETE',
  122. data: {email},
  123. });
  124. };
  125. const handleVerify = (email: string) => {
  126. doApiCall(`${ENDPOINT}confirm/`, {
  127. method: 'POST',
  128. data: {email},
  129. });
  130. };
  131. const primary = emails.find(({isPrimary}) => isPrimary);
  132. const secondary = emails.filter(({isPrimary}) => !isPrimary);
  133. return (
  134. <Panel>
  135. <PanelHeader>{t('Email Addresses')}</PanelHeader>
  136. <PanelBody>
  137. {primary && (
  138. <EmailRow onRemove={handleRemove} onVerify={handleVerify} {...primary} />
  139. )}
  140. {secondary.map(emailObj => (
  141. <EmailRow
  142. key={emailObj.email}
  143. onSetPrimary={handleSetPrimary}
  144. onRemove={handleRemove}
  145. onVerify={handleVerify}
  146. {...emailObj}
  147. />
  148. ))}
  149. </PanelBody>
  150. </Panel>
  151. );
  152. }
  153. type EmailRowProps = {
  154. email: string;
  155. onRemove: (email: string) => void;
  156. onVerify: (email: string) => void;
  157. hideRemove?: boolean;
  158. isPrimary?: boolean;
  159. isVerified?: boolean;
  160. onSetPrimary?: (email: string) => void;
  161. };
  162. function EmailRow({
  163. email,
  164. onRemove,
  165. onVerify,
  166. onSetPrimary,
  167. isVerified,
  168. isPrimary,
  169. hideRemove,
  170. }: EmailRowProps) {
  171. return (
  172. <EmailItem>
  173. <EmailTags>
  174. {email}
  175. {!isVerified && <Tag type="warning">{t('Unverified')}</Tag>}
  176. {isPrimary && <Tag type="success">{t('Primary')}</Tag>}
  177. </EmailTags>
  178. <ButtonBar gap={1}>
  179. {!isPrimary && isVerified && (
  180. <Button size="sm" onClick={() => onSetPrimary?.(email)}>
  181. {t('Set as primary')}
  182. </Button>
  183. )}
  184. {!isVerified && (
  185. <Button size="sm" onClick={() => onVerify(email)}>
  186. {t('Resend verification')}
  187. </Button>
  188. )}
  189. {!hideRemove && !isPrimary && (
  190. <Button
  191. aria-label={t('Remove email')}
  192. data-test-id="remove"
  193. priority="danger"
  194. size="sm"
  195. icon={<IconDelete />}
  196. onClick={() => onRemove(email)}
  197. />
  198. )}
  199. </ButtonBar>
  200. </EmailItem>
  201. );
  202. }
  203. const EmailTags = styled('div')`
  204. display: grid;
  205. grid-auto-flow: column;
  206. gap: ${space(1)};
  207. align-items: center;
  208. `;
  209. const EmailItem = styled(PanelItem)`
  210. justify-content: space-between;
  211. `;