accountEmails.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import {Fragment} 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 DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  9. import Form, {FormProps} from 'sentry/components/forms/form';
  10. import JsonForm from 'sentry/components/forms/jsonForm';
  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 Tag from 'sentry/components/tag';
  17. import accountEmailsFields from 'sentry/data/forms/accountEmails';
  18. import {IconDelete, IconStack} from 'sentry/icons';
  19. import {t} from 'sentry/locale';
  20. import {space} from 'sentry/styles/space';
  21. import {UserEmail} from 'sentry/types';
  22. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  23. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  24. const ENDPOINT = '/users/me/emails/';
  25. type Props = DeprecatedAsyncView['props'];
  26. type State = DeprecatedAsyncView['state'] & {
  27. emails: UserEmail[];
  28. };
  29. class AccountEmails extends DeprecatedAsyncView<Props, State> {
  30. getTitle() {
  31. return t('Emails');
  32. }
  33. getEndpoints() {
  34. return [];
  35. }
  36. handleSubmitSuccess: FormProps['onSubmitSuccess'] = (_change, model, id) => {
  37. if (id === undefined) {
  38. return;
  39. }
  40. model.setValue(id, '');
  41. this.remountComponent();
  42. };
  43. renderBody() {
  44. return (
  45. <Fragment>
  46. <SettingsPageHeader title={t('Email Addresses')} />
  47. <EmailAddresses />
  48. <Form
  49. apiMethod="POST"
  50. apiEndpoint={ENDPOINT}
  51. saveOnBlur
  52. allowUndo={false}
  53. onSubmitSuccess={this.handleSubmitSuccess}
  54. >
  55. <JsonForm forms={accountEmailsFields} />
  56. </Form>
  57. <AlertLink to="/settings/account/notifications" icon={<IconStack />}>
  58. {t('Want to change how many emails you get? Use the notifications panel.')}
  59. </AlertLink>
  60. </Fragment>
  61. );
  62. }
  63. }
  64. export default AccountEmails;
  65. export class EmailAddresses extends DeprecatedAsyncComponent<Props, State> {
  66. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  67. return [['emails', ENDPOINT]];
  68. }
  69. doApiCall(endpoint: string, requestParams: RequestOptions) {
  70. this.setState({loading: true, emails: []}, () =>
  71. this.api
  72. .requestPromise(endpoint, requestParams)
  73. .then(() => this.remountComponent())
  74. .catch(err => {
  75. this.remountComponent();
  76. if (err?.responseJSON?.email) {
  77. addErrorMessage(err.responseJSON.email);
  78. }
  79. })
  80. );
  81. }
  82. handleSetPrimary = (email: string) =>
  83. this.doApiCall(ENDPOINT, {
  84. method: 'PUT',
  85. data: {email},
  86. });
  87. handleRemove = (email: string) =>
  88. this.doApiCall(ENDPOINT, {
  89. method: 'DELETE',
  90. data: {email},
  91. });
  92. handleVerify = (email: string) =>
  93. this.doApiCall(`${ENDPOINT}confirm/`, {
  94. method: 'POST',
  95. data: {email},
  96. });
  97. render() {
  98. const {emails, loading} = this.state;
  99. const primary = emails?.find(({isPrimary}) => isPrimary);
  100. const secondary = emails?.filter(({isPrimary}) => !isPrimary);
  101. if (loading) {
  102. return (
  103. <Panel>
  104. <PanelHeader>{t('Email Addresses')}</PanelHeader>
  105. <PanelBody>
  106. <LoadingIndicator />
  107. </PanelBody>
  108. </Panel>
  109. );
  110. }
  111. return (
  112. <Panel>
  113. <PanelHeader>{t('Email Addresses')}</PanelHeader>
  114. <PanelBody>
  115. {primary && (
  116. <EmailRow
  117. onRemove={this.handleRemove}
  118. onVerify={this.handleVerify}
  119. {...primary}
  120. />
  121. )}
  122. {secondary?.map(emailObj => (
  123. <EmailRow
  124. key={emailObj.email}
  125. onSetPrimary={this.handleSetPrimary}
  126. onRemove={this.handleRemove}
  127. onVerify={this.handleVerify}
  128. {...emailObj}
  129. />
  130. ))}
  131. </PanelBody>
  132. </Panel>
  133. );
  134. }
  135. }
  136. type EmailRowProps = {
  137. email: string;
  138. onRemove: (email: string, e: React.MouseEvent) => void;
  139. onVerify: (email: string, e: React.MouseEvent) => void;
  140. hideRemove?: boolean;
  141. isPrimary?: boolean;
  142. isVerified?: boolean;
  143. onSetPrimary?: (email: string, e: React.MouseEvent) => 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={e => onSetPrimary?.(email, e)}>
  164. {t('Set as primary')}
  165. </Button>
  166. )}
  167. {!isVerified && (
  168. <Button size="sm" onClick={e => onVerify(email, e)}>
  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={e => onRemove(email, e)}
  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. `;