From abcac63518f1c99053b74f4b277aab211485866d Mon Sep 17 00:00:00 2001 From: Flaminia Cavallo Date: Thu, 9 Jan 2025 08:39:36 +0100 Subject: [PATCH 1/6] feat: first draft --- i18n/en.pot | 4 +- src/layout/FormFields.component.js | 20 +++++++++- src/layout/ModalField.component.js | 63 ++++++++++++++++++++++++++++++ src/userSettingsMapping.js | 2 +- 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 src/layout/ModalField.component.js diff --git a/i18n/en.pot b/i18n/en.pot index fd723b7b..23d6c939 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-01-07T11:04:56.155Z\n" -"PO-Revision-Date: 2025-01-07T11:04:56.155Z\n" +"POT-Creation-Date: 2025-01-08T13:52:44.921Z\n" +"PO-Revision-Date: 2025-01-08T13:52:44.921Z\n" msgid "Never" msgstr "Never" diff --git a/src/layout/FormFields.component.js b/src/layout/FormFields.component.js index 45f91315..19f64e59 100644 --- a/src/layout/FormFields.component.js +++ b/src/layout/FormFields.component.js @@ -18,6 +18,7 @@ import AvatarEditor from './AvatarEditor.component.js' import AppTheme from './theme.js' import { VerifyEmail } from './VerifyEmail.component.js' import { VerifyEmailWarning } from './VerifyEmailWarning.js' +import {ModalField} from "./ModalField.component"; const styles = { header: { @@ -243,6 +244,18 @@ function createVerifyButton(fieldBase, valueStore) { }) } +function createModalField(fieldBase, valueStore, onUpdate) { + return Object.assign({}, fieldBase, { + component: ModalField, + props: { + onUpdate, + userEmail: valueStore.state['email'] || '', + setUserEmail: email => { + valueStore.setState({...valueStore.state, email})} + }, + }) +} + function createFieldBaseObject(fieldName, mapping, valueStore) { if (!mapping) { log.warn(`Mapping not found for field: ${fieldName}`) @@ -276,7 +289,7 @@ function createFieldBaseObject(fieldName, mapping, valueStore) { ) } -function createField(fieldName, valueStore, d2) { +function createField(fieldName, valueStore, d2, onUpdate) { const mapping = userSettingsKeyMapping[fieldName] const fieldBase = createFieldBaseObject(fieldName, mapping, valueStore) @@ -295,6 +308,8 @@ function createField(fieldName, valueStore, d2) { return createAvatarEditor(fieldBase, d2, valueStore) case 'submit': return createVerifyButton(fieldBase, valueStore) + case 'modal': + return createModalField(fieldBase, valueStore, onUpdate) default: log.warn( `Unknown control type "${mapping.type}" encountered for field "${fieldName}"` @@ -365,9 +380,10 @@ class FormFields extends Component { renderFields(fieldNames) { const d2 = this.context.d2 const valueStore = this.props.valueStore + const onUpdate = this.props.onUpdateField // Create the regular fields const fields = fieldNames - .map((fieldName) => createField(fieldName, valueStore, d2)) + .map((fieldName) => createField(fieldName, valueStore, d2, onUpdate)) .filter((field) => !!field.name) .map((field) => wrapFieldWithLabel(field)) diff --git a/src/layout/ModalField.component.js b/src/layout/ModalField.component.js new file mode 100644 index 00000000..84082e6e --- /dev/null +++ b/src/layout/ModalField.component.js @@ -0,0 +1,63 @@ +import React, {useMemo, useState} from 'react' +import { + Button, + ButtonStrip, + composeValidators, + email, + hasValue, + InputField, + Modal, + ModalActions, + ModalContent, + ModalTitle, +} from "@dhis2/ui"; +import PropTypes from "prop-types"; + +export function ModalField({userEmail, setUserEmail, onUpdate}) { + + const [modalOpen, setModalOpen] = useState() + const [newEmail, setNewEmail] = useState(userEmail) + const isValidNewEmail = useMemo(() => + newEmail === "mamma" ? false : true + , [newEmail]) + return ( + <> + + + + Update email + + + setNewEmail(newValue.value)}/> + + + + + + + + + + + + ) +} + +ModalField.propTypes = { + userEmail: PropTypes.string +} diff --git a/src/userSettingsMapping.js b/src/userSettingsMapping.js index cf1163a0..c4b2c946 100644 --- a/src/userSettingsMapping.js +++ b/src/userSettingsMapping.js @@ -29,7 +29,7 @@ const settingsKeyMapping = { }, email: { label: i18n.t('E-mail'), - type: 'textfield', + type: 'modal', validators: ['email'], }, emailVerification: { From 17a281d2f59ac598b118adc411255479270811ff Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Fri, 10 Jan 2025 10:50:53 +0100 Subject: [PATCH 2/6] feat: more work for email update modal --- i18n/en.pot | 43 +++++- src/layout/FormFields.component.js | 27 +++- src/layout/ModalField.component.js | 169 +++++++++++++++++---- src/layout/ModalField.component.module.css | 11 ++ src/layout/VerifyEmail.component.js | 11 +- src/layout/VerifyEmailWarning.js | 10 +- src/userSettingsMapping.js | 1 - 7 files changed, 215 insertions(+), 57 deletions(-) create mode 100644 src/layout/ModalField.component.module.css diff --git a/i18n/en.pot b/i18n/en.pot index 23d6c939..c8448786 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-01-08T13:52:44.921Z\n" -"PO-Revision-Date: 2025-01-08T13:52:44.921Z\n" +"POT-Creation-Date: 2025-01-09T15:27:50.496Z\n" +"PO-Revision-Date: 2025-01-09T15:27:50.496Z\n" msgid "Never" msgstr "Never" @@ -346,6 +346,42 @@ msgstr "No options" msgid "System default" msgstr "System default" +msgid "No email provided" +msgstr "No email provided" + +msgid "Email is invalid" +msgstr "Email is invalid" + +msgid "Emails must match" +msgstr "Emails must match" + +msgid "Email" +msgstr "Email" + +msgid "Update email" +msgstr "Update email" + +msgid "Your email is currently verified" +msgstr "Your email is currently verified" + +msgid "If you change your email, you may need to reverify your email." +msgstr "If you change your email, you may need to reverify your email." + +msgid "Current email" +msgstr "Current email" + +msgid "Enter new email" +msgstr "Enter new email" + +msgid "Confirm new email" +msgstr "Confirm new email" + +msgid "Cancel" +msgstr "Cancel" + +msgid "Save" +msgstr "Save" + msgid "User profile" msgstr "User profile" @@ -522,9 +558,6 @@ msgstr "" msgid "Token details" msgstr "Token details" -msgid "Cancel" -msgstr "Cancel" - msgid "" "Important: IP address validation relies on the X-Forwarded-For header, " "which can be spoofed. For security, make sure a load balancer or reverse " diff --git a/src/layout/FormFields.component.js b/src/layout/FormFields.component.js index 19f64e59..c2ef636f 100644 --- a/src/layout/FormFields.component.js +++ b/src/layout/FormFields.component.js @@ -15,10 +15,10 @@ import optionValueStore from '../optionValue.store.js' import userSettingsStore from '../settings/userSettings.store.js' import userSettingsKeyMapping from '../userSettingsMapping.js' import AvatarEditor from './AvatarEditor.component.js' +import { ModalField } from './ModalField.component.js' import AppTheme from './theme.js' import { VerifyEmail } from './VerifyEmail.component.js' import { VerifyEmailWarning } from './VerifyEmailWarning.js' -import {ModalField} from "./ModalField.component"; const styles = { header: { @@ -244,14 +244,18 @@ function createVerifyButton(fieldBase, valueStore) { }) } -function createModalField(fieldBase, valueStore, onUpdate) { +function createModalField({ fieldBase, valueStore, onUpdate, d2 }) { return Object.assign({}, fieldBase, { component: ModalField, props: { onUpdate, userEmail: valueStore.state['email'] || '', - setUserEmail: email => { - valueStore.setState({...valueStore.state, email})} + userEmailVerified: d2?.currentUser?.emailVerified, + setUserEmail: (email) => { + valueStore.state['email'] = email + valueStore.state['emailUpdated'] = true + valueStore.setState(valueStore.state) + }, }, }) } @@ -289,7 +293,7 @@ function createFieldBaseObject(fieldName, mapping, valueStore) { ) } -function createField(fieldName, valueStore, d2, onUpdate) { +function createField({ fieldName, valueStore, d2, onUpdate }) { const mapping = userSettingsKeyMapping[fieldName] const fieldBase = createFieldBaseObject(fieldName, mapping, valueStore) @@ -309,7 +313,7 @@ function createField(fieldName, valueStore, d2, onUpdate) { case 'submit': return createVerifyButton(fieldBase, valueStore) case 'modal': - return createModalField(fieldBase, valueStore, onUpdate) + return createModalField({ fieldBase, valueStore, onUpdate, d2 }) default: log.warn( `Unknown control type "${mapping.type}" encountered for field "${fieldName}"` @@ -383,7 +387,9 @@ class FormFields extends Component { const onUpdate = this.props.onUpdateField // Create the regular fields const fields = fieldNames - .map((fieldName) => createField(fieldName, valueStore, d2, onUpdate)) + .map((fieldName) => + createField({ fieldName, valueStore, d2, onUpdate }) + ) .filter((field) => !!field.name) .map((field) => wrapFieldWithLabel(field)) @@ -405,7 +411,12 @@ class FormFields extends Component {
{this.props.pageLabel}
{this.context?.d2 && ( - + )} {this.renderFields(this.props.fieldKeys)} diff --git a/src/layout/ModalField.component.js b/src/layout/ModalField.component.js index 84082e6e..d0ef250e 100644 --- a/src/layout/ModalField.component.js +++ b/src/layout/ModalField.component.js @@ -1,63 +1,168 @@ -import React, {useMemo, useState} from 'react' +import i18n from '@dhis2/d2-i18n' import { Button, ButtonStrip, - composeValidators, - email, - hasValue, + email as emailValidator, InputField, Modal, ModalActions, ModalContent, ModalTitle, -} from "@dhis2/ui"; -import PropTypes from "prop-types"; + NoticeBox, + Tooltip, +} from '@dhis2/ui' +import TextField from 'd2-ui/lib/form-fields/TextField' +import PropTypes from 'prop-types' +import React, { useMemo, useState } from 'react' +import styles from './ModalField.component.module.css' -export function ModalField({userEmail, setUserEmail, onUpdate}) { +const TooltipWrapper = ({ disabled, content, children }) => { + if (!disabled) { + return <>{children} + } + return {children} +} + +TooltipWrapper.propTypes = { + children: PropTypes.node, + content: PropTypes.string, + disabled: PropTypes.bool, +} +const getSaveDisabledContent = ({ newEmail, emailValidationMessage }) => { + if (!newEmail) { + return i18n.t('No email provided') + } + if (emailValidationMessage) { + return i18n.t('Email is invalid') + } + return i18n.t('Emails must match') +} + +export function ModalField({ + userEmail, + userEmailVerified, + setUserEmail, + onUpdate, +}) { const [modalOpen, setModalOpen] = useState() - const [newEmail, setNewEmail] = useState(userEmail) - const isValidNewEmail = useMemo(() => - newEmail === "mamma" ? false : true - , [newEmail]) + const [newEmail, setNewEmail] = useState() + const [newEmailConfirm, setNewEmailConfirm] = useState() + const [newEmailTouched, setNewEmailTouched] = useState(false) + const emailValidationMessage = useMemo( + () => emailValidator(newEmail), + [newEmail] + ) + const emailsMatch = newEmail === newEmailConfirm + const saveDisabled = + !newEmail || Boolean(emailValidationMessage) || !emailsMatch + const saveDisabledContent = getSaveDisabledContent({ + newEmail, + emailValidationMessage, + }) + + const closeModal = () => { + setModalOpen(false) + setNewEmail() + setNewEmailConfirm() + setNewEmailTouched(false) + } return ( - <> - - - - Update email +
+ +
+ +
+ + {i18n.t('Update email')} + {userEmailVerified && ( + + {i18n.t( + 'If you change your email, you may need to reverify your email.' + )} + + )} + + setNewEmail(newValue.value)}/> + error={Boolean(emailValidationMessage)} + validationText={emailValidationMessage} + onChange={(newValue) => setNewEmail(newValue.value)} + className={styles.emailModalItem} + /> + + { + setNewEmailTouched(true) + setNewEmailConfirm(newValue.value) + }} + className={styles.emailModalItem} + /> - - + + + - +
) } ModalField.propTypes = { - userEmail: PropTypes.string + setUserEmail: PropTypes.func, + userEmail: PropTypes.string, + userEmailVerified: PropTypes.bool, + onUpdate: PropTypes.func, } diff --git a/src/layout/ModalField.component.module.css b/src/layout/ModalField.component.module.css new file mode 100644 index 00000000..13a81bcd --- /dev/null +++ b/src/layout/ModalField.component.module.css @@ -0,0 +1,11 @@ +.emailModalContainer { + margin-block-end: 8px; +} + +.emailTextField { + width: 100%; +} + +.emailModalItem { + margin-block-end: 16px; +} \ No newline at end of file diff --git a/src/layout/VerifyEmail.component.js b/src/layout/VerifyEmail.component.js index 0abfd373..72bdc8f4 100644 --- a/src/layout/VerifyEmail.component.js +++ b/src/layout/VerifyEmail.component.js @@ -1,5 +1,5 @@ import { useAlert, useDataMutation, useConfig } from '@dhis2/app-runtime' -import { Button } from '@dhis2/ui' +import { Button, email as emailValidator } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' import i18n from '../locales/index.js' @@ -9,9 +9,6 @@ const sendEmailVerificationMutation = { type: 'create', } -const emailRegExp = - /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ - export function VerifyEmail({ userEmail }) { const errorAlert = useAlert(({ message }) => message, { critical: true }) const successAlert = useAlert(({ message }) => message, { success: true }) @@ -37,7 +34,7 @@ export function VerifyEmail({ userEmail }) { const emailConfigured = systemInfo?.emailConfigured - const isValidEmail = emailRegExp.test(userEmail) + const isInvalidEmail = Boolean(emailValidator(userEmail)) if (!emailConfigured) { return null @@ -48,10 +45,10 @@ export function VerifyEmail({ userEmail }) { ) diff --git a/src/layout/VerifyEmailWarning.js b/src/layout/VerifyEmailWarning.js index 98199bf7..8ff688ce 100644 --- a/src/layout/VerifyEmailWarning.js +++ b/src/layout/VerifyEmailWarning.js @@ -3,12 +3,13 @@ import { NoticeBox } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' -export function VerifyEmailWarning({ config }) { +export function VerifyEmailWarning({ config, emailUpdated }) { const enforceVerifiedEmail = - config.system?.settings?.settings?.enforceVerifiedEmail || false - const emailVerified = config.currentUser?.emailVerified || false + config.system?.settings?.settings?.enforceVerifiedEmail ?? false + const emailNotVerified = + (!config.currentUser?.emailVerified || emailUpdated) ?? false - if (enforceVerifiedEmail && !emailVerified) { + if (enforceVerifiedEmail && emailNotVerified) { return (
@@ -36,4 +37,5 @@ VerifyEmailWarning.propTypes = { }), }), }).isRequired, + emailUpdated: PropTypes.bool, } diff --git a/src/userSettingsMapping.js b/src/userSettingsMapping.js index c4b2c946..8c648776 100644 --- a/src/userSettingsMapping.js +++ b/src/userSettingsMapping.js @@ -30,7 +30,6 @@ const settingsKeyMapping = { email: { label: i18n.t('E-mail'), type: 'modal', - validators: ['email'], }, emailVerification: { name: 'emailVerification', From 38b79c991053e3955eb5ebef530abe0d5e9c1d02 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Mon, 13 Jan 2025 14:13:04 +0100 Subject: [PATCH 3/6] feat: clean up --- i18n/en.pot | 37 +++++---- src/layout/FormFields.component.js | 10 --- src/layout/ModalField.component.js | 95 ++++++++++++++++++++-- src/layout/ModalField.component.module.css | 5 ++ src/layout/VerifyEmail.component.js | 18 ++-- src/profile/Profile.component.js | 1 - src/userSettingsMapping.js | 5 -- 7 files changed, 122 insertions(+), 49 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index c8448786..80593ff0 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-01-09T15:27:50.496Z\n" -"PO-Revision-Date: 2025-01-09T15:27:50.496Z\n" +"POT-Creation-Date: 2025-01-13T13:12:31.582Z\n" +"PO-Revision-Date: 2025-01-13T13:12:31.583Z\n" msgid "Never" msgstr "Never" @@ -355,14 +355,26 @@ msgstr "Email is invalid" msgid "Emails must match" msgstr "Emails must match" +msgid "Remove email" +msgstr "Remove email" + +msgid "Your email is currently verified" +msgstr "Your email is currently verified" + +msgid "Are you sure you want to remove your email?" +msgstr "Are you sure you want to remove your email?" + +msgid "Cancel" +msgstr "Cancel" + msgid "Email" msgstr "Email" -msgid "Update email" -msgstr "Update email" +msgid "Change email" +msgstr "Change email" -msgid "Your email is currently verified" -msgstr "Your email is currently verified" +msgid "There is no email to remove" +msgstr "There is no email to remove" msgid "If you change your email, you may need to reverify your email." msgstr "If you change your email, you may need to reverify your email." @@ -370,15 +382,15 @@ msgstr "If you change your email, you may need to reverify your email." msgid "Current email" msgstr "Current email" +msgid "no current email" +msgstr "no current email" + msgid "Enter new email" msgstr "Enter new email" msgid "Confirm new email" msgstr "Confirm new email" -msgid "Cancel" -msgstr "Cancel" - msgid "Save" msgstr "Save" @@ -409,8 +421,8 @@ msgstr "Email verification link sent successfully!" msgid "Failed to send email verification link." msgstr "Failed to send email verification link." -msgid "Verify Email" -msgstr "Verify Email" +msgid "Verify email" +msgstr "Verify email" msgid "" "Your email is not verified. Please verify your email to continue using the " @@ -640,9 +652,6 @@ msgstr "Other" msgid "E-mail" msgstr "E-mail" -msgid "E-mail Verification" -msgstr "E-mail Verification" - msgid "Mobile phone number" msgstr "Mobile phone number" diff --git a/src/layout/FormFields.component.js b/src/layout/FormFields.component.js index c2ef636f..10254669 100644 --- a/src/layout/FormFields.component.js +++ b/src/layout/FormFields.component.js @@ -17,7 +17,6 @@ import userSettingsKeyMapping from '../userSettingsMapping.js' import AvatarEditor from './AvatarEditor.component.js' import { ModalField } from './ModalField.component.js' import AppTheme from './theme.js' -import { VerifyEmail } from './VerifyEmail.component.js' import { VerifyEmailWarning } from './VerifyEmailWarning.js' const styles = { @@ -237,13 +236,6 @@ function createAvatarEditor(fieldBase, d2, valueStore) { }) } -function createVerifyButton(fieldBase, valueStore) { - return Object.assign({}, fieldBase, { - component: VerifyEmail, - props: { userEmail: valueStore.state['email'] || '' }, - }) -} - function createModalField({ fieldBase, valueStore, onUpdate, d2 }) { return Object.assign({}, fieldBase, { component: ModalField, @@ -310,8 +302,6 @@ function createField({ fieldName, valueStore, d2, onUpdate }) { return createAccountEditor(fieldBase, d2, valueStore) case 'avatar': return createAvatarEditor(fieldBase, d2, valueStore) - case 'submit': - return createVerifyButton(fieldBase, valueStore) case 'modal': return createModalField({ fieldBase, valueStore, onUpdate, d2 }) default: diff --git a/src/layout/ModalField.component.js b/src/layout/ModalField.component.js index d0ef250e..39b94c64 100644 --- a/src/layout/ModalField.component.js +++ b/src/layout/ModalField.component.js @@ -15,6 +15,7 @@ import TextField from 'd2-ui/lib/form-fields/TextField' import PropTypes from 'prop-types' import React, { useMemo, useState } from 'react' import styles from './ModalField.component.module.css' +import { VerifyEmail } from './VerifyEmail.component.js' const TooltipWrapper = ({ disabled, content, children }) => { if (!disabled) { @@ -39,6 +40,56 @@ const getSaveDisabledContent = ({ newEmail, emailValidationMessage }) => { return i18n.t('Emails must match') } +const RemoveModal = ({ + removeModalOpen, + closeModal, + userEmailVerified, + setUserEmail, + onUpdate, +}) => ( + + {i18n.t('Remove email')} + + + {userEmailVerified && ( + + )} +
{i18n.t('Are you sure you want to remove your email?')}
+
+ + + + + + + + +
+) + +RemoveModal.propTypes = { + closeModal: PropTypes.func, + removeModalOpen: PropTypes.bool, + setUserEmail: PropTypes.bool, + userEmailVerified: PropTypes.bool, + onUpdate: PropTypes.func, +} + export function ModalField({ userEmail, userEmailVerified, @@ -46,9 +97,10 @@ export function ModalField({ onUpdate, }) { const [modalOpen, setModalOpen] = useState() + const [removeModalOpen, setRemoveModalOpen] = useState() const [newEmail, setNewEmail] = useState() const [newEmailConfirm, setNewEmailConfirm] = useState() - const [newEmailTouched, setNewEmailTouched] = useState(false) + const [newEmailConfirmTouched, setNewEmailConfirmTouched] = useState(false) const emailValidationMessage = useMemo( () => emailValidator(newEmail), [newEmail] @@ -63,9 +115,10 @@ export function ModalField({ const closeModal = () => { setModalOpen(false) + setRemoveModalOpen(false) setNewEmail() setNewEmailConfirm() - setNewEmailTouched(false) + setNewEmailConfirmTouched(false) } return (
@@ -75,13 +128,26 @@ export function ModalField({ floatingLabelText={i18n.t('Email')} style={{ width: '100%' }} /> -
+
+ + + +
- {i18n.t('Update email')} + {i18n.t('Change email')} {userEmailVerified && ( @@ -98,7 +164,11 @@ export function ModalField({ { - setNewEmailTouched(true) + setNewEmailConfirmTouched(true) setNewEmailConfirm(newValue.value) }} className={styles.emailModalItem} @@ -156,6 +226,13 @@ export function ModalField({ +
) } diff --git a/src/layout/ModalField.component.module.css b/src/layout/ModalField.component.module.css index 13a81bcd..41545276 100644 --- a/src/layout/ModalField.component.module.css +++ b/src/layout/ModalField.component.module.css @@ -8,4 +8,9 @@ .emailModalItem { margin-block-end: 16px; +} + +.buttonContainer { + display: flex; + gap: 8px; } \ No newline at end of file diff --git a/src/layout/VerifyEmail.component.js b/src/layout/VerifyEmail.component.js index 72bdc8f4..5865126a 100644 --- a/src/layout/VerifyEmail.component.js +++ b/src/layout/VerifyEmail.component.js @@ -41,16 +41,14 @@ export function VerifyEmail({ userEmail }) { } return ( -
- -
+ ) } diff --git a/src/profile/Profile.component.js b/src/profile/Profile.component.js index 4664196f..3c0dc680 100644 --- a/src/profile/Profile.component.js +++ b/src/profile/Profile.component.js @@ -9,7 +9,6 @@ function EditProfile() { 'firstName', 'surname', 'email', - 'emailVerification', 'avatar', 'phoneNumber', 'introduction', diff --git a/src/userSettingsMapping.js b/src/userSettingsMapping.js index 8c648776..272c47a8 100644 --- a/src/userSettingsMapping.js +++ b/src/userSettingsMapping.js @@ -31,11 +31,6 @@ const settingsKeyMapping = { label: i18n.t('E-mail'), type: 'modal', }, - emailVerification: { - name: 'emailVerification', - label: i18n.t('E-mail Verification'), - type: 'submit', - }, phoneNumber: { label: i18n.t('Mobile phone number'), type: 'textfield', From 4087c372e67f5dfad261ca4111f7e151743689ca Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Mon, 13 Jan 2025 16:16:09 +0100 Subject: [PATCH 4/6] feat: empty email refinement --- i18n/en.pot | 7 +++++-- src/layout/FormFields.component.js | 3 +++ src/layout/ModalField.component.js | 2 +- src/layout/VerifyEmailWarning.js | 13 +++++++++---- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 80593ff0..13db6083 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-01-13T13:12:31.582Z\n" -"PO-Revision-Date: 2025-01-13T13:12:31.583Z\n" +"POT-Creation-Date: 2025-01-13T15:15:24.364Z\n" +"PO-Revision-Date: 2025-01-13T15:15:24.364Z\n" msgid "Never" msgstr "Never" @@ -431,6 +431,9 @@ msgstr "" "Your email is not verified. Please verify your email to continue using the " "system." +msgid "Please provide an email and verify it to continue using the system." +msgstr "Please provide an email and verify it to continue using the system." + msgid "Manage personal access tokens" msgstr "Manage personal access tokens" diff --git a/src/layout/FormFields.component.js b/src/layout/FormFields.component.js index 10254669..d291b918 100644 --- a/src/layout/FormFields.component.js +++ b/src/layout/FormFields.component.js @@ -406,6 +406,9 @@ class FormFields extends Component { emailUpdated={ this.props?.valueStore?.state?.emailUpdated } + userEmail={ + this.props?.valueStore?.state?.email ?? '' + } /> )} {this.renderFields(this.props.fieldKeys)} diff --git a/src/layout/ModalField.component.js b/src/layout/ModalField.component.js index 39b94c64..88dfb967 100644 --- a/src/layout/ModalField.component.js +++ b/src/layout/ModalField.component.js @@ -165,7 +165,7 @@ export function ModalField({ - {i18n.t( - 'Your email is not verified. Please verify your email to continue using the system.' - )} + {userEmail?.trim() !== '' + ? i18n.t( + 'Your email is not verified. Please verify your email to continue using the system.' + ) + : i18n.t( + 'Please provide an email and verify it to continue using the system.' + )}
) @@ -38,4 +42,5 @@ VerifyEmailWarning.propTypes = { }), }).isRequired, emailUpdated: PropTypes.bool, + userEmail: PropTypes.string, } From 705528cc00256374dd0ac681bc6e9d1102896cc9 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Mon, 13 Jan 2025 16:47:29 +0100 Subject: [PATCH 5/6] feat: disable remove email button if needed --- src/layout/ModalField.component.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layout/ModalField.component.js b/src/layout/ModalField.component.js index 88dfb967..072b53c4 100644 --- a/src/layout/ModalField.component.js +++ b/src/layout/ModalField.component.js @@ -134,13 +134,13 @@ export function ModalField({ {i18n.t('Change email')} From 7c54a407f6ed79cb36a020dccdd63453c3907d49 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Mon, 13 Jan 2025 18:33:14 +0100 Subject: [PATCH 6/6] feat: implement PR feedback --- i18n/en.pot | 94 +++---- src/layout/EmailField.component.js | 261 ++++++++++++++++++ ...le.css => EmailField.component.module.css} | 0 src/layout/FormFields.component.js | 25 +- src/layout/ModalField.component.js | 245 ---------------- src/userSettingsMapping.js | 2 +- 6 files changed, 323 insertions(+), 304 deletions(-) create mode 100644 src/layout/EmailField.component.js rename src/layout/{ModalField.component.module.css => EmailField.component.module.css} (100%) delete mode 100644 src/layout/ModalField.component.js diff --git a/i18n/en.pot b/i18n/en.pot index 13db6083..598cf9dc 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-01-13T15:15:24.364Z\n" -"PO-Revision-Date: 2025-01-13T15:15:24.364Z\n" +"POT-Creation-Date: 2025-01-13T17:35:56.021Z\n" +"PO-Revision-Date: 2025-01-13T17:35:56.021Z\n" msgid "Never" msgstr "Never" @@ -307,45 +307,6 @@ msgstr "Select profile picture" msgid "Remove profile picture" msgstr "Remove profile picture" -msgid "This field is required" -msgstr "This field is required" - -msgid "This field should be a URL" -msgstr "This field should be a URL" - -msgid "This field should contain a list of URLs" -msgstr "This field should contain a list of URLs" - -msgid "This field should be a number" -msgstr "This field should be a number" - -msgid "This field should be a positive number" -msgstr "This field should be a positive number" - -msgid "This field should be an email" -msgstr "This field should be an email" - -msgid "Please enter a valid international phone number (+0123456789)" -msgstr "Please enter a valid international phone number (+0123456789)" - -msgid "Yes" -msgstr "Yes" - -msgid "No" -msgstr "No" - -msgid "No value" -msgstr "No value" - -msgid "Use system default" -msgstr "Use system default" - -msgid "No options" -msgstr "No options" - -msgid "System default" -msgstr "System default" - msgid "No email provided" msgstr "No email provided" @@ -367,15 +328,9 @@ msgstr "Are you sure you want to remove your email?" msgid "Cancel" msgstr "Cancel" -msgid "Email" -msgstr "Email" - msgid "Change email" msgstr "Change email" -msgid "There is no email to remove" -msgstr "There is no email to remove" - msgid "If you change your email, you may need to reverify your email." msgstr "If you change your email, you may need to reverify your email." @@ -394,6 +349,51 @@ msgstr "Confirm new email" msgid "Save" msgstr "Save" +msgid "Email" +msgstr "Email" + +msgid "There is no email to remove" +msgstr "There is no email to remove" + +msgid "This field is required" +msgstr "This field is required" + +msgid "This field should be a URL" +msgstr "This field should be a URL" + +msgid "This field should contain a list of URLs" +msgstr "This field should contain a list of URLs" + +msgid "This field should be a number" +msgstr "This field should be a number" + +msgid "This field should be a positive number" +msgstr "This field should be a positive number" + +msgid "This field should be an email" +msgstr "This field should be an email" + +msgid "Please enter a valid international phone number (+0123456789)" +msgstr "Please enter a valid international phone number (+0123456789)" + +msgid "Yes" +msgstr "Yes" + +msgid "No" +msgstr "No" + +msgid "No value" +msgstr "No value" + +msgid "Use system default" +msgstr "Use system default" + +msgid "No options" +msgstr "No options" + +msgid "System default" +msgstr "System default" + msgid "User profile" msgstr "User profile" diff --git a/src/layout/EmailField.component.js b/src/layout/EmailField.component.js new file mode 100644 index 00000000..c44a1926 --- /dev/null +++ b/src/layout/EmailField.component.js @@ -0,0 +1,261 @@ +import i18n from '@dhis2/d2-i18n' +import { + Button, + ButtonStrip, + email as emailValidator, + InputField, + Modal, + ModalActions, + ModalContent, + ModalTitle, + NoticeBox, + Tooltip, +} from '@dhis2/ui' +import TextField from 'd2-ui/lib/form-fields/TextField' +import PropTypes from 'prop-types' +import React, { useMemo, useState } from 'react' +import styles from './EmailField.component.module.css' +import { VerifyEmail } from './VerifyEmail.component.js' + +const TooltipWrapper = ({ disabled, content, children }) => { + if (!disabled) { + return <>{children} + } + return {children} +} + +TooltipWrapper.propTypes = { + children: PropTypes.node, + content: PropTypes.string, + disabled: PropTypes.bool, +} + +const getSaveDisabledContent = ({ newEmail, emailValidationMessage }) => { + if (!newEmail) { + return i18n.t('No email provided') + } + if (emailValidationMessage) { + return i18n.t('Email is invalid') + } + return i18n.t('Emails must match') +} + +const RemoveModal = ({ + removeModalOpen, + setRemoveModalOpen, + userEmailVerified, + onUpdate, +}) => ( + setRemoveModalOpen(false)}> + {i18n.t('Remove email')} + + + {userEmailVerified && ( + + )} +
{i18n.t('Are you sure you want to remove your email?')}
+
+ + + + + + + + +
+) + +RemoveModal.propTypes = { + removeModalOpen: PropTypes.bool, + setRemoveModalOpen: PropTypes.func, + userEmailVerified: PropTypes.bool, + onUpdate: PropTypes.func, +} + +const EmailModal = ({ + emailModalOpen, + setEmailModalOpen, + userEmailVerified, + userEmail, + onUpdate, +}) => { + const [newEmail, setNewEmail] = useState() + const [newEmailConfirm, setNewEmailConfirm] = useState() + const [newEmailConfirmTouched, setNewEmailConfirmTouched] = useState(false) + const emailValidationMessage = useMemo( + () => emailValidator(newEmail), + [newEmail] + ) + const emailsMatch = newEmail === newEmailConfirm + const saveDisabled = + !newEmail || Boolean(emailValidationMessage) || !emailsMatch + const saveDisabledContent = getSaveDisabledContent({ + newEmail, + emailValidationMessage, + }) + + return ( + { + setEmailModalOpen(false) + }} + > + {i18n.t('Change email')} + + + {userEmailVerified && ( + + {i18n.t( + 'If you change your email, you may need to reverify your email.' + )} + + )} + + + setNewEmail(newValue.value)} + className={styles.emailModalItem} + /> + + { + setNewEmailConfirmTouched(true) + setNewEmailConfirm(newValue.value) + }} + className={styles.emailModalItem} + /> + + + + + + + + + + + + + ) +} + +EmailModal.propTypes = { + emailModalOpen: PropTypes.bool, + setEmailModalOpen: PropTypes.func, + userEmail: PropTypes.string, + userEmailVerified: PropTypes.bool, + onUpdate: PropTypes.func, +} + +export function EmailField({ userEmail, userEmailVerified, onUpdate }) { + const [emailModalOpen, setEmailModalOpen] = useState() + const [removeModalOpen, setRemoveModalOpen] = useState() + + return ( +
+ +
+ + + + + +
+ {emailModalOpen && ( + + )} + +
+ ) +} + +EmailField.propTypes = { + userEmail: PropTypes.string, + userEmailVerified: PropTypes.bool, + onUpdate: PropTypes.func, +} diff --git a/src/layout/ModalField.component.module.css b/src/layout/EmailField.component.module.css similarity index 100% rename from src/layout/ModalField.component.module.css rename to src/layout/EmailField.component.module.css diff --git a/src/layout/FormFields.component.js b/src/layout/FormFields.component.js index d291b918..8a213dda 100644 --- a/src/layout/FormFields.component.js +++ b/src/layout/FormFields.component.js @@ -15,7 +15,7 @@ import optionValueStore from '../optionValue.store.js' import userSettingsStore from '../settings/userSettings.store.js' import userSettingsKeyMapping from '../userSettingsMapping.js' import AvatarEditor from './AvatarEditor.component.js' -import { ModalField } from './ModalField.component.js' +import { EmailField } from './EmailField.component.js' import AppTheme from './theme.js' import { VerifyEmailWarning } from './VerifyEmailWarning.js' @@ -236,18 +236,16 @@ function createAvatarEditor(fieldBase, d2, valueStore) { }) } -function createModalField({ fieldBase, valueStore, onUpdate, d2 }) { +function createEmailField({ fieldBase, valueStore, onUpdate, d2 }) { return Object.assign({}, fieldBase, { - component: ModalField, + component: EmailField, props: { - onUpdate, + onUpdate: (newEmail) => { + onUpdate('email', newEmail) + onUpdate('emailUpdated', true) + }, userEmail: valueStore.state['email'] || '', userEmailVerified: d2?.currentUser?.emailVerified, - setUserEmail: (email) => { - valueStore.state['email'] = email - valueStore.state['emailUpdated'] = true - valueStore.setState(valueStore.state) - }, }, }) } @@ -302,8 +300,13 @@ function createField({ fieldName, valueStore, d2, onUpdate }) { return createAccountEditor(fieldBase, d2, valueStore) case 'avatar': return createAvatarEditor(fieldBase, d2, valueStore) - case 'modal': - return createModalField({ fieldBase, valueStore, onUpdate, d2 }) + case 'emailModal': + return createEmailField({ + fieldBase, + valueStore, + onUpdate, + d2, + }) default: log.warn( `Unknown control type "${mapping.type}" encountered for field "${fieldName}"` diff --git a/src/layout/ModalField.component.js b/src/layout/ModalField.component.js deleted file mode 100644 index 072b53c4..00000000 --- a/src/layout/ModalField.component.js +++ /dev/null @@ -1,245 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { - Button, - ButtonStrip, - email as emailValidator, - InputField, - Modal, - ModalActions, - ModalContent, - ModalTitle, - NoticeBox, - Tooltip, -} from '@dhis2/ui' -import TextField from 'd2-ui/lib/form-fields/TextField' -import PropTypes from 'prop-types' -import React, { useMemo, useState } from 'react' -import styles from './ModalField.component.module.css' -import { VerifyEmail } from './VerifyEmail.component.js' - -const TooltipWrapper = ({ disabled, content, children }) => { - if (!disabled) { - return <>{children} - } - return {children} -} - -TooltipWrapper.propTypes = { - children: PropTypes.node, - content: PropTypes.string, - disabled: PropTypes.bool, -} - -const getSaveDisabledContent = ({ newEmail, emailValidationMessage }) => { - if (!newEmail) { - return i18n.t('No email provided') - } - if (emailValidationMessage) { - return i18n.t('Email is invalid') - } - return i18n.t('Emails must match') -} - -const RemoveModal = ({ - removeModalOpen, - closeModal, - userEmailVerified, - setUserEmail, - onUpdate, -}) => ( - - {i18n.t('Remove email')} - - - {userEmailVerified && ( - - )} -
{i18n.t('Are you sure you want to remove your email?')}
-
- - - - - - - - -
-) - -RemoveModal.propTypes = { - closeModal: PropTypes.func, - removeModalOpen: PropTypes.bool, - setUserEmail: PropTypes.bool, - userEmailVerified: PropTypes.bool, - onUpdate: PropTypes.func, -} - -export function ModalField({ - userEmail, - userEmailVerified, - setUserEmail, - onUpdate, -}) { - const [modalOpen, setModalOpen] = useState() - const [removeModalOpen, setRemoveModalOpen] = useState() - const [newEmail, setNewEmail] = useState() - const [newEmailConfirm, setNewEmailConfirm] = useState() - const [newEmailConfirmTouched, setNewEmailConfirmTouched] = useState(false) - const emailValidationMessage = useMemo( - () => emailValidator(newEmail), - [newEmail] - ) - const emailsMatch = newEmail === newEmailConfirm - const saveDisabled = - !newEmail || Boolean(emailValidationMessage) || !emailsMatch - const saveDisabledContent = getSaveDisabledContent({ - newEmail, - emailValidationMessage, - }) - - const closeModal = () => { - setModalOpen(false) - setRemoveModalOpen(false) - setNewEmail() - setNewEmailConfirm() - setNewEmailConfirmTouched(false) - } - return ( -
- -
- - - - - -
- - {i18n.t('Change email')} - - - {userEmailVerified && ( - - {i18n.t( - 'If you change your email, you may need to reverify your email.' - )} - - )} - - - setNewEmail(newValue.value)} - className={styles.emailModalItem} - /> - - { - setNewEmailConfirmTouched(true) - setNewEmailConfirm(newValue.value) - }} - className={styles.emailModalItem} - /> - - - - - - - - - - - - - -
- ) -} - -ModalField.propTypes = { - setUserEmail: PropTypes.func, - userEmail: PropTypes.string, - userEmailVerified: PropTypes.bool, - onUpdate: PropTypes.func, -} diff --git a/src/userSettingsMapping.js b/src/userSettingsMapping.js index 272c47a8..562cabfa 100644 --- a/src/userSettingsMapping.js +++ b/src/userSettingsMapping.js @@ -29,7 +29,7 @@ const settingsKeyMapping = { }, email: { label: i18n.t('E-mail'), - type: 'modal', + type: 'emailModal', }, phoneNumber: { label: i18n.t('Mobile phone number'),