refactor: format and lint codebase (#971)
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { Checkbox } from '@/Components/Checkbox'
|
||||
import { DecoratedInput } from '@/Components/Input/DecoratedInput'
|
||||
import { Icon } from '@/Components/Icon'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
disabled?: boolean
|
||||
onVaultChange?: (isVault: boolean, vaultedEmail?: string) => void
|
||||
onStrictSignInChange?: (isStrictSignIn: boolean) => void
|
||||
}
|
||||
|
||||
export const AdvancedOptions: FunctionComponent<Props> = observer(
|
||||
({ appState, application, disabled = false, onVaultChange, onStrictSignInChange, children }) => {
|
||||
const { server, setServer, enableServerOption, setEnableServerOption } = appState.accountMenu
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
|
||||
const [isVault, setIsVault] = useState(false)
|
||||
const [vaultName, setVaultName] = useState('')
|
||||
const [vaultUserphrase, setVaultUserphrase] = useState('')
|
||||
|
||||
const [isStrictSignin, setIsStrictSignin] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const recomputeVaultedEmail = async () => {
|
||||
const vaultedEmail = await application.vaultToEmail(vaultName, vaultUserphrase)
|
||||
|
||||
if (!vaultedEmail) {
|
||||
if (vaultName?.length > 0 && vaultUserphrase?.length > 0) {
|
||||
application.alertService.alert('Unable to compute vault name.').catch(console.error)
|
||||
}
|
||||
return
|
||||
}
|
||||
onVaultChange?.(true, vaultedEmail)
|
||||
}
|
||||
|
||||
if (vaultName && vaultUserphrase) {
|
||||
recomputeVaultedEmail().catch(console.error)
|
||||
}
|
||||
}, [vaultName, vaultUserphrase, application, onVaultChange])
|
||||
|
||||
useEffect(() => {
|
||||
onVaultChange?.(isVault)
|
||||
}, [isVault, onVaultChange])
|
||||
|
||||
const handleIsVaultChange = () => {
|
||||
setIsVault(!isVault)
|
||||
}
|
||||
|
||||
const handleVaultNameChange = (name: string) => {
|
||||
setVaultName(name)
|
||||
}
|
||||
|
||||
const handleVaultUserphraseChange = (userphrase: string) => {
|
||||
setVaultUserphrase(userphrase)
|
||||
}
|
||||
|
||||
const handleServerOptionChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEnableServerOption(e.target.checked)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSyncServerChange = (server: string) => {
|
||||
setServer(server)
|
||||
application.setCustomHost(server).catch(console.error)
|
||||
}
|
||||
|
||||
const handleStrictSigninChange = () => {
|
||||
const newValue = !isStrictSignin
|
||||
setIsStrictSignin(newValue)
|
||||
onStrictSignInChange?.(newValue)
|
||||
}
|
||||
|
||||
const toggleShowAdvanced = () => {
|
||||
setShowAdvanced(!showAdvanced)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none font-bold"
|
||||
onClick={toggleShowAdvanced}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Advanced options
|
||||
<Icon type="chevron-down" className="color-grey-1 ml-1" />
|
||||
</div>
|
||||
</button>
|
||||
{showAdvanced ? (
|
||||
<div className="px-3 my-2">
|
||||
{children}
|
||||
|
||||
{appState.enableUnfinishedFeatures && (
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<Checkbox
|
||||
name="vault-mode"
|
||||
label="Vault Mode"
|
||||
checked={isVault}
|
||||
disabled={disabled}
|
||||
onChange={handleIsVaultChange}
|
||||
/>
|
||||
<a
|
||||
href="https://standardnotes.com/help/80"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Learn more"
|
||||
>
|
||||
<Icon type="info" className="color-neutral" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{appState.enableUnfinishedFeatures && isVault && (
|
||||
<>
|
||||
<DecoratedInput
|
||||
className={'mb-2'}
|
||||
left={[<Icon type="folder" className="color-neutral" />]}
|
||||
type="text"
|
||||
placeholder="Vault name"
|
||||
value={vaultName}
|
||||
onChange={handleVaultNameChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<DecoratedInput
|
||||
className={'mb-2'}
|
||||
left={[<Icon type="server" className="color-neutral" />]}
|
||||
type="text"
|
||||
placeholder="Vault userphrase"
|
||||
value={vaultUserphrase}
|
||||
onChange={handleVaultUserphraseChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{onStrictSignInChange && (
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<Checkbox
|
||||
name="use-strict-signin"
|
||||
label="Use strict sign-in"
|
||||
checked={isStrictSignin}
|
||||
disabled={disabled}
|
||||
onChange={handleStrictSigninChange}
|
||||
/>
|
||||
<a
|
||||
href="https://standardnotes.com/help/security"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Learn more"
|
||||
>
|
||||
<Icon type="info" className="color-neutral" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
name="custom-sync-server"
|
||||
label="Custom sync server"
|
||||
checked={enableServerOption}
|
||||
onChange={handleServerOptionChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<DecoratedInput
|
||||
type="text"
|
||||
left={[<Icon type="server" className="color-neutral" />]}
|
||||
placeholder="https://api.standardnotes.com"
|
||||
value={server}
|
||||
onChange={handleSyncServerChange}
|
||||
disabled={!enableServerOption && !disabled}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,154 @@
|
||||
import { STRING_NON_MATCHING_PASSWORDS } from '@/Strings'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { AccountMenuPane } from '.'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Checkbox } from '@/Components/Checkbox'
|
||||
import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { Icon } from '@/Components/Icon'
|
||||
import { IconButton } from '@/Components/Button/IconButton'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export const ConfirmPassword: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane, email, password }) => {
|
||||
const { notesAndTagsCount } = appState.accountMenu
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [isRegistering, setIsRegistering] = useState(false)
|
||||
const [isEphemeral, setIsEphemeral] = useState(false)
|
||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
passwordInputRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const handlePasswordChange = (text: string) => {
|
||||
setConfirmPassword(text)
|
||||
}
|
||||
|
||||
const handleEphemeralChange = () => {
|
||||
setIsEphemeral(!isEphemeral)
|
||||
}
|
||||
|
||||
const handleShouldMergeChange = () => {
|
||||
setShouldMergeLocal(!shouldMergeLocal)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (error.length) {
|
||||
setError('')
|
||||
}
|
||||
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)
|
||||
setError(err.message)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRegistering(false)
|
||||
})
|
||||
} else {
|
||||
setError(STRING_NON_MATCHING_PASSWORDS)
|
||||
setConfirmPassword('')
|
||||
passwordInputRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoBack = () => {
|
||||
setMenuPane(AccountMenuPane.Register)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={handleGoBack}
|
||||
focusable={true}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Confirm password</div>
|
||||
</div>
|
||||
<div className="px-3 mb-3 text-sm">
|
||||
Because your notes are encrypted using your password,{' '}
|
||||
<span className="color-dark-red">
|
||||
Standard Notes does not have a password reset option
|
||||
</span>
|
||||
. If you forget your password, you will permanently lose access to your data.
|
||||
</div>
|
||||
<form onSubmit={handleConfirmFormSubmit} className="px-3 mb-1">
|
||||
<DecoratedPasswordInput
|
||||
className="mb-2"
|
||||
disabled={isRegistering}
|
||||
left={[<Icon type="password" className="color-neutral" />]}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Confirm password"
|
||||
ref={passwordInputRef}
|
||||
value={confirmPassword}
|
||||
/>
|
||||
{error ? <div className="color-dark-red my-2">{error}</div> : null}
|
||||
<Button
|
||||
className="btn-w-full mt-1 mb-3"
|
||||
label={isRegistering ? 'Creating account...' : 'Create account & sign in'}
|
||||
variant="primary"
|
||||
onClick={handleConfirmFormSubmit}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<Checkbox
|
||||
name="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
checked={!isEphemeral}
|
||||
onChange={handleEphemeralChange}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
{notesAndTagsCount > 0 ? (
|
||||
<Checkbox
|
||||
name="should-merge-local"
|
||||
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
|
||||
checked={shouldMergeLocal}
|
||||
onChange={handleShouldMergeChange}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
) : null}
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
130
app/assets/javascripts/Components/AccountMenu/CreateAccount.tsx
Normal file
130
app/assets/javascripts/Components/AccountMenu/CreateAccount.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { AccountMenuPane } from '.'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { DecoratedInput } from '@/Components/Input/DecoratedInput'
|
||||
import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { Icon } from '@/Components/Icon'
|
||||
import { IconButton } from '@/Components/Button/IconButton'
|
||||
import { AdvancedOptions } from './AdvancedOptions'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
email: string
|
||||
setEmail: StateUpdater<string>
|
||||
password: string
|
||||
setPassword: StateUpdater<string>
|
||||
}
|
||||
|
||||
export const CreateAccount: FunctionComponent<Props> = observer(
|
||||
({ appState, application, setMenuPane, email, setEmail, password, setPassword }) => {
|
||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
const [isVault, setIsVault] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef.current) {
|
||||
emailInputRef.current?.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleEmailChange = (text: string) => {
|
||||
setEmail(text)
|
||||
}
|
||||
|
||||
const handlePasswordChange = (text: string) => {
|
||||
setPassword(text)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleRegisterFormSubmit(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegisterFormSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
setEmail(email)
|
||||
setPassword(password)
|
||||
setMenuPane(AccountMenuPane.ConfirmPassword)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setMenuPane(AccountMenuPane.GeneralMenu)
|
||||
setEmail('')
|
||||
setPassword('')
|
||||
}
|
||||
|
||||
const onVaultChange = (isVault: boolean, vaultedEmail?: string) => {
|
||||
setIsVault(isVault)
|
||||
if (isVault && vaultedEmail) {
|
||||
setEmail(vaultedEmail)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={handleClose}
|
||||
focusable={true}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Create account</div>
|
||||
</div>
|
||||
<form onSubmit={handleRegisterFormSubmit} className="px-3 mb-1">
|
||||
<DecoratedInput
|
||||
className="mb-2"
|
||||
disabled={isVault}
|
||||
left={[<Icon type="email" className="color-neutral" />]}
|
||||
onChange={handleEmailChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Email"
|
||||
ref={emailInputRef}
|
||||
type="email"
|
||||
value={email}
|
||||
/>
|
||||
<DecoratedPasswordInput
|
||||
className="mb-2"
|
||||
left={[<Icon type="password" className="color-neutral" />]}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Password"
|
||||
ref={passwordInputRef}
|
||||
value={password}
|
||||
/>
|
||||
<Button
|
||||
className="btn-w-full mt-1"
|
||||
label="Next"
|
||||
variant="primary"
|
||||
onClick={handleRegisterFormSubmit}
|
||||
/>
|
||||
</form>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<AdvancedOptions
|
||||
application={application}
|
||||
appState={appState}
|
||||
onVaultChange={onVaultChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,189 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Icon } from '@/Components/Icon'
|
||||
import { formatLastSyncDate } from '@/Components/Preferences/Panes/Account/Sync'
|
||||
import { SyncQueueStrategy } from '@standardnotes/snjs'
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/Strings'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { AccountMenuPane } from '.'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Menu } from '@/Components/Menu/Menu'
|
||||
import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem'
|
||||
import { WorkspaceSwitcherOption } from './WorkspaceSwitcher/WorkspaceSwitcherOption'
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
closeMenu: () => void
|
||||
}
|
||||
|
||||
const iconClassName = 'color-neutral mr-2'
|
||||
|
||||
export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane, closeMenu, mainApplicationGroup }) => {
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
|
||||
const [lastSyncDate, setLastSyncDate] = useState(
|
||||
formatLastSyncDate(application.sync.getLastSyncDate() as Date),
|
||||
)
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true)
|
||||
|
||||
application.sync
|
||||
.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && (res as any).error) {
|
||||
throw new Error()
|
||||
} else {
|
||||
setLastSyncDate(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
application.alertService.alert(STRING_GENERIC_SYNC_ERROR).catch(console.error)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSyncingInProgress(false)
|
||||
})
|
||||
}
|
||||
|
||||
const user = application.getUser()
|
||||
|
||||
const CREATE_ACCOUNT_INDEX = 1
|
||||
const SWITCHER_INDEX = 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-3 mt-1 mb-1">
|
||||
<div className="sn-account-menu-headline">Account</div>
|
||||
<div className="flex cursor-pointer" onClick={closeMenu}>
|
||||
<Icon type="close" className="color-neutral" />
|
||||
</div>
|
||||
</div>
|
||||
{user ? (
|
||||
<>
|
||||
<div className="px-3 mb-3 color-foreground text-sm">
|
||||
<div>You're signed in as:</div>
|
||||
<div className="my-0.5 font-bold wrap">{user.email}</div>
|
||||
<span className="color-neutral">{application.getHost()}</span>
|
||||
</div>
|
||||
<div className="flex items-start justify-between px-3 mb-3">
|
||||
{isSyncingInProgress ? (
|
||||
<div className="flex items-center color-info font-semibold">
|
||||
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
|
||||
Syncing...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start">
|
||||
<Icon type="check-circle" className="mr-2 success" />
|
||||
<div>
|
||||
<div class="font-semibold success">Last synced:</div>
|
||||
<div class="color-text">{lastSyncDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex cursor-pointer color-grey-1" onClick={doSynchronization}>
|
||||
<Icon type="sync" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="px-3 mb-1">
|
||||
<div className="mb-3 color-foreground">
|
||||
You’re offline. Sign in to sync your notes and preferences across all your devices
|
||||
and enable end-to-end encryption.
|
||||
</div>
|
||||
<div className="flex items-center color-grey-1">
|
||||
<Icon type="cloud-off" className="mr-2" />
|
||||
<span className="font-semibold">Offline</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Menu
|
||||
isOpen={appState.accountMenu.show}
|
||||
a11yLabel="General account menu"
|
||||
closeMenu={closeMenu}
|
||||
initialFocus={!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX}
|
||||
>
|
||||
<MenuItemSeparator />
|
||||
<WorkspaceSwitcherOption
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
appState={appState}
|
||||
/>
|
||||
<MenuItemSeparator />
|
||||
{user ? (
|
||||
<MenuItem
|
||||
type={MenuItemType.IconButton}
|
||||
onClick={() => {
|
||||
appState.accountMenu.closeAccountMenu()
|
||||
appState.preferences.setCurrentPane('account')
|
||||
appState.preferences.openPreferences()
|
||||
}}
|
||||
>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Account settings
|
||||
</MenuItem>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem
|
||||
type={MenuItemType.IconButton}
|
||||
onClick={() => {
|
||||
setMenuPane(AccountMenuPane.Register)
|
||||
}}
|
||||
>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Create free account
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.IconButton}
|
||||
onClick={() => {
|
||||
setMenuPane(AccountMenuPane.SignIn)
|
||||
}}
|
||||
>
|
||||
<Icon type="signIn" className={iconClassName} />
|
||||
Sign in
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
className="justify-between"
|
||||
type={MenuItemType.IconButton}
|
||||
onClick={() => {
|
||||
appState.accountMenu.closeAccountMenu()
|
||||
appState.preferences.setCurrentPane('help-feedback')
|
||||
appState.preferences.openPreferences()
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="help" className={iconClassName} />
|
||||
Help & feedback
|
||||
</div>
|
||||
<span className="color-neutral">v{appState.version}</span>
|
||||
</MenuItem>
|
||||
{user ? (
|
||||
<>
|
||||
<MenuItemSeparator />
|
||||
<MenuItem
|
||||
type={MenuItemType.IconButton}
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true)
|
||||
}}
|
||||
>
|
||||
<Icon type="signOut" className={iconClassName} />
|
||||
Sign out workspace
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
206
app/assets/javascripts/Components/AccountMenu/SignIn.tsx
Normal file
206
app/assets/javascripts/Components/AccountMenu/SignIn.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { isDev } from '@/Utils'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { AccountMenuPane } from '.'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Checkbox } from '@/Components/Checkbox'
|
||||
import { DecoratedInput } from '@/Components/Input/DecoratedInput'
|
||||
import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { Icon } from '@/Components/Icon'
|
||||
import { IconButton } from '@/Components/Button/IconButton'
|
||||
import { AdvancedOptions } from './AdvancedOptions'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
}
|
||||
|
||||
export const SignInPane: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane }) => {
|
||||
const { notesAndTagsCount } = appState.accountMenu
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [isEphemeral, setIsEphemeral] = useState(false)
|
||||
|
||||
const [isStrictSignin, setIsStrictSignin] = useState(false)
|
||||
const [isSigningIn, setIsSigningIn] = useState(false)
|
||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
|
||||
const [isVault, setIsVault] = useState(false)
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef?.current) {
|
||||
emailInputRef.current?.focus()
|
||||
}
|
||||
if (isDev && window.devAccountEmail) {
|
||||
setEmail(window.devAccountEmail)
|
||||
setPassword(window.devAccountPassword as string)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resetInvalid = () => {
|
||||
if (error.length) {
|
||||
setError('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEmailChange = (text: string) => {
|
||||
setEmail(text)
|
||||
}
|
||||
|
||||
const handlePasswordChange = (text: string) => {
|
||||
if (error.length) {
|
||||
setError('')
|
||||
}
|
||||
setPassword(text)
|
||||
}
|
||||
|
||||
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)
|
||||
setError(err.message ?? err.toString())
|
||||
setPassword('')
|
||||
passwordInputRef?.current?.blur()
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSigningIn(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSignInFormSubmit(e)
|
||||
}
|
||||
}
|
||||
|
||||
const onVaultChange = useCallback(
|
||||
(newIsVault: boolean, vaultedEmail?: string) => {
|
||||
setIsVault(newIsVault)
|
||||
if (newIsVault && vaultedEmail) {
|
||||
setEmail(vaultedEmail)
|
||||
}
|
||||
},
|
||||
[setEmail],
|
||||
)
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
|
||||
focusable={true}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Sign in</div>
|
||||
</div>
|
||||
<div className="px-3 mb-1">
|
||||
<DecoratedInput
|
||||
className={`mb-2 ${error ? 'border-dark-red' : null}`}
|
||||
left={[<Icon type="email" className="color-neutral" />]}
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onFocus={resetInvalid}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isSigningIn || isVault}
|
||||
ref={emailInputRef}
|
||||
/>
|
||||
<DecoratedPasswordInput
|
||||
className={`mb-2 ${error ? 'border-dark-red' : null}`}
|
||||
disabled={isSigningIn}
|
||||
left={[<Icon type="password" className="color-neutral" />]}
|
||||
onChange={handlePasswordChange}
|
||||
onFocus={resetInvalid}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Password"
|
||||
ref={passwordInputRef}
|
||||
value={password}
|
||||
/>
|
||||
{error ? <div className="color-dark-red my-2">{error}</div> : null}
|
||||
<Button
|
||||
className="btn-w-full mt-1 mb-3"
|
||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||
variant="primary"
|
||||
onClick={handleSignInFormSubmit}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<Checkbox
|
||||
name="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
checked={!isEphemeral}
|
||||
disabled={isSigningIn}
|
||||
onChange={handleEphemeralChange}
|
||||
/>
|
||||
{notesAndTagsCount > 0 ? (
|
||||
<Checkbox
|
||||
name="should-merge-local"
|
||||
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
|
||||
checked={shouldMergeLocal}
|
||||
disabled={isSigningIn}
|
||||
onChange={handleShouldMergeChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<AdvancedOptions
|
||||
appState={appState}
|
||||
application={application}
|
||||
disabled={isSigningIn}
|
||||
onVaultChange={onVaultChange}
|
||||
onStrictSignInChange={handleStrictSigninChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
44
app/assets/javascripts/Components/AccountMenu/User.tsx
Normal file
44
app/assets/javascripts/Components/AccountMenu/User.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { User as UserType } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const User = observer(({ appState, application }: Props) => {
|
||||
const { server } = appState.accountMenu
|
||||
const user = application.getUser() as UserType
|
||||
|
||||
return (
|
||||
<div className="sk-panel-section">
|
||||
{appState.sync.errorMessage && (
|
||||
<div className="sk-notification danger">
|
||||
<div className="sk-notification-title">Sync Unreachable</div>
|
||||
<div className="sk-notification-text">
|
||||
Hmm...we can't seem to sync your account. The reason: {appState.sync.errorMessage}
|
||||
</div>
|
||||
<a
|
||||
className="sk-a info-contrast sk-bold sk-panel-row"
|
||||
href="https://standardnotes.com/help"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Need help?
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="sk-panel-row">
|
||||
<div className="sk-panel-column">
|
||||
<div className="sk-h1 sk-bold wrap">{user.email}</div>
|
||||
<div className="sk-subtitle neutral">{server}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sk-panel-row" />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default User
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Icon } from '@/Components/Icon'
|
||||
import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { ApplicationDescriptor } from '@standardnotes/snjs/dist/@types'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
|
||||
type Props = {
|
||||
descriptor: ApplicationDescriptor
|
||||
onClick: () => void
|
||||
onDelete: () => void
|
||||
renameDescriptor: (label: string) => void
|
||||
}
|
||||
|
||||
export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||
descriptor,
|
||||
onClick,
|
||||
onDelete,
|
||||
renameDescriptor,
|
||||
}) => {
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isRenaming) {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}, [isRenaming])
|
||||
|
||||
const handleInputKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
inputRef.current?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputBlur = (event: FocusEvent) => {
|
||||
const name = (event.target as HTMLInputElement).value
|
||||
renameDescriptor(name)
|
||||
setIsRenaming(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
type={MenuItemType.RadioButton}
|
||||
className="sn-dropdown-item py-2 focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={onClick}
|
||||
checked={descriptor.primary}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
{isRenaming ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={descriptor.label}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
) : (
|
||||
<div>{descriptor.label}</div>
|
||||
)}
|
||||
{descriptor.primary && (
|
||||
<div>
|
||||
<button
|
||||
className="w-5 h-5 p-0 mr-3 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsRenaming((isRenaming) => !isRenaming)
|
||||
}}
|
||||
>
|
||||
<Icon type="pencil" className="sn-icon--mid color-neutral" />
|
||||
</button>
|
||||
<button
|
||||
className="w-5 h-5 p-0 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Icon type="trash" className="sn-icon--mid color-danger" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { ApplicationDescriptor } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon'
|
||||
import { Menu } from '@/Components/Menu/Menu'
|
||||
import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem'
|
||||
import { WorkspaceMenuItem } from './WorkspaceMenuItem'
|
||||
|
||||
type Props = {
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
appState: AppState
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
|
||||
({ mainApplicationGroup, appState, isOpen }) => {
|
||||
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>(
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const removeAppGroupObserver = mainApplicationGroup.addApplicationChangeObserver(() => {
|
||||
const applicationDescriptors = mainApplicationGroup.getDescriptors()
|
||||
setApplicationDescriptors(applicationDescriptors)
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeAppGroupObserver()
|
||||
}
|
||||
}, [mainApplicationGroup])
|
||||
|
||||
return (
|
||||
<Menu a11yLabel="Workspace switcher menu" className="px-0 focus:shadow-none" isOpen={isOpen}>
|
||||
{applicationDescriptors.map((descriptor) => (
|
||||
<WorkspaceMenuItem
|
||||
descriptor={descriptor}
|
||||
onDelete={() => {
|
||||
appState.accountMenu.setSigningOut(true)
|
||||
}}
|
||||
onClick={() => {
|
||||
mainApplicationGroup.loadApplicationForDescriptor(descriptor).catch(console.error)
|
||||
}}
|
||||
renameDescriptor={(label: string) =>
|
||||
mainApplicationGroup.renameDescriptor(descriptor, label)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<MenuItemSeparator />
|
||||
<MenuItem
|
||||
type={MenuItemType.IconButton}
|
||||
onClick={() => {
|
||||
mainApplicationGroup.addNewApplication().catch(console.error)
|
||||
}}
|
||||
>
|
||||
<Icon type="user-add" className="color-neutral mr-2" />
|
||||
Add another workspace
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon'
|
||||
import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu'
|
||||
|
||||
type Props = {
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(
|
||||
({ mainApplicationGroup, appState }) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
|
||||
|
||||
const toggleMenu = () => {
|
||||
if (!isOpen) {
|
||||
const menuPosition = calculateSubmenuStyle(buttonRef.current)
|
||||
if (menuPosition) {
|
||||
setMenuStyle(menuPosition)
|
||||
}
|
||||
}
|
||||
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTimeout(() => {
|
||||
const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current)
|
||||
|
||||
if (newMenuPosition) {
|
||||
setMenuStyle(newMenuPosition)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
role="menuitem"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="user-switch" className="color-neutral mr-2" />
|
||||
Switch workspace
|
||||
</div>
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto"
|
||||
style={menuStyle}
|
||||
>
|
||||
<WorkspaceSwitcherMenu
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
appState={appState}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
127
app/assets/javascripts/Components/AccountMenu/index.tsx
Normal file
127
app/assets/javascripts/Components/AccountMenu/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { useRef, useState } from 'preact/hooks'
|
||||
import { GeneralAccountMenu } from './GeneralAccountMenu'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { SignInPane } from './SignIn'
|
||||
import { CreateAccount } from './CreateAccount'
|
||||
import { ConfirmPassword } from './ConfirmPassword'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
|
||||
export enum AccountMenuPane {
|
||||
GeneralMenu,
|
||||
SignIn,
|
||||
Register,
|
||||
ConfirmPassword,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
onClickOutside: () => void
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
}
|
||||
|
||||
type PaneSelectorProps = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
menuPane: AccountMenuPane
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
closeMenu: () => void
|
||||
}
|
||||
|
||||
const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
||||
({ application, appState, menuPane, setMenuPane, closeMenu, mainApplicationGroup }) => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
switch (menuPane) {
|
||||
case AccountMenuPane.GeneralMenu:
|
||||
return (
|
||||
<GeneralAccountMenu
|
||||
appState={appState}
|
||||
application={application}
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
setMenuPane={setMenuPane}
|
||||
closeMenu={closeMenu}
|
||||
/>
|
||||
)
|
||||
case AccountMenuPane.SignIn:
|
||||
return (
|
||||
<SignInPane appState={appState} application={application} setMenuPane={setMenuPane} />
|
||||
)
|
||||
case AccountMenuPane.Register:
|
||||
return (
|
||||
<CreateAccount
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
/>
|
||||
)
|
||||
case AccountMenuPane.ConfirmPassword:
|
||||
return (
|
||||
<ConfirmPassword
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
email={email}
|
||||
password={password}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const AccountMenu: FunctionComponent<Props> = observer(
|
||||
({ application, appState, onClickOutside, mainApplicationGroup }) => {
|
||||
const { currentPane, setCurrentPane, shouldAnimateCloseMenu, closeAccountMenu } =
|
||||
appState.accountMenu
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useCloseOnClickOutside(ref, () => {
|
||||
onClickOutside()
|
||||
})
|
||||
|
||||
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (event) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
if (currentPane === AccountMenuPane.GeneralMenu) {
|
||||
closeAccountMenu()
|
||||
} else if (currentPane === AccountMenuPane.ConfirmPassword) {
|
||||
setCurrentPane(AccountMenuPane.Register)
|
||||
} else {
|
||||
setCurrentPane(AccountMenuPane.GeneralMenu)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} id="account-menu" className="sn-component">
|
||||
<div
|
||||
className={`sn-menu-border sn-account-menu sn-dropdown ${
|
||||
shouldAnimateCloseMenu ? 'slide-up-animation' : 'sn-dropdown--animated'
|
||||
} min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto absolute`}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<MenuPaneSelector
|
||||
appState={appState}
|
||||
application={application}
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
menuPane={currentPane}
|
||||
setMenuPane={setCurrentPane}
|
||||
closeMenu={closeAccountMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user