accountEmails.tsx 5.6 KB

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