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:
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
Reference in New Issue
Block a user