accountEmails.tsx 5.8 KB

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