accountEmails.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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 AlertLink from 'sentry/components/alertLink';
  6. import Tag from 'sentry/components/badge/tag';
  7. import {Button} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  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 to="/settings/account/notifications" icon={<IconStack />}>
  62. {t('Want to change how many emails you get? Use the notifications panel.')}
  63. </AlertLink>
  64. </Fragment>
  65. );
  66. }
  67. export default AccountEmails;
  68. function makeEmailsEndpointKey(): ApiQueryKey {
  69. return [ENDPOINT];
  70. }
  71. export function EmailAddresses() {
  72. const api = useApi();
  73. const [isUpdating, setIsUpdating] = useState(false);
  74. const {
  75. data: emails = [],
  76. isPending,
  77. isError,
  78. refetch,
  79. } = useApiQuery<UserEmail[]>(makeEmailsEndpointKey(), {staleTime: 0, gcTime: 0});
  80. if (isPending || isUpdating) {
  81. return (
  82. <Panel>
  83. <PanelHeader>{t('Email Addresses')}</PanelHeader>
  84. <PanelBody>
  85. <LoadingIndicator />
  86. </PanelBody>
  87. </Panel>
  88. );
  89. }
  90. if (isError) {
  91. return <LoadingError onRetry={refetch} />;
  92. }
  93. function doApiCall(endpoint: string, requestParams: RequestOptions) {
  94. setIsUpdating(true);
  95. api
  96. .requestPromise(endpoint, requestParams)
  97. .catch(err => {
  98. if (err?.responseJSON?.email) {
  99. addErrorMessage(err.responseJSON.email);
  100. }
  101. })
  102. .finally(() => {
  103. refetch();
  104. setIsUpdating(false);
  105. });
  106. }
  107. const handleSetPrimary = (email: string) => {
  108. doApiCall(ENDPOINT, {
  109. method: 'PUT',
  110. data: {email},
  111. });
  112. };
  113. const handleRemove = (email: string) => {
  114. doApiCall(ENDPOINT, {
  115. method: 'DELETE',
  116. data: {email},
  117. });
  118. };
  119. const handleVerify = (email: string) => {
  120. doApiCall(`${ENDPOINT}confirm/`, {
  121. method: 'POST',
  122. data: {email},
  123. });
  124. };
  125. const primary = emails.find(({isPrimary}) => isPrimary);
  126. const secondary = emails.filter(({isPrimary}) => !isPrimary);
  127. return (
  128. <Panel>
  129. <PanelHeader>{t('Email Addresses')}</PanelHeader>
  130. <PanelBody>
  131. {primary && (
  132. <EmailRow onRemove={handleRemove} onVerify={handleVerify} {...primary} />
  133. )}
  134. {secondary.map(emailObj => (
  135. <EmailRow
  136. key={emailObj.email}
  137. onSetPrimary={handleSetPrimary}
  138. onRemove={handleRemove}
  139. onVerify={handleVerify}
  140. {...emailObj}
  141. />
  142. ))}
  143. </PanelBody>
  144. </Panel>
  145. );
  146. }
  147. type EmailRowProps = {
  148. email: string;
  149. onRemove: (email: string) => void;
  150. onVerify: (email: string) => void;
  151. hideRemove?: boolean;
  152. isPrimary?: boolean;
  153. isVerified?: boolean;
  154. onSetPrimary?: (email: string) => void;
  155. };
  156. function EmailRow({
  157. email,
  158. onRemove,
  159. onVerify,
  160. onSetPrimary,
  161. isVerified,
  162. isPrimary,
  163. hideRemove,
  164. }: EmailRowProps) {
  165. return (
  166. <EmailItem>
  167. <EmailTags>
  168. {email}
  169. {!isVerified && <Tag type="warning">{t('Unverified')}</Tag>}
  170. {isPrimary && <Tag type="success">{t('Primary')}</Tag>}
  171. </EmailTags>
  172. <ButtonBar gap={1}>
  173. {!isPrimary && isVerified && (
  174. <Button size="sm" onClick={() => onSetPrimary?.(email)}>
  175. {t('Set as primary')}
  176. </Button>
  177. )}
  178. {!isVerified && (
  179. <Button size="sm" onClick={() => onVerify(email)}>
  180. {t('Resend verification')}
  181. </Button>
  182. )}
  183. {!hideRemove && !isPrimary && (
  184. <Button
  185. aria-label={t('Remove email')}
  186. data-test-id="remove"
  187. priority="danger"
  188. size="sm"
  189. icon={<IconDelete />}
  190. onClick={() => onRemove(email)}
  191. />
  192. )}
  193. </ButtonBar>
  194. </EmailItem>
  195. );
  196. }
  197. const EmailTags = styled('div')`
  198. display: grid;
  199. grid-auto-flow: column;
  200. gap: ${space(1)};
  201. align-items: center;
  202. `;
  203. const EmailItem = styled(PanelItem)`
  204. justify-content: space-between;
  205. `;