From 6729e75d24ed71fcf6b05d50b25e6a0ebb5dc8ea Mon Sep 17 00:00:00 2001 From: Anji Tong Date: Thu, 21 Nov 2024 20:23:33 +0000 Subject: [PATCH] feat(organizations): disable email updates for regular organization members TASK-997 (#5233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🗒️ Checklist 1. [x] run linter locally 2. [x] update all related docs (API, README, inline, etc.), if any 3. [x] draft PR with a title `(): TASK-1234` 4. [x] tag PR: at least `frontend` or `backend` unless it's global 5. [x] fill in the template below and delete template comments 6. [x] review thyself: read the diff and repro the preview as written 7. [ ] open PR & confirm that CI passes 8. [x] request reviewers, if needed 9. [ ] delete this section before merging ### 📣 Summary <!-- Delete this section if changes are internal only. --> <!-- One sentence summary for the public changelog, worded for non-technical seasoned Kobo users. --> UI for changing emails is disabled for `Members` of a multi-member organization. ### 👀 Preview steps 1. Make sure your user's email is correct in both the user section of the django admin as well as the email address section. If there is a difference it would only show the former here. See https://www.notion.so/kobotoolbox/task-1236 2. Make an MMO that has an owner, admin, and a member, as well as a regular user 3. Navigate to the account settings page 4. Visit the security section 5. If the user is a regular user, or an MMO owner or admin, they should be able to change their email address as normal 6. If the user is a member of an MMO the text box and button should be gone ### 📖 Description <!-- Delete this section if summary already said everything. --> <!-- Full description for the public changelog, worded for non-technical seasoned Kobo users. --> Remove the text box and button entirely if the user is a `member`. Replaced with just text of the email returned from `/me`. ### 💭 Notes <!-- Delete this section if empty. --> <!-- Anything else useful that's not said above,worded for reviewers, testers, and future git archaeologist collegues. Examples: - screenshots, copy-pasted logs, etc. - what was tried but didn't work, - conscious short-term vs long-term tradeoffs, - proactively answer likely questions, --> Make sure the user either has the proper email reflected in the user section of the django admin or was created through an email confirmation link. See https://www.notion.so/kobotoolbox/task-1236 --------- Co-authored-by: James Kiger <james.kiger@gmail.com> --- .../security/email/emailSection.component.tsx | 159 ++++++++++-------- .../security/email/emailSection.module.scss | 10 ++ 2 files changed, 95 insertions(+), 74 deletions(-) diff --git a/jsapp/js/account/security/email/emailSection.component.tsx b/jsapp/js/account/security/email/emailSection.component.tsx index e350affae5..4877dcc8a8 100644 --- a/jsapp/js/account/security/email/emailSection.component.tsx +++ b/jsapp/js/account/security/email/emailSection.component.tsx @@ -10,6 +10,7 @@ import { deleteUnverifiedUserEmails, } from './emailSection.api'; import type {EmailResponse} from './emailSection.api'; +import {useOrganizationQuery} from '../../organization/organizationQuery'; // Partial components import Button from 'jsapp/js/components/common/button'; @@ -33,6 +34,8 @@ interface EmailState { export default function EmailSection() { const [session] = useState(() => sessionStore); + const orgQuery = useOrganizationQuery(); + let initialEmail = ''; if ('email' in session.currentAccount) { initialEmail = session.currentAccount.email; @@ -116,6 +119,10 @@ export default function EmailSection() { const unverifiedEmail = email.emails.find( (userEmail) => !userEmail.verified && !userEmail.primary ); + const isReady = session.isInitialLoadComplete && 'email' in currentAccount; + const userCanChangeEmail = orgQuery.data?.is_mmo + ? orgQuery.data.request_user_role !== 'member' + : true; return ( <section className={securityStyles.securitySection}> @@ -123,83 +130,87 @@ export default function EmailSection() { <h2 className={securityStyles.securitySectionTitleText}>{t('Email address')}</h2> </div> - <div className={cx(securityStyles.securitySectionBody, styles.body)}> - {!session.isPending && - session.isInitialLoadComplete && - 'email' in currentAccount && ( - <TextBox - value={email.newEmail} - placeholder={t('Type new email address')} - onChange={onTextFieldChange.bind(onTextFieldChange)} - type='email' - /> - )} - - {unverifiedEmail?.email && - !session.isPending && - session.isInitialLoadComplete && - 'email' in currentAccount && ( - <> - <div className={styles.unverifiedEmail}> - <Icon name='alert' /> - <p className={styles.blurb}> - <strong> - {t('Check your email ##UNVERIFIED_EMAIL##. ').replace( - '##UNVERIFIED_EMAIL##', - unverifiedEmail.email - )} - </strong> - - {t( - 'A verification link has been sent to confirm your ownership. Once confirmed, this address will replace ##UNVERIFIED_EMAIL##' - ).replace('##UNVERIFIED_EMAIL##', currentAccount.email)} - </p> - </div> - - <div className={styles.unverifiedEmailButtons}> - <Button - label='Resend' - size='m' - type='secondary' - onClick={resendNewUserEmail.bind( - resendNewUserEmail, + <div + className={cx([ + securityStyles.securitySectionBody, + userCanChangeEmail ? styles.body : styles.emailUpdateDisabled, + ])} + > + {isReady && userCanChangeEmail ? ( + <TextBox + value={email.newEmail} + placeholder={t('Type new email address')} + onChange={onTextFieldChange.bind(onTextFieldChange)} + type='email' + /> + ) : ( + <div className={styles.emailText}>{email.newEmail}</div> + )} + + {unverifiedEmail?.email && isReady && ( + <> + <div className={styles.unverifiedEmail}> + <Icon name='alert' /> + <p className={styles.blurb}> + <strong> + {t('Check your email ##UNVERIFIED_EMAIL##. ').replace( + '##UNVERIFIED_EMAIL##', unverifiedEmail.email )} - /> - <Button - label='Remove' - size='m' - type='secondary-danger' - onClick={deleteNewUserEmail} - /> - </div> - - {email.refreshedEmail && ( - <label> - {t('Email was sent again: ##TIMESTAMP##').replace( - '##TIMESTAMP##', - email.refreshedEmailDate - )} - </label> - )} - </> - )} + </strong> + + {t( + 'A verification link has been sent to confirm your ownership. Once confirmed, this address will replace ##UNVERIFIED_EMAIL##' + ).replace('##UNVERIFIED_EMAIL##', currentAccount.email)} + </p> + </div> + + <div className={styles.unverifiedEmailButtons}> + <Button + label='Resend' + size='m' + type='secondary' + onClick={resendNewUserEmail.bind( + resendNewUserEmail, + unverifiedEmail.email + )} + /> + <Button + label='Remove' + size='m' + type='secondary-danger' + onClick={deleteNewUserEmail} + /> + </div> + + {email.refreshedEmail && ( + <label> + {t('Email was sent again: ##TIMESTAMP##').replace( + '##TIMESTAMP##', + email.refreshedEmailDate + )} + </label> + )} + </> + )} </div> - - <form - className={styles.options} - onSubmit={(e) => { - e.preventDefault(); - handleSubmit(); - }} - > - <Button - label='Change' - size='m' - type='primary' - onClick={handleSubmit} - /> - </form> + {userCanChangeEmail && ( + <div className={styles.options}> + <form + onSubmit={(e) => { + e.preventDefault(); + handleSubmit(); + }} + > + <Button + label='Change' + size='m' + type='primary' + onClick={handleSubmit} + /> + </form> + </div> + )} </section> ); } diff --git a/jsapp/js/account/security/email/emailSection.module.scss b/jsapp/js/account/security/email/emailSection.module.scss index 7675c0999f..3350c95c9d 100644 --- a/jsapp/js/account/security/email/emailSection.module.scss +++ b/jsapp/js/account/security/email/emailSection.module.scss @@ -34,3 +34,13 @@ display: flex; gap: 10px; } + +.emailText { + font-weight: 600; +} + +.emailUpdateDisabled { + flex: 5; + // To compensate for the `options` class not displaying when there is no email + margin-right: calc(30% + 8px); +}