diff --git a/app/assets/icons/ic-accessibility.svg b/app/assets/icons/ic-accessibility.svg index de1e6249e..ffb780043 100644 --- a/app/assets/icons/ic-accessibility.svg +++ b/app/assets/icons/ic-accessibility.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-account-circle.svg b/app/assets/icons/ic-account-circle.svg new file mode 100644 index 000000000..53abd5a94 --- /dev/null +++ b/app/assets/icons/ic-account-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-arrow-left.svg b/app/assets/icons/ic-arrow-left.svg new file mode 100644 index 000000000..f80337cff --- /dev/null +++ b/app/assets/icons/ic-arrow-left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-authenticator.svg b/app/assets/icons/ic-authenticator.svg new file mode 100644 index 000000000..9a1193919 --- /dev/null +++ b/app/assets/icons/ic-authenticator.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-check-bold.svg b/app/assets/icons/ic-check-bold.svg new file mode 100644 index 000000000..01df89c32 --- /dev/null +++ b/app/assets/icons/ic-check-bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-check-circle.svg b/app/assets/icons/ic-check-circle.svg new file mode 100644 index 000000000..ddb5737f7 --- /dev/null +++ b/app/assets/icons/ic-check-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-check.svg b/app/assets/icons/ic-check.svg new file mode 100644 index 000000000..ff64da1ba --- /dev/null +++ b/app/assets/icons/ic-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-chevron-down.svg b/app/assets/icons/ic-chevron-down.svg new file mode 100644 index 000000000..1c89552e6 --- /dev/null +++ b/app/assets/icons/ic-chevron-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-cloud-off.svg b/app/assets/icons/ic-cloud-off.svg new file mode 100644 index 000000000..49015714d --- /dev/null +++ b/app/assets/icons/ic-cloud-off.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-code.svg b/app/assets/icons/ic-code.svg new file mode 100644 index 000000000..4a871e270 --- /dev/null +++ b/app/assets/icons/ic-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-copy.svg b/app/assets/icons/ic-copy.svg index 9ad40e8f1..694626a33 100644 --- a/app/assets/icons/ic-copy.svg +++ b/app/assets/icons/ic-copy.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-download.svg b/app/assets/icons/ic-download.svg index de2c70fc2..923b753bd 100644 --- a/app/assets/icons/ic-download.svg +++ b/app/assets/icons/ic-download.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-email.svg b/app/assets/icons/ic-email.svg new file mode 100644 index 000000000..378c18e0c --- /dev/null +++ b/app/assets/icons/ic-email.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-eye-off.svg b/app/assets/icons/ic-eye-off.svg new file mode 100644 index 000000000..76cf09013 --- /dev/null +++ b/app/assets/icons/ic-eye-off.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-eye.svg b/app/assets/icons/ic-eye.svg new file mode 100644 index 000000000..9248599f6 --- /dev/null +++ b/app/assets/icons/ic-eye.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-help.svg b/app/assets/icons/ic-help.svg index c312b7255..eaed4c3f7 100644 --- a/app/assets/icons/ic-help.svg +++ b/app/assets/icons/ic-help.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-info.svg b/app/assets/icons/ic-info.svg index 47ea73219..14107de40 100644 --- a/app/assets/icons/ic-info.svg +++ b/app/assets/icons/ic-info.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-keyboard.svg b/app/assets/icons/ic-keyboard.svg index 9a18af39c..8068326fd 100644 --- a/app/assets/icons/ic-keyboard.svg +++ b/app/assets/icons/ic-keyboard.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-listed.svg b/app/assets/icons/ic-listed.svg index 03e347717..3bac23a5a 100644 --- a/app/assets/icons/ic-listed.svg +++ b/app/assets/icons/ic-listed.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-lock.svg b/app/assets/icons/ic-lock.svg new file mode 100644 index 000000000..946b674fc --- /dev/null +++ b/app/assets/icons/ic-lock.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-markdown.svg b/app/assets/icons/ic-markdown.svg new file mode 100644 index 000000000..bceed54b3 --- /dev/null +++ b/app/assets/icons/ic-markdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-menu-arrow-down.svg b/app/assets/icons/ic-menu-arrow-down.svg new file mode 100644 index 000000000..be1d6a489 --- /dev/null +++ b/app/assets/icons/ic-menu-arrow-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-security.svg b/app/assets/icons/ic-security.svg index badc9d1ad..dfa4b37cc 100644 --- a/app/assets/icons/ic-security.svg +++ b/app/assets/icons/ic-security.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-server.svg b/app/assets/icons/ic-server.svg new file mode 100644 index 000000000..faa3ea19e --- /dev/null +++ b/app/assets/icons/ic-server.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-settings.svg b/app/assets/icons/ic-settings.svg index cc14f94a8..2191bea9a 100644 --- a/app/assets/icons/ic-settings.svg +++ b/app/assets/icons/ic-settings.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-signin.svg b/app/assets/icons/ic-signin.svg new file mode 100644 index 000000000..f211b7c45 --- /dev/null +++ b/app/assets/icons/ic-signin.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-signout.svg b/app/assets/icons/ic-signout.svg new file mode 100644 index 000000000..3b66c862a --- /dev/null +++ b/app/assets/icons/ic-signout.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-spreadsheets.svg b/app/assets/icons/ic-spreadsheets.svg new file mode 100644 index 000000000..70f175be2 --- /dev/null +++ b/app/assets/icons/ic-spreadsheets.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-star.svg b/app/assets/icons/ic-star.svg index 638dae331..f74b0d567 100644 --- a/app/assets/icons/ic-star.svg +++ b/app/assets/icons/ic-star.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-sync.svg b/app/assets/icons/ic-sync.svg new file mode 100644 index 000000000..b93f0ba9d --- /dev/null +++ b/app/assets/icons/ic-sync.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-tasks.svg b/app/assets/icons/ic-tasks.svg new file mode 100644 index 000000000..0f8ef0587 --- /dev/null +++ b/app/assets/icons/ic-tasks.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-text-paragraph.svg b/app/assets/icons/ic-text-paragraph.svg new file mode 100644 index 000000000..4f43cdc0c --- /dev/null +++ b/app/assets/icons/ic-text-paragraph.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-themes.svg b/app/assets/icons/ic-themes.svg index 88606ca76..33abb061a 100644 --- a/app/assets/icons/ic-themes.svg +++ b/app/assets/icons/ic-themes.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-user.svg b/app/assets/icons/ic-user.svg index 65ac58800..1bdfaf61f 100644 --- a/app/assets/icons/ic-user.svg +++ b/app/assets/icons/ic-user.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/javascripts/@types/qrcode.react.d.ts b/app/assets/javascripts/@types/qrcode.react.d.ts new file mode 100644 index 000000000..f997f01d9 --- /dev/null +++ b/app/assets/javascripts/@types/qrcode.react.d.ts @@ -0,0 +1 @@ +declare module 'qrcode.react'; diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index b493808ad..971f793eb 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -1,8 +1,5 @@ 'use strict'; -declare const __VERSION__: string; -declare const __WEB__: boolean; - import { SNLog } from '@standardnotes/snjs'; import angular from 'angular'; import { configRoutes } from './routes'; @@ -66,6 +63,7 @@ import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel'; import { IconDirective } from './components/Icon'; import { NoteTagsContainerDirective } from './components/NoteTagsContainer'; import { PreferencesDirective } from './preferences'; +import { AppVersion, IsWebPlatform } from '@/version'; function reloadHiddenFirefoxTab(): boolean { /** @@ -91,7 +89,8 @@ function reloadHiddenFirefoxTab(): boolean { const startApplication: StartApplication = async function startApplication( defaultSyncServerHost: string, bridge: Bridge, - webSocketUrl: string, + enableUnfinishedFeatures: boolean, + webSocketUrl: string ) { if (reloadHiddenFirefoxTab()) { return; @@ -109,6 +108,7 @@ const startApplication: StartApplication = async function startApplication( .constant('bridge', bridge) .constant('defaultSyncServerHost', defaultSyncServerHost) .constant('appVersion', bridge.appVersion) + .constant('enableUnfinishedFeatures', enableUnfinishedFeatures) .constant('webSocketUrl', webSocketUrl); // Controllers @@ -191,11 +191,12 @@ const startApplication: StartApplication = async function startApplication( }); }; -if (__WEB__) { +if (IsWebPlatform) { startApplication( - (window as any)._default_sync_server, - new BrowserBridge(__VERSION__), - (window as any)._websocket_url, + (window as any)._default_sync_server as string, + new BrowserBridge(AppVersion), + (window as any)._enable_unfinished_features as boolean, + (window as any)._websocket_url as string, ); } else { (window as any).startApplication = startApplication; diff --git a/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx b/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx new file mode 100644 index 000000000..82865ede9 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx @@ -0,0 +1,73 @@ +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useState } from 'preact/hooks'; +import { Checkbox } from '../Checkbox'; +import { Icon } from '../Icon'; +import { InputWithIcon } from '../InputWithIcon'; + +type Props = { + application: WebApplication; + appState: AppState; + disabled?: boolean; +}; + +export const AdvancedOptions: FunctionComponent = observer( + ({ appState, application, disabled = false, children }) => { + const { server, setServer, enableServerOption, setEnableServerOption } = + appState.accountMenu; + const [showAdvanced, setShowAdvanced] = useState(false); + + const handleServerOptionChange = (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setEnableServerOption(e.target.checked); + } + }; + + const handleSyncServerChange = (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setServer(e.target.value); + application.setCustomHost(e.target.value); + } + }; + + const toggleShowAdvanced = () => { + setShowAdvanced(!showAdvanced); + }; + + return ( + <> +
+ Advanced options + +
+ + {showAdvanced ? ( +
+ {children} + + +
+ ) : null} + + ); + } +); diff --git a/app/assets/javascripts/components/AccountMenu/Authentication.tsx b/app/assets/javascripts/components/AccountMenu/Authentication.tsx index a3b42a0b8..3687f0865 100644 --- a/app/assets/javascripts/components/AccountMenu/Authentication.tsx +++ b/app/assets/javascripts/components/AccountMenu/Authentication.tsx @@ -3,7 +3,7 @@ import { STRING_ACCOUNT_MENU_UNCHECK_MERGE, STRING_GENERATING_LOGIN_KEYS, STRING_GENERATING_REGISTER_KEYS, - STRING_NON_MATCHING_PASSWORDS + STRING_NON_MATCHING_PASSWORDS, } from '@/strings'; import { JSXInternal } from 'preact/src/jsx'; import TargetedEvent = JSXInternal.TargetedEvent; @@ -17,13 +17,9 @@ import { AppState } from '@/ui_models/app_state'; type Props = { application: WebApplication; appState: AppState; -} - -const Authentication = observer(({ - application, - appState, - }: Props) => { +}; +const Authentication = observer(({ application, appState }: Props) => { const [showAdvanced, setShowAdvanced] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(false); const [email, setEmail] = useState(''); @@ -39,16 +35,14 @@ const Authentication = observer(({ const { server, notesAndTagsCount, - showLogin, + showSignIn, showRegister, - setShowLogin, + setShowSignIn, setShowRegister, setServer, - closeAccountMenu + closeAccountMenu, } = appState.accountMenu; - const user = application.getUser(); - useEffect(() => { if (isEmailFocused) { emailInputRef.current.focus(); @@ -58,11 +52,11 @@ const Authentication = observer(({ // Reset password and confirmation fields when hiding the form useEffect(() => { - if (!showLogin && !showRegister) { + if (!showSignIn && !showRegister) { setPassword(''); setPasswordConfirmation(''); } - }, [showLogin, showRegister]); + }, [showSignIn, showRegister]); const handleHostInputChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; @@ -75,7 +69,7 @@ const Authentication = observer(({ const passwordConfirmationInputRef = useRef(); const handleSignInClick = () => { - setShowLogin(true); + setShowSignIn(true); setIsEmailFocused(true); }; @@ -90,7 +84,7 @@ const Authentication = observer(({ passwordConfirmationInputRef.current?.blur(); }; - const login = async () => { + const signin = async () => { setStatus(STRING_GENERATING_LOGIN_KEYS); setIsAuthenticating(true); @@ -105,13 +99,13 @@ const Authentication = observer(({ if (!error) { setIsAuthenticating(false); setPassword(''); - setShowLogin(false); + setShowSignIn(false); closeAccountMenu(); return; } - setShowLogin(true); + setShowSignIn(true); setStatus(undefined); setPassword(''); @@ -150,10 +144,11 @@ const Authentication = observer(({ } }; - const handleAuthFormSubmit = (event: - TargetedEvent | - TargetedMouseEvent | - TargetedKeyboardEvent + const handleAuthFormSubmit = ( + event: + | TargetedEvent + | TargetedMouseEvent + | TargetedKeyboardEvent ) => { event.preventDefault(); @@ -163,8 +158,8 @@ const Authentication = observer(({ blurAuthFields(); - if (showLogin) { - login(); + if (showSignIn) { + signin(); } else { register(); } @@ -186,19 +181,23 @@ const Authentication = observer(({ setEmail(value); }; - const handlePasswordConfirmationChange = (event: TargetedEvent) => { + const handlePasswordConfirmationChange = ( + event: TargetedEvent + ) => { const { value } = event.target as HTMLInputElement; setPasswordConfirmation(value); }; - const handleMergeLocalData = async (event: TargetedEvent) => { + const handleMergeLocalData = async ( + event: TargetedEvent + ) => { const { checked } = event.target as HTMLInputElement; setShouldMergeLocal(checked); if (!checked) { const confirmResult = await confirmDialog({ text: STRING_ACCOUNT_MENU_UNCHECK_MERGE, - confirmButtonStyle: 'danger' + confirmButtonStyle: 'danger', }); setShouldMergeLocal(!confirmResult); } @@ -206,10 +205,12 @@ const Authentication = observer(({ return ( <> - {!user && !showLogin && !showRegister && ( + {!application.hasAccount() && !showSignIn && !showRegister && (
-
Sign in or register to enable sync and end-to-end encryption.
+
+ Sign in or register to enable sync and end-to-end encryption. +
- Standard Notes is free on every platform, and comes - standard with sync and encryption. + Standard Notes is free on every platform, and comes standard with + sync and encryption.
)} - {(showLogin || showRegister) && ( + {(showSignIn || showRegister) && (
- {showLogin ? 'Sign In' : 'Register'} + {showSignIn ? 'Sign In' : 'Register'}
-
+
- {showRegister && - } + {showRegister && ( + + )}
@@ -301,24 +308,28 @@ const Authentication = observer(({ required />
- {showLogin && ( + {showSignIn && ( )} @@ -327,9 +338,12 @@ const Authentication = observer(({ )} {!isAuthenticating && (
-
)} @@ -337,9 +351,9 @@ const Authentication = observer(({
No Password Reset.
- Because your notes are encrypted using your password, - Standard Notes does not have a password reset option. - You cannot forget your password. + Because your notes are encrypted using your password, Standard + Notes does not have a password reset option. You cannot forget + your password.
)} @@ -358,7 +372,7 @@ const Authentication = observer(({ setIsEphemeral(prevState => !prevState)} + onChange={() => setIsEphemeral((prevState) => !prevState)} />

Stay signed in

@@ -371,7 +385,9 @@ const Authentication = observer(({ checked={shouldMergeLocal} onChange={handleMergeLocalData} /> -

Merge local data ({notesAndTagsCount}) notes and tags

+

+ Merge local data ({notesAndTagsCount}) notes and tags +

)} @@ -379,7 +395,8 @@ const Authentication = observer(({ )} - )} + )} + ); }); diff --git a/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx b/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx new file mode 100644 index 000000000..5511e2cae --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx @@ -0,0 +1,172 @@ +import { STRING_NON_MATCHING_PASSWORDS } from '@/strings'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { StateUpdater, useRef, useState } from 'preact/hooks'; +import { AccountMenuPane } from '.'; +import { Button } from '../Button'; +import { Checkbox } from '../Checkbox'; +import { IconButton } from '../IconButton'; +import { InputWithIcon } from '../InputWithIcon'; +import { AdvancedOptions } from './AdvancedOptions'; + +type Props = { + appState: AppState; + application: WebApplication; + setMenuPane: (pane: AccountMenuPane) => void; + email: string; + password: string; + setPassword: StateUpdater; +}; + +export const ConfirmPassword: FunctionComponent = observer( + ({ application, appState, setMenuPane, email, password, setPassword }) => { + const { notesAndTagsCount } = appState.accountMenu; + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [isRegistering, setIsRegistering] = useState(false); + const [isEphemeral, setIsEphemeral] = useState(false); + const [shouldMergeLocal, setShouldMergeLocal] = useState(true); + + const passwordInputRef = useRef(); + + const handlePasswordChange = (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setConfirmPassword(e.target.value); + } + }; + + const handleEphemeralChange = () => { + setIsEphemeral(!isEphemeral); + }; + + const handleShouldMergeChange = () => { + setShouldMergeLocal(!shouldMergeLocal); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleConfirmFormSubmit(e); + } + }; + + const handleConfirmFormSubmit = (e: Event) => { + e.preventDefault(); + + if (!password) { + passwordInputRef?.current.focus(); + return; + } + + if (password === confirmPassword) { + setIsRegistering(true); + application + .register(email, password, isEphemeral, shouldMergeLocal) + .then((res) => { + if (res.error) { + throw new Error(res.error.message); + } + appState.accountMenu.closeAccountMenu(); + appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu); + }) + .catch((err) => { + console.error(err); + application.alertService.alert(err).finally(() => { + setPassword(''); + handleGoBack(); + }); + }) + .finally(() => { + setIsRegistering(false); + }); + } else { + application.alertService + .alert(STRING_NON_MATCHING_PASSWORDS) + .finally(() => { + setConfirmPassword(''); + passwordInputRef?.current.focus(); + }); + } + }; + + const handleGoBack = () => { + setMenuPane(AccountMenuPane.Register); + }; + + return ( + <> +
+ +
Confirm password
+
+
+ Because your notes are encrypted using your password,{' '} + + Standard Notes does not have a password reset option + + . If you forget your password, you will permanently lose access to + your data. +
+
+ + + ) : ( + <> + + + + )} + + {user ? ( + <> +
+ + + ) : null} + + ); + } +); diff --git a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx index 69d675e1a..95df944c4 100644 --- a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx +++ b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx @@ -40,7 +40,6 @@ const PasscodeLock = observer(({ const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession()); const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); - const handleAddPassCode = () => { setShowPasscodeForm(true); setIsPasscodeFocused(true); diff --git a/app/assets/javascripts/components/AccountMenu/SignIn.tsx b/app/assets/javascripts/components/AccountMenu/SignIn.tsx new file mode 100644 index 000000000..3b1ab702f --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/SignIn.tsx @@ -0,0 +1,227 @@ +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import { AccountMenuPane } from '.'; +import { Button } from '../Button'; +import { Checkbox } from '../Checkbox'; +import { Icon } from '../Icon'; +import { IconButton } from '../IconButton'; +import { InputWithIcon } from '../InputWithIcon'; +import { AdvancedOptions } from './AdvancedOptions'; + +type Props = { + appState: AppState; + application: WebApplication; + setMenuPane: (pane: AccountMenuPane) => void; +}; + +export const SignInPane: FunctionComponent = observer( + ({ application, appState, setMenuPane }) => { + const { notesAndTagsCount } = appState.accountMenu; + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isInvalid, setIsInvalid] = useState(false); + const [isEphemeral, setIsEphemeral] = useState(false); + const [isStrictSignin, setIsStrictSignin] = useState(false); + const [isSigningIn, setIsSigningIn] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [shouldMergeLocal, setShouldMergeLocal] = useState(true); + + const emailInputRef = useRef(); + const passwordInputRef = useRef(); + + useEffect(() => { + if (emailInputRef?.current) { + emailInputRef.current.focus(); + } + }, []); + + const resetInvalid = () => { + if (isInvalid) { + setIsInvalid(false); + } + }; + + const handleEmailChange = (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setEmail(e.target.value); + } + }; + + const handlePasswordChange = (e: Event) => { + if (isInvalid) { + setIsInvalid(false); + } + if (e.target instanceof HTMLInputElement) { + setPassword(e.target.value); + } + }; + + const handleEphemeralChange = () => { + setIsEphemeral(!isEphemeral); + }; + + const handleStrictSigninChange = () => { + setIsStrictSignin(!isStrictSignin); + }; + + const handleShouldMergeChange = () => { + setShouldMergeLocal(!shouldMergeLocal); + }; + + const signIn = () => { + setIsSigningIn(true); + emailInputRef?.current.blur(); + passwordInputRef?.current.blur(); + + application + .signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal) + .then((res) => { + if (res.error) { + throw new Error(res.error.message); + } + appState.accountMenu.closeAccountMenu(); + }) + .catch((err) => { + console.error(err); + if (err.toString().includes('Invalid email or password')) { + setIsInvalid(true); + } else { + application.alertService.alert(err); + } + setPassword(''); + passwordInputRef?.current.blur(); + }) + .finally(() => { + setIsSigningIn(false); + }); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleSignInFormSubmit(e); + } + }; + + const handleSignInFormSubmit = (e: Event) => { + e.preventDefault(); + + if (!email || email.length === 0) { + emailInputRef?.current.focus(); + return; + } + + if (!password || password.length === 0) { + passwordInputRef?.current.focus(); + return; + } + + signIn(); + }; + + return ( + <> +
+ setMenuPane(AccountMenuPane.GeneralMenu)} + focusable={true} + disabled={isSigningIn} + /> +
Sign in
+
+
+
+ + + {isInvalid ? ( +
+ Invalid email or password. +
+ ) : null} +
+
+
+ +
+ + + + +
+
+ + ); + } +); diff --git a/app/assets/javascripts/components/AccountMenu/User.tsx b/app/assets/javascripts/components/AccountMenu/User.tsx index 07e5f58ac..1dc2b2af4 100644 --- a/app/assets/javascripts/components/AccountMenu/User.tsx +++ b/app/assets/javascripts/components/AccountMenu/User.tsx @@ -1,6 +1,5 @@ import { observer } from 'mobx-react-lite'; import { AppState } from '@/ui_models/app_state'; -import { PasswordWizardType } from '@/types'; import { WebApplication } from '@/ui_models/application'; import { User } from '@standardnotes/snjs/dist/@types/services/api/responses'; @@ -10,22 +9,12 @@ type Props = { } const User = observer(({ - appState, - application, - }: Props) => { - const { server, closeAccountMenu } = appState.accountMenu; + appState, + application, +}: Props) => { + const { server } = appState.accountMenu; const user = application.getUser(); - const openPasswordWizard = () => { - closeAccountMenu(); - application.presentPasswordWizard(PasswordWizardType.ChangePassword); - }; - - const openSessionsModal = () => { - closeAccountMenu(); - appState.openSessionsModal(); - }; - return (
{appState.sync.errorMessage && ( @@ -56,12 +45,6 @@ const User = observer(({
); }); diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/components/AccountMenu/index.tsx index cf8c45b2f..77da5608c 100644 --- a/app/assets/javascripts/components/AccountMenu/index.tsx +++ b/app/assets/javascripts/components/AccountMenu/index.tsx @@ -2,90 +2,115 @@ import { observer } from 'mobx-react-lite'; import { toDirective } from '@/components/utils'; import { AppState } from '@/ui_models/app_state'; import { WebApplication } from '@/ui_models/application'; -import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; -import Authentication from '@/components/AccountMenu/Authentication'; -import Footer from '@/components/AccountMenu/Footer'; -import User from '@/components/AccountMenu/User'; -import Encryption from '@/components/AccountMenu/Encryption'; -import Protections from '@/components/AccountMenu/Protections'; -import PasscodeLock from '@/components/AccountMenu/PasscodeLock'; -import DataBackup from '@/components/AccountMenu/DataBackup'; -import ErrorReporting from '@/components/AccountMenu/ErrorReporting'; -import { useEffect } from 'preact/hooks'; +import { useState } from 'preact/hooks'; +import { GeneralAccountMenu } from './GeneralAccountMenu'; +import { FunctionComponent } from 'preact'; +import { SignInPane } from './SignIn'; +import { CreateAccount } from './CreateAccount'; +import { ConfirmSignoutContainer } from '../ConfirmSignoutModal'; +import { ConfirmPassword } from './ConfirmPassword'; + +export enum AccountMenuPane { + GeneralMenu, + SignIn, + Register, + ConfirmPassword, +} type Props = { appState: AppState; application: WebApplication; }; -const AccountMenu = observer(({ application, appState }: Props) => { - const { - show: showAccountMenu, - showLogin, - showRegister, - setShowLogin, - setShowRegister, - closeAccountMenu - } = appState.accountMenu; +type PaneSelectorProps = Props & { + menuPane: AccountMenuPane; + setMenuPane: (pane: AccountMenuPane) => void; + closeMenu: () => void; +}; - const user = application.getUser(); +const MenuPaneSelector: FunctionComponent = observer( + ({ application, appState, menuPane, setMenuPane, closeMenu }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); - useEffect(() => { - // Reset "Login" and "Registration" sections state when hiding account menu, - // so the next time account menu is opened these sections are closed - if (!showAccountMenu) { - setShowLogin(false); - setShowRegister(false); - } - }, [setShowLogin, setShowRegister, showAccountMenu]); - - return ( -
-
-
-
Account
- Close -
-
- + ); + case AccountMenuPane.SignIn: + return ( + + ); + case AccountMenuPane.Register: + return ( + + ); + case AccountMenuPane.ConfirmPassword: + return ( + + ); + } + } +); + +const AccountMenu: FunctionComponent = observer( + ({ application, appState }) => { + const { + currentPane, + setCurrentPane, + shouldAnimateCloseMenu, + closeAccountMenu, + } = appState.accountMenu; + + return ( +
+
+ - {!showLogin && !showRegister && ( -
- {user && ( - - )} - - - - - -
- )}
-
-
- ); -}); - -export const AccountMenuDirective = toDirective( - AccountMenu + ); + } ); + +export const AccountMenuDirective = toDirective(AccountMenu); diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx index bf13b5509..6737c971e 100644 --- a/app/assets/javascripts/components/Button.tsx +++ b/app/assets/javascripts/components/Button.tsx @@ -1,27 +1,34 @@ +import { JSXInternal } from 'preact/src/jsx'; +import TargetedEvent = JSXInternal.TargetedEvent; +import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; + import { FunctionComponent } from 'preact'; const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`; -const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \ -focus:bg-contrast hover:bg-contrast`; -const primaryClass = `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 \ -focus:brightness-130`; +type ButtonType = 'normal' | 'primary' | 'danger'; + +const buttonClasses: { [type in ButtonType]: string } = { + normal: `${baseClass} bg-default color-text border-neutral border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`, + primary: `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 focus:brightness-130`, + danger: `${baseClass} bg-default color-danger border-neutral border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`, +}; export const Button: FunctionComponent<{ className?: string; - type: 'normal' | 'primary'; + type: ButtonType; label: string; - onClick: () => void; + onClick: (event: TargetedEvent | TargetedMouseEvent) => void; disabled?: boolean; }> = ({ type, label, className = '', onClick, disabled = false }) => { - const buttonClass = type === 'primary' ? primaryClass : normalClass; + const buttonClass = buttonClasses[type]; const cursorClass = disabled ? 'cursor-default' : 'cursor-pointer'; return ( @@ -95,7 +95,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => { } else { application.signOut(); } - close(); + closeDialog(); }} > {application.hasAccount() diff --git a/app/assets/javascripts/components/ConfirmationDialog.tsx b/app/assets/javascripts/components/ConfirmationDialog.tsx new file mode 100644 index 000000000..78114ed66 --- /dev/null +++ b/app/assets/javascripts/components/ConfirmationDialog.tsx @@ -0,0 +1,35 @@ +import { ComponentChildren, FunctionComponent } from 'preact'; +import { + AlertDialog, + AlertDialogDescription, + AlertDialogLabel, +} from '@reach/alert-dialog'; +import { useRef } from 'preact/hooks'; + +export const ConfirmationDialog: FunctionComponent<{ + title: string | ComponentChildren; +}> = ({ title, children }) => { + const ldRef = useRef(); + + return ( + + {/* sn-component is focusable by default, but doesn't stretch to child width + resulting in a badly focused dialog. Utility classes are not available + at the sn-component level, only below it. tabIndex -1 disables focus + and enables it on the child component */} +
+
+ {title} +
+ + + {children} + +
+
+ + ); +}; diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx index f649ab38f..fe3b67178 100644 --- a/app/assets/javascripts/components/DecoratedInput.tsx +++ b/app/assets/javascripts/components/DecoratedInput.tsx @@ -1,42 +1,69 @@ import { FunctionalComponent, ComponentChild } from 'preact'; +import { HtmlInputTypes } from '@/enums'; interface Props { + type?: HtmlInputTypes; className?: string; disabled?: boolean; left?: ComponentChild[]; right?: ComponentChild[]; text?: string; + placeholder?: string; + onChange?: (text: string) => void; + autocomplete?: boolean; } /** * Input that can be decorated on the left and right side */ export const DecoratedInput: FunctionalComponent = ({ + type = 'text', className = '', disabled = false, left, right, text, + placeholder = '', + onChange, + autocomplete = false, }) => { - const base = - 'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center gap-4'; + const baseClasses = + 'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center bg-contrast'; const stateClasses = disabled - ? 'no-border bg-grey-5' + ? 'no-border' : 'border-solid border-1 border-gray-300'; - const classes = `${base} ${stateClasses} ${className}`; + const classes = `${baseClasses} ${stateClasses} ${className}`; + const inputBaseClasses = 'w-full no-border color-text focus:shadow-none bg-contrast'; + const inputStateClasses = disabled ? 'overflow-ellipsis' : ''; return (
- {left} + {left?.map((leftChild) => ( + <> + {leftChild} +
+ + ))}
+ onChange && onChange((e.target as HTMLInputElement).value) + } + data-lpignore={type !== 'password' ? true : false} + autocomplete={autocomplete ? 'on' : 'off'} />
- {right} + {right?.map((rightChild) => ( + <> +
+ {rightChild} + + ))}
); }; diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx new file mode 100644 index 000000000..e06994bae --- /dev/null +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -0,0 +1,119 @@ +import { + ListboxArrow, + ListboxButton, + ListboxInput, + ListboxList, + ListboxOption, + ListboxPopover, +} from '@reach/listbox'; +import VisuallyHidden from '@reach/visually-hidden'; +import { FunctionComponent } from 'preact'; +import { IconType, Icon } from './Icon'; +import { useState } from 'preact/hooks'; + +export type DropdownItem = { + icon?: IconType; + label: string; + value: string; +}; + +type DropdownProps = { + id: string; + label: string; + items: DropdownItem[]; + defaultValue: string; + onChange: (value: string) => void; +}; + +type ListboxButtonProps = { + icon?: IconType; + value: string | null; + label: string; + isExpanded: boolean; +}; + +const CustomDropdownButton: FunctionComponent = ({ + label, + isExpanded, + icon, +}) => ( + <> +
+ {icon ? ( +
+ +
+ ) : null} +
{label}
+
+ + + + +); + +export const Dropdown: FunctionComponent = ({ + id, + label, + items, + defaultValue, + onChange, +}) => { + const [value, setValue] = useState(defaultValue); + + const labelId = `${id}-label`; + + const handleChange = (value: string) => { + setValue(value); + onChange(value); + }; + + return ( + <> + {label} + + { + const current = items.find((item) => item.value === value); + const icon = current ? current?.icon : null; + return CustomDropdownButton({ + value, + label, + isExpanded, + ...(icon ? { icon } : null), + }); + }} + /> + +
+ + {items.map((item) => ( + + {item.icon ? ( +
+ +
+ ) : null} +
{item.label}
+
+ ))} +
+
+
+
+ + ); +}; diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index 1a30ee761..051ac9c19 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -1,4 +1,5 @@ import PencilOffIcon from '../../icons/ic-pencil-off.svg'; +import PlainTextIcon from '../../icons/ic-text-paragraph.svg'; import RichTextIcon from '../../icons/ic-text-rich.svg'; import TrashIcon from '../../icons/ic-trash.svg'; import PinIcon from '../../icons/ic-pin.svg'; @@ -13,6 +14,12 @@ import PasswordIcon from '../../icons/ic-textbox-password.svg'; import TrashSweepIcon from '../../icons/ic-trash-sweep.svg'; import MoreIcon from '../../icons/ic-more.svg'; import TuneIcon from '../../icons/ic-tune.svg'; +import MenuArrowDownIcon from '../../icons/ic-menu-arrow-down.svg'; +import AuthenticatorIcon from '../../icons/ic-authenticator.svg'; +import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg'; +import TasksIcon from '../../icons/ic-tasks.svg'; +import MarkdownIcon from '../../icons/ic-markdown.svg'; +import CodeIcon from '../../icons/ic-code.svg'; import AccessibilityIcon from '../../icons/ic-accessibility.svg'; import HelpIcon from '../../icons/ic-help.svg'; @@ -26,13 +33,46 @@ import UserIcon from '../../icons/ic-user.svg'; import CopyIcon from '../../icons/ic-copy.svg'; import DownloadIcon from '../../icons/ic-download.svg'; import InfoIcon from '../../icons/ic-info.svg'; +import CheckIcon from '../../icons/ic-check.svg'; +import CheckBoldIcon from '../../icons/ic-check-bold.svg'; +import AccountCircleIcon from '../../icons/ic-account-circle.svg'; +import CloudOffIcon from '../../icons/ic-cloud-off.svg'; +import SignInIcon from '../../icons/ic-signin.svg'; +import SignOutIcon from '../../icons/ic-signout.svg'; +import CheckCircleIcon from '../../icons/ic-check-circle.svg'; +import SyncIcon from '../../icons/ic-sync.svg'; +import ArrowLeftIcon from '../../icons/ic-arrow-left.svg'; +import ChevronDownIcon from '../../icons/ic-chevron-down.svg'; +import EmailIcon from '../../icons/ic-email.svg'; +import ServerIcon from '../../icons/ic-server.svg'; +import EyeIcon from '../../icons/ic-eye.svg'; +import EyeOffIcon from '../../icons/ic-eye-off.svg'; +import LockIcon from '../../icons/ic-lock.svg'; import { toDirective } from './utils'; import { FunctionalComponent } from 'preact'; const ICONS = { + lock: LockIcon, + eye: EyeIcon, + 'eye-off': EyeOffIcon, + server: ServerIcon, + email: EmailIcon, + 'chevron-down': ChevronDownIcon, + 'arrow-left': ArrowLeftIcon, + sync: SyncIcon, + 'check-circle': CheckCircleIcon, + signIn: SignInIcon, + signOut: SignOutIcon, + 'cloud-off': CloudOffIcon, 'pencil-off': PencilOffIcon, + 'plain-text': PlainTextIcon, 'rich-text': RichTextIcon, + code: CodeIcon, + markdown: MarkdownIcon, + authenticator: AuthenticatorIcon, + spreadsheets: SpreadsheetsIcon, + tasks: TasksIcon, trash: TrashIcon, pin: PinIcon, unpin: UnpinIcon, @@ -58,6 +98,10 @@ const ICONS = { copy: CopyIcon, download: DownloadIcon, info: InfoIcon, + check: CheckIcon, + 'check-bold': CheckBoldIcon, + 'account-circle': AccountCircleIcon, + 'menu-arrow-down': MenuArrowDownIcon, }; export type IconType = keyof typeof ICONS; diff --git a/app/assets/javascripts/components/IconButton.tsx b/app/assets/javascripts/components/IconButton.tsx index e15517419..45ce99942 100644 --- a/app/assets/javascripts/components/IconButton.tsx +++ b/app/assets/javascripts/components/IconButton.tsx @@ -10,6 +10,17 @@ interface Props { className?: string; icon: IconType; + + iconClassName?: string; + + /** + * Button tooltip + */ + title: string; + + focusable: boolean; + + disabled?: boolean; } /** @@ -18,21 +29,26 @@ interface Props { */ export const IconButton: FunctionComponent = ({ onClick, - className, + className = '', icon, + title, + focusable, + iconClassName = '', + disabled = false, }) => { const click = (e: MouseEvent) => { e.preventDefault(); onClick(); }; + const focusableClass = focusable ? '' : 'focus:shadow-none'; return ( ); }; diff --git a/app/assets/javascripts/components/Input.tsx b/app/assets/javascripts/components/Input.tsx index 0955b632c..39fac5e08 100644 --- a/app/assets/javascripts/components/Input.tsx +++ b/app/assets/javascripts/components/Input.tsx @@ -11,9 +11,9 @@ export const Input: FunctionalComponent = ({ disabled = false, text, }) => { - const base = `rounded py-1.5 px-3 text-input my-1 h-8`; + const base = `rounded py-1.5 px-3 text-input my-1 h-8 bg-contrast`; const stateClasses = disabled - ? 'no-border bg-grey-5' + ? 'no-border' : 'border-solid border-1 border-gray-300'; const classes = `${base} ${stateClasses} ${className}`; return ( diff --git a/app/assets/javascripts/components/InputWithIcon.tsx b/app/assets/javascripts/components/InputWithIcon.tsx new file mode 100644 index 000000000..5277d0238 --- /dev/null +++ b/app/assets/javascripts/components/InputWithIcon.tsx @@ -0,0 +1,89 @@ +import { FunctionComponent, Ref } from 'preact'; +import { JSXInternal } from 'preact/src/jsx'; +import { forwardRef } from 'preact/compat'; +import { Icon, IconType } from './Icon'; +import { IconButton } from './IconButton'; + +type ToggleProps = { + toggleOnIcon: IconType; + toggleOffIcon: IconType; + title: string; + toggled: boolean; + onClick: (toggled: boolean) => void; +}; + +type Props = { + icon: IconType; + inputType: 'text' | 'email' | 'password'; + className?: string; + iconClassName?: string; + value: string | undefined; + onChange: JSXInternal.GenericEventHandler; + onFocus?: JSXInternal.GenericEventHandler; + onKeyDown?: JSXInternal.KeyboardEventHandler; + disabled?: boolean; + placeholder: string; + toggle?: ToggleProps; +}; + +const DISABLED_CLASSNAME = 'bg-grey-5 cursor-not-allowed'; + +export const InputWithIcon: FunctionComponent = forwardRef( + ( + { + icon, + inputType, + className, + iconClassName, + value, + onChange, + onFocus, + onKeyDown, + disabled, + toggle, + placeholder, + }, + ref: Ref + ) => { + const handleToggle = () => { + if (toggle) toggle.onClick(!toggle?.toggled); + }; + + return ( +
+
+ +
+ + {toggle ? ( +
+ +
+ ) : null} +
+ ); + } +); diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index ba80ebe48..9a31fd1e6 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -54,7 +54,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => { const getTabIndex = () => { if (focusedTagUuid) { return focusedTagUuid === tag.uuid ? 0 : -1; - } + } if (autocompleteInputFocused) { return -1; } diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index 292e7265b..252516a98 100644 --- a/app/assets/javascripts/components/NotesContextMenu.tsx +++ b/app/assets/javascripts/components/NotesContextMenu.tsx @@ -24,7 +24,7 @@ const NotesContextMenu = observer(({ application, appState }: Props) => { ); useCloseOnClickOutside( - contextMenuRef, + contextMenuRef, (open: boolean) => appState.notes.setContextMenuOpen(open) ); diff --git a/app/assets/javascripts/components/OtherSessionsSignOut.tsx b/app/assets/javascripts/components/OtherSessionsSignOut.tsx new file mode 100644 index 000000000..b72487bd1 --- /dev/null +++ b/app/assets/javascripts/components/OtherSessionsSignOut.tsx @@ -0,0 +1,80 @@ +import { useRef, useState } from 'preact/hooks'; +import { + AlertDialog, + AlertDialogDescription, + AlertDialogLabel, +} from '@reach/alert-dialog'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; + +type Props = { + application: WebApplication; + appState: AppState; +}; + +export const OtherSessionsSignOutContainer = observer((props: Props) => { + if (!props.appState.accountMenu.otherSessionsSignOut) { + return null; + } + return ; +}); + +const ConfirmOtherSessionsSignOut = observer( + ({ application, appState }: Props) => { + const cancelRef = useRef(); + function closeDialog() { + appState.accountMenu.setOtherSessionsSignOut(false); + } + + return ( + +
+
+
+
+
+ + End all other sessions? + + +

+ This action will sign out all other devices signed into + your account, and remove your data from those devices when + they next regain connection to the internet. You may sign + back in on those devices at any time. +

+
+
+ + +
+
+
+
+
+
+
+ ); + } +); diff --git a/app/assets/javascripts/components/SessionsModal.tsx b/app/assets/javascripts/components/SessionsModal.tsx index 875f25f9c..e06c7df7b 100644 --- a/app/assets/javascripts/components/SessionsModal.tsx +++ b/app/assets/javascripts/components/SessionsModal.tsx @@ -26,12 +26,12 @@ type Session = RemoteSession & { function useSessions( application: SNApplication ): [ - Session[], - () => void, - boolean, - (uuid: UuidString) => Promise, - string -] { + Session[], + () => void, + boolean, + (uuid: UuidString) => Promise, + string + ] { const [sessions, setSessions] = useState([]); const [lastRefreshDate, setLastRefreshDate] = useState(Date.now()); const [refreshing, setRefreshing] = useState(true); @@ -240,7 +240,7 @@ const SessionsModal: FunctionComponent<{ ); }; -const Sessions: FunctionComponent<{ +export const Sessions: FunctionComponent<{ appState: AppState; application: WebApplication; }> = observer(({ appState, application }) => { diff --git a/app/assets/javascripts/components/Switch.tsx b/app/assets/javascripts/components/Switch.tsx index 0a35c2a74..49da00dc9 100644 --- a/app/assets/javascripts/components/Switch.tsx +++ b/app/assets/javascripts/components/Switch.tsx @@ -23,7 +23,7 @@ export const Switch: FunctionalComponent = ( const className = props.className ?? ''; return (