refactor: format and lint codebase (#971)

This commit is contained in:
Aman Harwara
2022-04-13 22:02:34 +05:30
committed by GitHub
parent dc9c1ea0fc
commit 8e467f9e6d
367 changed files with 13778 additions and 16093 deletions

View File

@@ -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}
</>
)
},
)

View File

@@ -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>
</>
)
},
)

View 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}
/>
</>
)
},
)

View File

@@ -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">
Youre 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 &amp; 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>
</>
)
},
)

View 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}
/>
</>
)
},
)

View 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

View File

@@ -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>
)
}

View File

@@ -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>
)
},
)

View File

@@ -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>
)}
</>
)
},
)

View 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>
)
},
)