accountEmails.tsx 5.7 KB

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