accountEmails.tsx 5.6 KB

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