feat: New account menu and text input with icon & toggle (#665)
* feat: Add new icons * Revert "feat: Add new icons" This reverts commit 0acb403fe846dbb2e48fd22de35c3568c3cb4453. * feat: Add new icons for account menu * feat: Add new Icons * feat: Add "currentPane" state to prefs view * feat: Update account menu to new design * feat: Add input component with icon & toggle * fix: sync icon & function * fix: Fix eye icon * feat: Create re-usable checkbox feat: Add "merge local" option * feat: Allow using className on IconButton * feat: Add disabled state on input feat: Make toggle circle * refactor: Move checkbox to components * feat: Handle invalid email/password error * feat: Implement new design for Create Account * feat: Implement new account menu design * feat: Add disabled option to IconButton * feat: Set account menu pane from other component * feat: Add 2fa account menu pane feat: Add lock icon * feat: Remove unnecessary 2FA menu pane feat: Reset current menu pane on clickOutside * feat: Change "Log in" to "Sign in" * feat: Remove sync from footer * feat: Change "Login" to "Sign in" feat: Add spinner to "Syncing..." refactor: Use then-catch-finally for sync * feat: Use common enableCustomServer state * feat: Animate account menu closing * fix: Reset menu pane only after it's closed * feat: Add keyDown handler to InputWithIcon * feat: Handle Enter press in inputs * Update app/assets/javascripts/components/InputWithIcon.tsx Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> * Update app/assets/javascripts/components/InputWithIcon.tsx Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> * refactor: Use server state from AccountMenuState * Update app/assets/javascripts/components/AccountMenu/CreateAccount.tsx Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> * Update app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> * feat: Use common AdvancedOptions * feat: Add "eye-off" icon and toggle state * feat: Allow undefined values * refactor: Remove enableCustomServer state * feat: Persist server option state * feat: Add bottom-100 and cursor-auto util classes refactor: Use bottom-100 and cursor-auto classes * refactor: Invert ternary operator * refactor: Remove unused imports * refactor: Use toggled as prop instead of state * refactor: Change "Log in/out" to "Sign in/out" * refactor: Change "Login" to "Sign in" * refactor: Remove hardcoded width/height * refactor: Use success class * feat: Remove hardcoded width & height from svg * fix: Fix chevron-down icon Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com> Co-authored-by: Antonella Sgarlatta <antonella@standardnotes.org>
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
import { RoundIconButton } from '@/components/RoundIconButton';
|
||||
import { TitleBar, Title } from '@/components/TitleBar';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { AccountPreferences, HelpAndFeedback, Listed, General, Security } from './panes';
|
||||
import {
|
||||
AccountPreferences,
|
||||
HelpAndFeedback,
|
||||
Listed,
|
||||
General,
|
||||
Security,
|
||||
} from './panes';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PreferencesMenu } from './PreferencesMenu';
|
||||
import { PreferencesMenuView } from './PreferencesMenuView';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { MfaProps } from './panes/two-factor-auth/MfaProps';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useEffect, useMemo } from 'preact/hooks';
|
||||
import { Extensions } from './panes/Extensions';
|
||||
|
||||
interface PreferencesProps extends MfaProps {
|
||||
@@ -22,7 +28,9 @@ const PaneSelector: FunctionComponent<
|
||||
> = observer((props) => {
|
||||
switch (props.menu.selectedPaneId) {
|
||||
case 'general':
|
||||
return <General appState={props.appState} application={props.application} />
|
||||
return (
|
||||
<General appState={props.appState} application={props.application} />
|
||||
);
|
||||
case 'account':
|
||||
return (
|
||||
<AccountPreferences
|
||||
@@ -67,20 +75,22 @@ const PreferencesCanvas: FunctionComponent<
|
||||
|
||||
export const PreferencesView: FunctionComponent<PreferencesProps> = observer(
|
||||
(props) => {
|
||||
const menu = useMemo(() => new PreferencesMenu(), []);
|
||||
|
||||
useEffect(() => {
|
||||
menu.selectPane(props.appState.preferences.currentPane);
|
||||
const removeEscKeyObserver = props.application.io.addKeyObserver({
|
||||
key: 'Escape',
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault();
|
||||
props.closePreferences();
|
||||
}
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
removeEscKeyObserver();
|
||||
};
|
||||
}, [props]);
|
||||
const menu = new PreferencesMenu();
|
||||
}, [props, menu]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full absolute top-left-0 flex flex-col bg-contrast z-index-preferences">
|
||||
<TitleBar className="items-center justify-between">
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
Sync,
|
||||
SubscriptionWrapper,
|
||||
Credentials,
|
||||
LogOutWrapper,
|
||||
SignOutWrapper,
|
||||
Authentication,
|
||||
} from '@/preferences/panes/account';
|
||||
import { PreferencesPane } from '@/preferences/components';
|
||||
@@ -23,7 +23,7 @@ export const AccountPreferences = observer(
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Authentication application={application} appState={appState} />
|
||||
<LogOutWrapper application={application} appState={appState} />
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export const AccountPreferences = observer(
|
||||
<Credentials application={application} />
|
||||
<Sync application={application} />
|
||||
<SubscriptionWrapper application={application} />
|
||||
<LogOutWrapper application={application} appState={appState} />
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,65 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { PreferencesGroup, PreferencesSegment, Subtitle, Text, Title } from "@/preferences/components";
|
||||
import { WebApplication } from "@/ui_models/application";
|
||||
import { AppState } from "@/ui_models/app_state";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { FunctionComponent } from "preact";
|
||||
import { AccountMenuPane } from '@/components/AccountMenu';
|
||||
import { Button } from '@/components/Button';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Subtitle,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const Authentication: FunctionComponent<{ application: WebApplication, appState: AppState }> =
|
||||
observer(({ appState }) => {
|
||||
export const Authentication: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ appState }) => {
|
||||
const clickSignIn = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.SignIn);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
const clickSignIn = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setShowLogin(true);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
const clickRegister = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.Register);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
const clickRegister = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setShowRegister(true);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-col items-center px-12">
|
||||
<Title>You're not signed in</Title>
|
||||
<Subtitle className="text-center">Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.</Subtitle>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row w-full">
|
||||
<Button type="primary" onClick={clickSignIn} label="Sign in" className="flex-grow" />
|
||||
<div className="min-w-3" />
|
||||
<Button type="primary" onClick={clickRegister} label="Register" className="flex-grow" />
|
||||
</div>
|
||||
<div className="min-h-3" />
|
||||
<Text className="text-center">Standard Notes is free on every platform, and comes standard with sync and encryption.</Text>
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-col items-center px-12">
|
||||
<Title>You're not signed in</Title>
|
||||
<Subtitle className="text-center">
|
||||
Sign in to sync your notes and preferences across all your devices
|
||||
and enable end-to-end encryption.
|
||||
</Subtitle>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row w-full">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={clickSignIn}
|
||||
label="Sign in"
|
||||
className="flex-grow"
|
||||
/>
|
||||
<div className="min-w-3" />
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={clickRegister}
|
||||
label="Register"
|
||||
className="flex-grow"
|
||||
/>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
<div className="min-h-3" />
|
||||
<Text className="text-center">
|
||||
Standard Notes is free on every platform, and comes standard with
|
||||
sync and encryption.
|
||||
</Text>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
|
||||
import { OtherSessionsLogoutContainer } from '@/components/OtherSessionsLogout';
|
||||
import { OtherSessionsSignOutContainer } from '@/components/OtherSessionsSignOut';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
@@ -13,30 +13,33 @@ import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
const LogOutView: FunctionComponent<{
|
||||
const SignOutView: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Log out</Title>
|
||||
<Title>Sign out</Title>
|
||||
<div className="min-h-2" />
|
||||
<Subtitle>Other devices</Subtitle>
|
||||
<Text>Want to log out on all devices except this one?</Text>
|
||||
<Text>Want to sign out on all devices except this one?</Text>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
className="mr-3"
|
||||
type="normal"
|
||||
label="Log out other sessions"
|
||||
label="Sign out other sessions"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setOtherSessionsLogout(true);
|
||||
appState.accountMenu.setOtherSessionsSignOut(true);
|
||||
}}
|
||||
/>
|
||||
<Button type="normal" label="Manage sessions" onClick={() => appState.openSessionsModal()} />
|
||||
<Button
|
||||
type="normal"
|
||||
label="Manage sessions"
|
||||
onClick={() => appState.openSessionsModal()}
|
||||
/>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
@@ -45,20 +48,19 @@ const LogOutView: FunctionComponent<{
|
||||
<div className="min-h-3" />
|
||||
<Button
|
||||
type="danger"
|
||||
label="Log out and clear local data"
|
||||
label="Sign out and clear local data"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<OtherSessionsLogoutContainer appState={appState} application={application} />
|
||||
|
||||
<ConfirmSignoutContainer
|
||||
<OtherSessionsSignOutContainer
|
||||
appState={appState}
|
||||
application={application}
|
||||
/>
|
||||
|
||||
<ConfirmSignoutContainer appState={appState} application={application} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -85,19 +87,19 @@ const ClearSessionDataView: FunctionComponent<{
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
<ConfirmSignoutContainer
|
||||
appState={appState}
|
||||
application={application}
|
||||
/>
|
||||
|
||||
</>);
|
||||
<ConfirmSignoutContainer appState={appState} application={application} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const LogOutWrapper: FunctionComponent<{
|
||||
export const SignOutWrapper: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
const isLoggedIn = application.getUser() != undefined;
|
||||
if (!isLoggedIn) return <ClearSessionDataView appState={appState} application={application} />;
|
||||
return <LogOutView appState={appState} application={application} />;
|
||||
if (!isLoggedIn)
|
||||
return (
|
||||
<ClearSessionDataView appState={appState} application={application} />
|
||||
);
|
||||
return <SignOutView appState={appState} application={application} />;
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/preferences/components';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { SyncQueueStrategy } from '@node_modules/@standardnotes/snjs';
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||
@@ -12,48 +17,54 @@ type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const Sync: FunctionComponent<Props> = observer(({ application }: Props) => {
|
||||
const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate);
|
||||
};
|
||||
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate);
|
||||
};
|
||||
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||
export const Sync: FunctionComponent<Props> = observer(
|
||||
({ application }: Props) => {
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
|
||||
const response = await application.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true
|
||||
});
|
||||
setIsSyncingInProgress(false);
|
||||
if (response && response.error) {
|
||||
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
setLastSyncDate(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||
}
|
||||
};
|
||||
const response = await application.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
});
|
||||
setIsSyncingInProgress(false);
|
||||
if (response && response.error) {
|
||||
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
setLastSyncDate(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className='flex flex-row items-center'>
|
||||
<div className='flex-grow flex flex-col'>
|
||||
<Title>Sync</Title>
|
||||
<Text>
|
||||
Last synced <span className='font-bold'>on {lastSyncDate}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className='min-w-20 mt-3'
|
||||
type='normal'
|
||||
label='Sync now'
|
||||
disabled={isSyncingInProgress}
|
||||
onClick={doSynchronization}
|
||||
/>
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Title>Sync</Title>
|
||||
<Text>
|
||||
Last synced <span className="font-bold">on {lastSyncDate}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className="min-w-20 mt-3"
|
||||
type="normal"
|
||||
label="Sync now"
|
||||
disabled={isSyncingInProgress}
|
||||
onClick={doSynchronization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ModalDialog,
|
||||
ModalDialogButtons,
|
||||
ModalDialogDescription,
|
||||
ModalDialogLabel
|
||||
ModalDialogLabel,
|
||||
} from '@/components/shared/ModalDialog';
|
||||
import { Button } from '@/components/Button';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
@@ -15,29 +15,31 @@ import { useBeforeUnload } from '@/hooks/useBeforeUnload';
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Continue',
|
||||
GeneratingKeys = 'Generating Keys...',
|
||||
Finish = 'Finish'
|
||||
Finish = 'Finish',
|
||||
}
|
||||
|
||||
enum Steps {
|
||||
InitialStep,
|
||||
FinishStep
|
||||
FinishStep,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onCloseDialog: () => void;
|
||||
application: WebApplication;
|
||||
}
|
||||
};
|
||||
|
||||
export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
onCloseDialog,
|
||||
application
|
||||
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 [submitButtonTitle, setSubmitButtonTitle] = useState(
|
||||
SubmitButtonTitles.Default
|
||||
);
|
||||
const [currentStep, setCurrentStep] = useState(Steps.InitialStep);
|
||||
|
||||
useBeforeUnload();
|
||||
@@ -46,16 +48,12 @@ export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
|
||||
const validateCurrentPassword = async () => {
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
applicationAlertService.alert(
|
||||
'Please enter your current password.'
|
||||
);
|
||||
applicationAlertService.alert('Please enter your current password.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!newPassword || newPassword.length === 0) {
|
||||
applicationAlertService.alert(
|
||||
'Please enter a new password.'
|
||||
);
|
||||
applicationAlertService.alert('Please enter a new password.');
|
||||
return false;
|
||||
}
|
||||
if (newPassword !== newPasswordConfirmation) {
|
||||
@@ -67,7 +65,7 @@ export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
|
||||
if (!application.getUser()?.email) {
|
||||
applicationAlertService.alert(
|
||||
'We don\'t have your email stored. Please log out then log back in to fix this issue.'
|
||||
"We don't have your email stored. Please sign out then sign back in to fix this issue."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -172,15 +170,15 @@ export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
<ModalDialogButtons>
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<Button
|
||||
className='min-w-20'
|
||||
type='normal'
|
||||
label='Cancel'
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Cancel"
|
||||
onClick={handleDialogClose}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className='min-w-20'
|
||||
type='primary'
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label={submitButtonTitle}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
|
||||
export { Sync } from './Sync';
|
||||
export { Credentials } from './Credentials';
|
||||
export { LogOutWrapper } from './LogOutView';
|
||||
export { SignOutWrapper } from './SignOutView';
|
||||
export { Authentication } from './Authentication';
|
||||
|
||||
Reference in New Issue
Block a user