feat: Update "Change Email" and "Change Password" modal designs (#714)

* feat: Add new utility classes

* feat: Update "Change Email" modal design

* feat: Use .sk-input.contrast className to mimic password wizard inputs

* feat: Add explicit labels to password wizard inputs

* feat: Make button sizing consistent

* refactor: Remove unused dependencies

* refactor: Remove unused component
This commit is contained in:
Aman Harwara
2021-11-01 19:49:06 +05:30
committed by GitHub
parent 5da51048bf
commit 4f56c453cb
8 changed files with 86 additions and 308 deletions

View File

@@ -5,7 +5,6 @@ import {
AlertDialogLabel, AlertDialogLabel,
} from '@node_modules/@reach/alert-dialog'; } from '@node_modules/@reach/alert-dialog';
import { useRef } from '@node_modules/preact/hooks'; import { useRef } from '@node_modules/preact/hooks';
import { IconButton } from '@/components/IconButton';
export const ModalDialog: FunctionComponent = ({ children }) => { export const ModalDialog: FunctionComponent = ({ children }) => {
const ldRef = useRef<HTMLButtonElement>(null); const ldRef = useRef<HTMLButtonElement>(null);
@@ -19,7 +18,7 @@ export const ModalDialog: FunctionComponent = ({ children }) => {
<div tabIndex={-1} className="sn-component"> <div tabIndex={-1} className="sn-component">
<div <div
tabIndex={0} tabIndex={0}
className="w-160 bg-default rounded shadow-overlay focus:padded-ring-info" className="sk-panel w-160 bg-default rounded shadow-overlay focus:padded-ring-info"
> >
{children} {children}
</div> </div>
@@ -30,17 +29,20 @@ export const ModalDialog: FunctionComponent = ({ children }) => {
export const ModalDialogLabel: FunctionComponent<{ export const ModalDialogLabel: FunctionComponent<{
closeDialog: () => void; closeDialog: () => void;
}> = ({ children, closeDialog }) => ( className?: string;
<AlertDialogLabel className=""> }> = ({ children, closeDialog, className }) => (
<div className="px-4 py-4 flex flex-row items-center"> <AlertDialogLabel className={className}>
<div className="flex-grow color-text text-lg font-bold">{children}</div> <div className="w-full flex flex-row justify-between items-center">
<IconButton <div className="flex-grow color-text text-base font-medium">
focusable={true} {children}
title="Close" </div>
className="color-neutral h-5 w-5" <div
icon="close" tabIndex={0}
onClick={() => closeDialog()} className="font-bold color-info cursor-pointer"
/> onClick={closeDialog}
>
Close
</div>
</div> </div>
<hr className="h-1px bg-border no-border m-0" /> <hr className="h-1px bg-border no-border m-0" />
</AlertDialogLabel> </AlertDialogLabel>
@@ -55,17 +57,20 @@ export const ModalDialogDescription: FunctionComponent<{ className?: string }> =
</AlertDialogDescription> </AlertDialogDescription>
); );
export const ModalDialogButtons: FunctionComponent = ({ children }) => ( export const ModalDialogButtons: FunctionComponent<{ className?: string }> = ({
children,
className,
}) => (
<> <>
<hr className="h-1px bg-border no-border m-0" /> <hr className="h-1px bg-border no-border m-0" />
<div className="px-4 py-4 flex flex-row justify-end items-center"> <div className={`px-4 py-4 flex flex-row items-center ${className}`}>
{children != undefined && Array.isArray(children) {children != undefined && Array.isArray(children)
? children.map((child, idx, arr) => ( ? children.map((child, idx, arr) => (
<> <>
{child} {child}
{idx < arr.length - 1 ? <div className="min-w-3" /> : undefined} {idx < arr.length - 1 ? <div className="min-w-3" /> : undefined}
</> </>
)) ))
: children} : children}
</div> </div>
</> </>

View File

@@ -1,37 +1,47 @@
import { DecoratedInput } from '@/components/DecoratedInput';
import { StateUpdater } from 'preact/hooks'; import { StateUpdater } from 'preact/hooks';
import { FunctionalComponent } from 'preact'; import { FunctionalComponent } from 'preact';
import { HtmlInputTypes } from '@/enums';
type Props = { type Props = {
setNewEmail: StateUpdater<string> setNewEmail: StateUpdater<string>;
setCurrentPassword: StateUpdater<string> setCurrentPassword: StateUpdater<string>;
} };
const labelClassName = `block mb-1`;
const inputClassName = 'sk-input contrast';
export const ChangeEmailForm: FunctionalComponent<Props> = ({ export const ChangeEmailForm: FunctionalComponent<Props> = ({
setNewEmail, setNewEmail,
setCurrentPassword setCurrentPassword,
}) => { }) => {
return ( return (
( <div className="w-full flex flex-col">
<> <div className="mt-2 mb-3">
<div className={'mt-2 mb-3'}> <label className={labelClassName} htmlFor="change-email-email-input">
<DecoratedInput New Email:
onChange={(newEmail) => { </label>
setNewEmail(newEmail); <input
}} id="change-email-email-input"
placeholder={'New Email'} className={inputClassName}
/> type="email"
</div> onChange={({ target }) => {
<div className={'mt-2 mb-3'}> setNewEmail((target as HTMLInputElement).value);
<DecoratedInput }}
type={HtmlInputTypes.Password} />
onChange={(currentPassword) => { </div>
setCurrentPassword(currentPassword); <div className="mb-2">
}} <label className={labelClassName} htmlFor="change-email-password-input">
placeholder={'Current Password'} Current Password:
/> </label>
</div> <input
</> id="change-email-password-input"
) className={inputClassName}
type="password"
onChange={({ target }) => {
setCurrentPassword((target as HTMLInputElement).value);
}}
/>
</div>
</div>
); );
}; };

View File

@@ -154,10 +154,13 @@ export const ChangeEmail: FunctionalComponent<Props> = ({
return ( return (
<div> <div>
<ModalDialog> <ModalDialog>
<ModalDialogLabel closeDialog={handleDialogClose}> <ModalDialogLabel
closeDialog={handleDialogClose}
className="sk-panel-header px-4.5"
>
Change Email Change Email
</ModalDialogLabel> </ModalDialogLabel>
<ModalDialogDescription> <ModalDialogDescription className="px-4.5">
{currentStep === Steps.InitialStep && ( {currentStep === Steps.InitialStep && (
<ChangeEmailForm <ChangeEmailForm
setNewEmail={setNewEmail} setNewEmail={setNewEmail}
@@ -166,15 +169,7 @@ export const ChangeEmail: FunctionalComponent<Props> = ({
)} )}
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />} {currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
</ModalDialogDescription> </ModalDialogDescription>
<ModalDialogButtons> <ModalDialogButtons className="px-4.5">
{currentStep === Steps.InitialStep && (
<Button
className="min-w-20"
type="normal"
label="Cancel"
onClick={handleDialogClose}
/>
)}
<Button <Button
className="min-w-20" className="min-w-20"
type="primary" type="primary"

View File

@@ -1,45 +0,0 @@
import { DecoratedInput } from '@/components/DecoratedInput';
import { StateUpdater } from 'preact/hooks';
import { FunctionalComponent } from 'preact';
import { HtmlInputTypes } from '@/enums';
type Props = {
setCurrentPassword: StateUpdater<string>
setNewPassword: StateUpdater<string>
setNewPasswordConfirmation: StateUpdater<string>
}
export const ChangePasswordForm: FunctionalComponent<Props> = ({
setCurrentPassword,
setNewPassword,
setNewPasswordConfirmation
}) => {
return (
(
<>
<div className={'mt-2 mb-3'}>
<DecoratedInput
type={HtmlInputTypes.Password}
onChange={(currentPassword) => {
setCurrentPassword(currentPassword);
}}
placeholder={'Current Password'}
/>
</div>
<div className={'mt-2 mb-3'}>
<DecoratedInput
type={HtmlInputTypes.Password}
placeholder={'New Password'}
onChange={newPassword => setNewPassword(newPassword)}
/>
</div>
<div className={'mt-2 mb-3'}>
<DecoratedInput
type={HtmlInputTypes.Password}
placeholder={'Confirm New Password'}
onChange={newPasswordConfirmation => setNewPasswordConfirmation(newPasswordConfirmation)}
/>
</div>
</>
)
);
};

View File

@@ -1,12 +0,0 @@
import { FunctionalComponent } from 'preact';
export const ChangePasswordSuccess: FunctionalComponent = () => {
return (
<>
<div className={'sk-label sk-bold info'}>Your password has been successfully changed.</div>
<p className={'sk-p'}>
Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.
</p>
</>
);
};

View File

@@ -1,189 +0,0 @@
import { useState } from '@node_modules/preact/hooks';
import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel,
} from '@/components/shared/ModalDialog';
import { Button } from '@/components/Button';
import { FunctionalComponent } from 'preact';
import { WebApplication } from '@/ui_models/application';
import { ChangePasswordSuccess } from '@/preferences/panes/account/changePassword/ChangePasswordSuccess';
import { ChangePasswordForm } from '@/preferences/panes/account/changePassword/ChangePasswordForm';
import { useBeforeUnload } from '@/hooks/useBeforeUnload';
enum SubmitButtonTitles {
Default = 'Continue',
GeneratingKeys = 'Generating Keys...',
Finish = 'Finish',
}
enum Steps {
InitialStep,
FinishStep,
}
type Props = {
onCloseDialog: () => void;
application: WebApplication;
};
export const ChangePassword: FunctionalComponent<Props> = ({
onCloseDialog,
application,
}) => {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('');
const [isContinuing, setIsContinuing] = useState(false);
const [lockContinue, setLockContinue] = useState(false);
const [submitButtonTitle, setSubmitButtonTitle] = useState(
SubmitButtonTitles.Default
);
const [currentStep, setCurrentStep] = useState(Steps.InitialStep);
useBeforeUnload();
const applicationAlertService = application.alertService;
const validateCurrentPassword = async () => {
if (!currentPassword || currentPassword.length === 0) {
applicationAlertService.alert('Please enter your current password.');
return false;
}
if (!newPassword || newPassword.length === 0) {
applicationAlertService.alert('Please enter a new password.');
return false;
}
if (newPassword !== newPasswordConfirmation) {
applicationAlertService.alert(
'Your new password does not match its confirmation.'
);
return false;
}
if (!application.getUser()?.email) {
applicationAlertService.alert(
"We don't have your email stored. Please sign out then sign back in to fix this issue."
);
return false;
}
/** Validate current password */
const success = await application.validateAccountPassword(currentPassword);
if (!success) {
applicationAlertService.alert(
'The current password you entered is not correct. Please try again.'
);
}
return success;
};
const resetProgressState = () => {
setSubmitButtonTitle(SubmitButtonTitles.Default);
setIsContinuing(false);
};
const processPasswordChange = async () => {
await application.downloadBackup();
setLockContinue(true);
const response = await application.changePassword(
currentPassword,
newPassword
);
const success = !response.error;
setLockContinue(false);
return success;
};
const dismiss = () => {
if (lockContinue) {
applicationAlertService.alert(
'Cannot close window until pending tasks are complete.'
);
} else {
onCloseDialog();
}
};
const handleSubmit = async () => {
if (lockContinue || isContinuing) {
return;
}
if (currentStep === Steps.FinishStep) {
dismiss();
return;
}
setIsContinuing(true);
setSubmitButtonTitle(SubmitButtonTitles.GeneratingKeys);
const valid = await validateCurrentPassword();
if (!valid) {
resetProgressState();
return;
}
const success = await processPasswordChange();
if (!success) {
resetProgressState();
return;
}
setIsContinuing(false);
setSubmitButtonTitle(SubmitButtonTitles.Finish);
setCurrentStep(Steps.FinishStep);
};
const handleDialogClose = () => {
if (lockContinue) {
applicationAlertService.alert(
'Cannot close window until pending tasks are complete.'
);
} else {
onCloseDialog();
}
};
return (
<div>
<ModalDialog>
<ModalDialogLabel closeDialog={handleDialogClose}>
Change Password
</ModalDialogLabel>
<ModalDialogDescription>
{currentStep === Steps.InitialStep && (
<ChangePasswordForm
setCurrentPassword={setCurrentPassword}
setNewPassword={setNewPassword}
setNewPasswordConfirmation={setNewPasswordConfirmation}
/>
)}
{currentStep === Steps.FinishStep && <ChangePasswordSuccess />}
</ModalDialogDescription>
<ModalDialogButtons>
{currentStep === Steps.InitialStep && (
<Button
className="min-w-20"
type="normal"
label="Cancel"
onClick={handleDialogClose}
/>
)}
<Button
className="min-w-20"
type="primary"
label={submitButtonTitle}
onClick={handleSubmit}
/>
</ModalDialogButtons>
</ModalDialog>
</div>
);
};

View File

@@ -427,6 +427,12 @@
padding-right: 0; padding-right: 0;
} }
.sn-component .px-4\.5,
.sn-component .sk-panel .px-4\.5 {
padding-left: 1.375rem;
padding-right: 1.375rem;
}
.px-9 { .px-9 {
padding-left: 2.25rem; padding-left: 2.25rem;
padding-right: 2.25rem; padding-right: 2.25rem;
@@ -648,3 +654,7 @@
.list-style-none { .list-style-none {
list-style-type: none; list-style-type: none;
} }
.rounded-0\.5 {
border-radius: 0.125rem;
}

View File

@@ -12,24 +12,28 @@
.sk-panel-row .sk-panel-row
.sk-panel-column.stretch .sk-panel-column.stretch
form.sk-panel-form form.sk-panel-form
label.block.mb-1(for='password-wiz-current-password') Current Password:
input.sk-input.contrast( input.sk-input.contrast(
id='password-wiz-current-password'
ng-model='ctrl.state.formData.currentPassword', ng-model='ctrl.state.formData.currentPassword',
placeholder='Current Password',
should-focus='true', should-focus='true',
sn-autofocus='true', sn-autofocus='true',
type='password' type='password'
) )
.sk-panel-row .sk-panel-row
label.block.mb-1(for='password-wiz-new-password') New Password:
input.sk-input.contrast( input.sk-input.contrast(
id='password-wiz-new-password',
ng-if='ctrl.props.changePassword', ng-if='ctrl.props.changePassword',
ng-model='ctrl.state.formData.newPassword', ng-model='ctrl.state.formData.newPassword',
placeholder='New Password',
type='password' type='password'
) )
.sk-panel-row
label.block.mb-1(for='password-wiz-confirm-new-password') Confirm New Password:
input.sk-input.contrast( input.sk-input.contrast(
id='password-wiz-confirm-new-password',
ng-if='ctrl.props.changePassword', ng-if='ctrl.props.changePassword',
ng-model='ctrl.state.formData.newPasswordConfirmation', ng-model='ctrl.state.formData.newPasswordConfirmation',
placeholder='Confirm New Password',
type='password' type='password'
) )
.sk-panel-section(ng-if='ctrl.state.step == 2') .sk-panel-section(ng-if='ctrl.state.step == 2')
@@ -41,7 +45,7 @@
| Please ensure you are running the latest version of Standard Notes | Please ensure you are running the latest version of Standard Notes
| on all platforms to ensure maximum compatibility. | on all platforms to ensure maximum compatibility.
.sk-panel-footer .sk-panel-footer
button.sn-button.small.info( button.sn-button.min-w-20.info(
ng-click='ctrl.nextStep()', ng-click='ctrl.nextStep()',
ng-disabled='ctrl.state.lockContinue' ng-disabled='ctrl.state.lockContinue'
) {{ctrl.state.continueTitle}} ) {{ctrl.state.continueTitle}}