refactor: replace 'preact' with 'react' (#1048)
This commit is contained in:
@@ -22,14 +22,13 @@ declare global {
|
||||
|
||||
import { IsWebPlatform, WebAppVersion } from '@/Version'
|
||||
import { DesktopManagerInterface, SNLog } from '@standardnotes/snjs'
|
||||
import { render } from 'preact'
|
||||
import { ApplicationGroupView } from './Components/ApplicationGroupView/ApplicationGroupView'
|
||||
import ApplicationGroupView from './Components/ApplicationGroupView/ApplicationGroupView'
|
||||
import { WebDevice } from './Device/WebDevice'
|
||||
import { StartApplication } from './Device/StartApplication'
|
||||
import { ApplicationGroup } from './UIModels/ApplicationGroup'
|
||||
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
|
||||
import { WebApplication } from './UIModels/Application'
|
||||
import { unmountComponentAtRoot } from './Utils/PreactUtils'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
let keyCount = 0
|
||||
const getKey = () => {
|
||||
@@ -38,6 +37,11 @@ const getKey = () => {
|
||||
|
||||
const RootId = 'app-group-root'
|
||||
|
||||
const rootElement = document.createElement('div')
|
||||
rootElement.id = RootId
|
||||
const appendedRootNode = document.body.appendChild(rootElement)
|
||||
const root = createRoot(appendedRootNode)
|
||||
|
||||
const startApplication: StartApplication = async function startApplication(
|
||||
defaultSyncServerHost: string,
|
||||
device: WebOrDesktopDevice,
|
||||
@@ -48,19 +52,14 @@ const startApplication: StartApplication = async function startApplication(
|
||||
SNLog.onError = console.error
|
||||
|
||||
const onDestroy = () => {
|
||||
const root = document.getElementById(RootId) as HTMLElement
|
||||
unmountComponentAtRoot(root)
|
||||
root.remove()
|
||||
const rootElement = document.getElementById(RootId) as HTMLElement
|
||||
root.unmount()
|
||||
rootElement.remove()
|
||||
renderApp()
|
||||
}
|
||||
|
||||
const renderApp = () => {
|
||||
const root = document.createElement('div')
|
||||
root.id = RootId
|
||||
|
||||
const parentNode = document.body.appendChild(root)
|
||||
|
||||
render(
|
||||
root.render(
|
||||
<ApplicationGroupView
|
||||
key={getKey()}
|
||||
server={defaultSyncServerHost}
|
||||
@@ -69,7 +68,6 @@ const startApplication: StartApplication = async function startApplication(
|
||||
websocketUrl={webSocketUrl}
|
||||
onDestroy={onDestroy}
|
||||
/>,
|
||||
parentNode,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ import { ApplicationEvent } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState, AppStateEvent } from '@/UIModels/AppState'
|
||||
import { autorun, IReactionDisposer, IReactionPublic } from 'mobx'
|
||||
import { Component } from 'preact'
|
||||
import { findDOMNode, unmountComponentAtNode } from 'preact/compat'
|
||||
|
||||
import { Component } from 'react'
|
||||
export type PureComponentState = Partial<Record<string, any>>
|
||||
export type PureComponentProps = Partial<Record<string, any>>
|
||||
|
||||
@@ -36,20 +34,6 @@ export abstract class PureComponent<P = PureComponentProps, S = PureComponentSta
|
||||
;(this.state as unknown) = undefined
|
||||
}
|
||||
|
||||
protected dismissModal(): void {
|
||||
const elem = this.getElement()
|
||||
if (!elem) {
|
||||
return
|
||||
}
|
||||
|
||||
const parent = elem.parentElement
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
parent.remove()
|
||||
unmountComponentAtNode(parent)
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.deinit()
|
||||
}
|
||||
@@ -58,10 +42,6 @@ export abstract class PureComponent<P = PureComponentProps, S = PureComponentSta
|
||||
return this.application.getAppState()
|
||||
}
|
||||
|
||||
protected getElement(): Element | null {
|
||||
return findDOMNode(this)
|
||||
}
|
||||
|
||||
autorun(view: (r: IReactionPublic) => void): void {
|
||||
this.reactionDisposers.push(autorun(view))
|
||||
}
|
||||
|
||||
@@ -2,15 +2,10 @@ import { observer } from 'mobx-react-lite'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { useCallback, 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 { useCallback, useRef, FunctionComponent, KeyboardEventHandler } from 'react'
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { AccountMenuPane } from './AccountMenuPane'
|
||||
import MenuPaneSelector from './MenuPaneSelector'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
@@ -19,114 +14,61 @@ type Props = {
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
}
|
||||
|
||||
type PaneSelectorProps = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
menuPane: AccountMenuPane
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
closeMenu: () => void
|
||||
const AccountMenu: FunctionComponent<Props> = ({ application, appState, onClickOutside, mainApplicationGroup }) => {
|
||||
const { currentPane, shouldAnimateCloseMenu } = appState.accountMenu
|
||||
|
||||
const closeAccountMenu = useCallback(() => {
|
||||
appState.accountMenu.closeAccountMenu()
|
||||
}, [appState])
|
||||
|
||||
const setCurrentPane = useCallback(
|
||||
(pane: AccountMenuPane) => {
|
||||
appState.accountMenu.setCurrentPane(pane)
|
||||
},
|
||||
[appState],
|
||||
)
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useCloseOnClickOutside(ref, () => {
|
||||
onClickOutside()
|
||||
})
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
if (currentPane === AccountMenuPane.GeneralMenu) {
|
||||
closeAccountMenu()
|
||||
} else if (currentPane === AccountMenuPane.ConfirmPassword) {
|
||||
setCurrentPane(AccountMenuPane.Register)
|
||||
} else {
|
||||
setCurrentPane(AccountMenuPane.GeneralMenu)
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
[closeAccountMenu, currentPane, setCurrentPane],
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={ref} id="account-menu" className="sn-component">
|
||||
<div
|
||||
className={`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>
|
||||
)
|
||||
}
|
||||
|
||||
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, shouldAnimateCloseMenu } = appState.accountMenu
|
||||
|
||||
const closeAccountMenu = useCallback(() => {
|
||||
appState.accountMenu.closeAccountMenu()
|
||||
}, [appState])
|
||||
|
||||
const setCurrentPane = useCallback(
|
||||
(pane: AccountMenuPane) => {
|
||||
appState.accountMenu.setCurrentPane(pane)
|
||||
},
|
||||
[appState],
|
||||
)
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useCloseOnClickOutside(ref, () => {
|
||||
onClickOutside()
|
||||
})
|
||||
|
||||
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
if (currentPane === AccountMenuPane.GeneralMenu) {
|
||||
closeAccountMenu()
|
||||
} else if (currentPane === AccountMenuPane.ConfirmPassword) {
|
||||
setCurrentPane(AccountMenuPane.Register)
|
||||
} else {
|
||||
setCurrentPane(AccountMenuPane.GeneralMenu)
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
[closeAccountMenu, currentPane, setCurrentPane],
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={ref} id="account-menu" className="sn-component">
|
||||
<div
|
||||
className={`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>
|
||||
)
|
||||
},
|
||||
)
|
||||
export default observer(AccountMenu)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||
import { Checkbox } from '@/Components/Checkbox/Checkbox'
|
||||
import { DecoratedInput } from '@/Components/Input/DecoratedInput'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import Checkbox from '@/Components/Checkbox/Checkbox'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -15,170 +14,177 @@ type Props = {
|
||||
onStrictSignInChange?: (isStrictSignIn: boolean) => void
|
||||
}
|
||||
|
||||
export const AdvancedOptions: FunctionComponent<Props> = observer(
|
||||
({ appState, application, disabled = false, onPrivateWorkspaceChange, onStrictSignInChange, children }) => {
|
||||
const { server, setServer, enableServerOption, setEnableServerOption } = appState.accountMenu
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
const AdvancedOptions: FunctionComponent<Props> = ({
|
||||
appState,
|
||||
application,
|
||||
disabled = false,
|
||||
onPrivateWorkspaceChange,
|
||||
onStrictSignInChange,
|
||||
children,
|
||||
}) => {
|
||||
const { server, setServer, enableServerOption, setEnableServerOption } = appState.accountMenu
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
|
||||
const [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false)
|
||||
const [privateWorkspaceName, setPrivateWorkspaceName] = useState('')
|
||||
const [privateWorkspaceUserphrase, setPrivateWorkspaceUserphrase] = useState('')
|
||||
const [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false)
|
||||
const [privateWorkspaceName, setPrivateWorkspaceName] = useState('')
|
||||
const [privateWorkspaceUserphrase, setPrivateWorkspaceUserphrase] = useState('')
|
||||
|
||||
const [isStrictSignin, setIsStrictSignin] = useState(false)
|
||||
const [isStrictSignin, setIsStrictSignin] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const recomputePrivateWorkspaceIdentifier = async () => {
|
||||
const identifier = await application.computePrivateWorkspaceIdentifier(
|
||||
privateWorkspaceName,
|
||||
privateWorkspaceUserphrase,
|
||||
)
|
||||
useEffect(() => {
|
||||
const recomputePrivateWorkspaceIdentifier = async () => {
|
||||
const identifier = await application.computePrivateWorkspaceIdentifier(
|
||||
privateWorkspaceName,
|
||||
privateWorkspaceUserphrase,
|
||||
)
|
||||
|
||||
if (!identifier) {
|
||||
if (privateWorkspaceName?.length > 0 && privateWorkspaceUserphrase?.length > 0) {
|
||||
application.alertService.alert('Unable to compute private workspace name.').catch(console.error)
|
||||
}
|
||||
return
|
||||
if (!identifier) {
|
||||
if (privateWorkspaceName?.length > 0 && privateWorkspaceUserphrase?.length > 0) {
|
||||
application.alertService.alert('Unable to compute private workspace name.').catch(console.error)
|
||||
}
|
||||
onPrivateWorkspaceChange?.(true, identifier)
|
||||
return
|
||||
}
|
||||
onPrivateWorkspaceChange?.(true, identifier)
|
||||
}
|
||||
|
||||
if (privateWorkspaceName && privateWorkspaceUserphrase) {
|
||||
recomputePrivateWorkspaceIdentifier().catch(console.error)
|
||||
if (privateWorkspaceName && privateWorkspaceUserphrase) {
|
||||
recomputePrivateWorkspaceIdentifier().catch(console.error)
|
||||
}
|
||||
}, [privateWorkspaceName, privateWorkspaceUserphrase, application, onPrivateWorkspaceChange])
|
||||
|
||||
useEffect(() => {
|
||||
onPrivateWorkspaceChange?.(isPrivateWorkspace)
|
||||
}, [isPrivateWorkspace, onPrivateWorkspaceChange])
|
||||
|
||||
const handleIsPrivateWorkspaceChange = useCallback(() => {
|
||||
setIsPrivateWorkspace(!isPrivateWorkspace)
|
||||
}, [isPrivateWorkspace])
|
||||
|
||||
const handlePrivateWorkspaceNameChange = useCallback((name: string) => {
|
||||
setPrivateWorkspaceName(name)
|
||||
}, [])
|
||||
|
||||
const handlePrivateWorkspaceUserphraseChange = useCallback((userphrase: string) => {
|
||||
setPrivateWorkspaceUserphrase(userphrase)
|
||||
}, [])
|
||||
|
||||
const handleServerOptionChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEnableServerOption(e.target.checked)
|
||||
}
|
||||
}, [privateWorkspaceName, privateWorkspaceUserphrase, application, onPrivateWorkspaceChange])
|
||||
},
|
||||
[setEnableServerOption],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
onPrivateWorkspaceChange?.(isPrivateWorkspace)
|
||||
}, [isPrivateWorkspace, onPrivateWorkspaceChange])
|
||||
const handleSyncServerChange = useCallback(
|
||||
(server: string) => {
|
||||
setServer(server)
|
||||
application.setCustomHost(server).catch(console.error)
|
||||
},
|
||||
[application, setServer],
|
||||
)
|
||||
|
||||
const handleIsPrivateWorkspaceChange = useCallback(() => {
|
||||
setIsPrivateWorkspace(!isPrivateWorkspace)
|
||||
}, [isPrivateWorkspace])
|
||||
const handleStrictSigninChange = useCallback(() => {
|
||||
const newValue = !isStrictSignin
|
||||
setIsStrictSignin(newValue)
|
||||
onStrictSignInChange?.(newValue)
|
||||
}, [isStrictSignin, onStrictSignInChange])
|
||||
|
||||
const handlePrivateWorkspaceNameChange = useCallback((name: string) => {
|
||||
setPrivateWorkspaceName(name)
|
||||
}, [])
|
||||
const toggleShowAdvanced = useCallback(() => {
|
||||
setShowAdvanced(!showAdvanced)
|
||||
}, [showAdvanced])
|
||||
|
||||
const handlePrivateWorkspaceUserphraseChange = useCallback((userphrase: string) => {
|
||||
setPrivateWorkspaceUserphrase(userphrase)
|
||||
}, [])
|
||||
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-passive-1 ml-1" />
|
||||
</div>
|
||||
</button>
|
||||
{showAdvanced ? (
|
||||
<div className="px-3 my-2">
|
||||
{children}
|
||||
|
||||
const handleServerOptionChange = useCallback(
|
||||
(e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEnableServerOption(e.target.checked)
|
||||
}
|
||||
},
|
||||
[setEnableServerOption],
|
||||
)
|
||||
|
||||
const handleSyncServerChange = useCallback(
|
||||
(server: string) => {
|
||||
setServer(server)
|
||||
application.setCustomHost(server).catch(console.error)
|
||||
},
|
||||
[application, setServer],
|
||||
)
|
||||
|
||||
const handleStrictSigninChange = useCallback(() => {
|
||||
const newValue = !isStrictSignin
|
||||
setIsStrictSignin(newValue)
|
||||
onStrictSignInChange?.(newValue)
|
||||
}, [isStrictSignin, onStrictSignInChange])
|
||||
|
||||
const toggleShowAdvanced = useCallback(() => {
|
||||
setShowAdvanced(!showAdvanced)
|
||||
}, [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-passive-1 ml-1" />
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<Checkbox
|
||||
name="private-workspace"
|
||||
label="Private workspace"
|
||||
checked={isPrivateWorkspace}
|
||||
disabled={disabled}
|
||||
onChange={handleIsPrivateWorkspaceChange}
|
||||
/>
|
||||
<a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more">
|
||||
<Icon type="info" className="color-neutral" />
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
{showAdvanced ? (
|
||||
<div className="px-3 my-2">
|
||||
{children}
|
||||
|
||||
{isPrivateWorkspace && (
|
||||
<>
|
||||
<DecoratedInput
|
||||
className={'mb-2'}
|
||||
left={[<Icon type="server" className="color-neutral" />]}
|
||||
type="text"
|
||||
placeholder="Userphrase"
|
||||
value={privateWorkspaceUserphrase}
|
||||
onChange={handlePrivateWorkspaceUserphraseChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<DecoratedInput
|
||||
className={'mb-2'}
|
||||
left={[<Icon type="folder" className="color-neutral" />]}
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={privateWorkspaceName}
|
||||
onChange={handlePrivateWorkspaceNameChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{onStrictSignInChange && (
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<Checkbox
|
||||
name="private-workspace"
|
||||
label="Private workspace"
|
||||
checked={isPrivateWorkspace}
|
||||
name="use-strict-signin"
|
||||
label="Use strict sign-in"
|
||||
checked={isStrictSignin}
|
||||
disabled={disabled}
|
||||
onChange={handleIsPrivateWorkspaceChange}
|
||||
onChange={handleStrictSigninChange}
|
||||
/>
|
||||
<a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more">
|
||||
<a
|
||||
href="https://standardnotes.com/help/security"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Learn more"
|
||||
>
|
||||
<Icon type="info" className="color-neutral" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPrivateWorkspace && (
|
||||
<>
|
||||
<DecoratedInput
|
||||
className={'mb-2'}
|
||||
left={[<Icon type="server" className="color-neutral" />]}
|
||||
type="text"
|
||||
placeholder="Userphrase"
|
||||
value={privateWorkspaceUserphrase}
|
||||
onChange={handlePrivateWorkspaceUserphraseChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<DecoratedInput
|
||||
className={'mb-2'}
|
||||
left={[<Icon type="folder" className="color-neutral" />]}
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={privateWorkspaceName}
|
||||
onChange={handlePrivateWorkspaceNameChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{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}
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
export default observer(AdvancedOptions)
|
||||
|
||||
@@ -2,14 +2,13 @@ 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 { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AccountMenuPane } from './AccountMenuPane'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Checkbox } from '@/Components/Checkbox/Checkbox'
|
||||
import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { IconButton } from '@/Components/Button/IconButton'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Checkbox from '@/Components/Checkbox/Checkbox'
|
||||
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
@@ -19,140 +18,140 @@ type Props = {
|
||||
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 ConfirmPassword: FunctionComponent<Props> = ({ 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)
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
passwordInputRef.current?.focus()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
passwordInputRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const handlePasswordChange = useCallback((text: string) => {
|
||||
setConfirmPassword(text)
|
||||
}, [])
|
||||
const handlePasswordChange = useCallback((text: string) => {
|
||||
setConfirmPassword(text)
|
||||
}, [])
|
||||
|
||||
const handleEphemeralChange = useCallback(() => {
|
||||
setIsEphemeral(!isEphemeral)
|
||||
}, [isEphemeral])
|
||||
const handleEphemeralChange = useCallback(() => {
|
||||
setIsEphemeral(!isEphemeral)
|
||||
}, [isEphemeral])
|
||||
|
||||
const handleShouldMergeChange = useCallback(() => {
|
||||
setShouldMergeLocal(!shouldMergeLocal)
|
||||
}, [shouldMergeLocal])
|
||||
const handleShouldMergeChange = useCallback(() => {
|
||||
setShouldMergeLocal(!shouldMergeLocal)
|
||||
}, [shouldMergeLocal])
|
||||
|
||||
const handleConfirmFormSubmit = useCallback(
|
||||
(e: Event) => {
|
||||
e.preventDefault()
|
||||
const handleConfirmFormSubmit = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!password) {
|
||||
passwordInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
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()
|
||||
}
|
||||
},
|
||||
[appState, application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
|
||||
)
|
||||
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()
|
||||
}
|
||||
},
|
||||
[appState, application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
|
||||
)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (error.length) {
|
||||
setError('')
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
handleConfirmFormSubmit(e)
|
||||
}
|
||||
},
|
||||
[handleConfirmFormSubmit, error],
|
||||
)
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (error.length) {
|
||||
setError('')
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
handleConfirmFormSubmit(e)
|
||||
}
|
||||
},
|
||||
[handleConfirmFormSubmit, error],
|
||||
)
|
||||
|
||||
const handleGoBack = useCallback(() => {
|
||||
setMenuPane(AccountMenuPane.Register)
|
||||
}, [setMenuPane])
|
||||
const handleGoBack = useCallback(() => {
|
||||
setMenuPane(AccountMenuPane.Register)
|
||||
}, [setMenuPane])
|
||||
|
||||
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 p-0"
|
||||
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-danger">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-danger 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}
|
||||
/>
|
||||
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 p-0"
|
||||
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-danger">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-danger 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="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
checked={!isEphemeral}
|
||||
onChange={handleEphemeralChange}
|
||||
name="should-merge-local"
|
||||
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
|
||||
checked={shouldMergeLocal}
|
||||
onChange={handleShouldMergeChange}
|
||||
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>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
) : null}
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ConfirmPassword)
|
||||
|
||||
@@ -1,140 +1,147 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { StateUpdater, useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AccountMenuPane } from './AccountMenuPane'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { DecoratedInput } from '@/Components/Input/DecoratedInput'
|
||||
import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { IconButton } from '@/Components/Button/IconButton'
|
||||
import { AdvancedOptions } from './AdvancedOptions'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
||||
import Icon from '@/Components/Icon/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>
|
||||
setEmail: React.Dispatch<React.SetStateAction<string>>
|
||||
password: string
|
||||
setPassword: StateUpdater<string>
|
||||
setPassword: React.Dispatch<React.SetStateAction<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 [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false)
|
||||
const CreateAccount: FunctionComponent<Props> = ({
|
||||
appState,
|
||||
application,
|
||||
setMenuPane,
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
}) => {
|
||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
const [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef.current) {
|
||||
useEffect(() => {
|
||||
if (emailInputRef.current) {
|
||||
emailInputRef.current?.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleEmailChange = useCallback(
|
||||
(text: string) => {
|
||||
setEmail(text)
|
||||
},
|
||||
[setEmail],
|
||||
)
|
||||
|
||||
const handlePasswordChange = useCallback(
|
||||
(text: string) => {
|
||||
setPassword(text)
|
||||
},
|
||||
[setPassword],
|
||||
)
|
||||
|
||||
const handleRegisterFormSubmit = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleEmailChange = useCallback(
|
||||
(text: string) => {
|
||||
setEmail(text)
|
||||
},
|
||||
[setEmail],
|
||||
)
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const handlePasswordChange = useCallback(
|
||||
(text: string) => {
|
||||
setPassword(text)
|
||||
},
|
||||
[setPassword],
|
||||
)
|
||||
setEmail(email)
|
||||
setPassword(password)
|
||||
setMenuPane(AccountMenuPane.ConfirmPassword)
|
||||
},
|
||||
[email, password, setPassword, setMenuPane, setEmail],
|
||||
)
|
||||
|
||||
const handleRegisterFormSubmit = useCallback(
|
||||
(e: Event) => {
|
||||
e.preventDefault()
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleRegisterFormSubmit(e)
|
||||
}
|
||||
},
|
||||
[handleRegisterFormSubmit],
|
||||
)
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
const handleClose = useCallback(() => {
|
||||
setMenuPane(AccountMenuPane.GeneralMenu)
|
||||
setEmail('')
|
||||
setPassword('')
|
||||
}, [setEmail, setMenuPane, setPassword])
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
const onPrivateWorkspaceChange = useCallback(
|
||||
(isPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => {
|
||||
setIsPrivateWorkspace(isPrivateWorkspace)
|
||||
if (isPrivateWorkspace && privateWorkspaceIdentifier) {
|
||||
setEmail(privateWorkspaceIdentifier)
|
||||
}
|
||||
},
|
||||
[setEmail],
|
||||
)
|
||||
|
||||
setEmail(email)
|
||||
setPassword(password)
|
||||
setMenuPane(AccountMenuPane.ConfirmPassword)
|
||||
},
|
||||
[email, password, setPassword, setMenuPane, setEmail],
|
||||
)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleRegisterFormSubmit(e)
|
||||
}
|
||||
},
|
||||
[handleRegisterFormSubmit],
|
||||
)
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setMenuPane(AccountMenuPane.GeneralMenu)
|
||||
setEmail('')
|
||||
setPassword('')
|
||||
}, [setEmail, setMenuPane, setPassword])
|
||||
|
||||
const onPrivateWorkspaceChange = useCallback(
|
||||
(isPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => {
|
||||
setIsPrivateWorkspace(isPrivateWorkspace)
|
||||
if (isPrivateWorkspace && privateWorkspaceIdentifier) {
|
||||
setEmail(privateWorkspaceIdentifier)
|
||||
}
|
||||
},
|
||||
[setEmail],
|
||||
)
|
||||
|
||||
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 p-0"
|
||||
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={isPrivateWorkspace}
|
||||
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}
|
||||
onPrivateWorkspaceChange={onPrivateWorkspaceChange}
|
||||
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 p-0"
|
||||
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={isPrivateWorkspace}
|
||||
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}
|
||||
onPrivateWorkspaceChange={onPrivateWorkspaceChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(CreateAccount)
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { formatLastSyncDate } from '@/Components/Preferences/Panes/Account/Sync'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { SyncQueueStrategy } from '@standardnotes/snjs'
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/Strings'
|
||||
import { useCallback, useMemo, useState } from 'preact/hooks'
|
||||
import { useCallback, useMemo, useState, FunctionComponent } from 'react'
|
||||
import { AccountMenuPane } from './AccountMenuPane'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Menu } from '@/Components/Menu/Menu'
|
||||
import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem'
|
||||
import { WorkspaceSwitcherOption } from './WorkspaceSwitcher/WorkspaceSwitcherOption'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import WorkspaceSwitcherOption from './WorkspaceSwitcher/WorkspaceSwitcherOption'
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { formatLastSyncDate } from '@/Utils/FormatLastSyncDate'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
@@ -23,156 +24,162 @@ type Props = {
|
||||
|
||||
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 GeneralAccountMenu: FunctionComponent<Props> = ({
|
||||
application,
|
||||
appState,
|
||||
setMenuPane,
|
||||
closeMenu,
|
||||
mainApplicationGroup,
|
||||
}) => {
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
|
||||
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
|
||||
|
||||
const doSynchronization = useCallback(async () => {
|
||||
setIsSyncingInProgress(true)
|
||||
const doSynchronization = useCallback(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)
|
||||
})
|
||||
}, [application])
|
||||
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)
|
||||
})
|
||||
}, [application])
|
||||
|
||||
const user = useMemo(() => application.getUser(), [application])
|
||||
const user = useMemo(() => application.getUser(), [application])
|
||||
|
||||
const openPreferences = useCallback(() => {
|
||||
appState.accountMenu.closeAccountMenu()
|
||||
appState.preferences.setCurrentPane('account')
|
||||
appState.preferences.openPreferences()
|
||||
}, [appState])
|
||||
const openPreferences = useCallback(() => {
|
||||
appState.accountMenu.closeAccountMenu()
|
||||
appState.preferences.setCurrentPane('account')
|
||||
appState.preferences.openPreferences()
|
||||
}, [appState])
|
||||
|
||||
const openHelp = useCallback(() => {
|
||||
appState.accountMenu.closeAccountMenu()
|
||||
appState.preferences.setCurrentPane('help-feedback')
|
||||
appState.preferences.openPreferences()
|
||||
}, [appState])
|
||||
const openHelp = useCallback(() => {
|
||||
appState.accountMenu.closeAccountMenu()
|
||||
appState.preferences.setCurrentPane('help-feedback')
|
||||
appState.preferences.openPreferences()
|
||||
}, [appState])
|
||||
|
||||
const signOut = useCallback(() => {
|
||||
appState.accountMenu.setSigningOut(true)
|
||||
}, [appState])
|
||||
const signOut = useCallback(() => {
|
||||
appState.accountMenu.setSigningOut(true)
|
||||
}, [appState])
|
||||
|
||||
const activateRegisterPane = useCallback(() => {
|
||||
setMenuPane(AccountMenuPane.Register)
|
||||
}, [setMenuPane])
|
||||
const activateRegisterPane = useCallback(() => {
|
||||
setMenuPane(AccountMenuPane.Register)
|
||||
}, [setMenuPane])
|
||||
|
||||
const activateSignInPane = useCallback(() => {
|
||||
setMenuPane(AccountMenuPane.SignIn)
|
||||
}, [setMenuPane])
|
||||
const activateSignInPane = useCallback(() => {
|
||||
setMenuPane(AccountMenuPane.SignIn)
|
||||
}, [setMenuPane])
|
||||
|
||||
const CREATE_ACCOUNT_INDEX = 1
|
||||
const SWITCHER_INDEX = 0
|
||||
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>
|
||||
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>
|
||||
{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-passive-1" onClick={doSynchronization}>
|
||||
<Icon type="sync" />
|
||||
</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 className="font-semibold success">Last synced:</div>
|
||||
<div className="color-text">{lastSyncDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex cursor-pointer color-passive-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-passive-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={openPreferences}>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Account settings
|
||||
</MenuItem>
|
||||
) : (
|
||||
<>
|
||||
<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-passive-1">
|
||||
<Icon type="cloud-off" className="mr-2" />
|
||||
<span className="font-semibold">Offline</span>
|
||||
</div>
|
||||
</div>
|
||||
<MenuItem type={MenuItemType.IconButton} onClick={activateRegisterPane}>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Create free account
|
||||
</MenuItem>
|
||||
<MenuItem type={MenuItemType.IconButton} onClick={activateSignInPane}>
|
||||
<Icon type="signIn" className={iconClassName} />
|
||||
Sign in
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<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={openPreferences}>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Account settings
|
||||
<MenuItem className="justify-between" type={MenuItemType.IconButton} onClick={openHelp}>
|
||||
<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={signOut}>
|
||||
<Icon type="signOut" className={iconClassName} />
|
||||
Sign out workspace
|
||||
</MenuItem>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem type={MenuItemType.IconButton} onClick={activateRegisterPane}>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Create free account
|
||||
</MenuItem>
|
||||
<MenuItem type={MenuItemType.IconButton} onClick={activateSignInPane}>
|
||||
<Icon type="signIn" className={iconClassName} />
|
||||
Sign in
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<MenuItem className="justify-between" type={MenuItemType.IconButton} onClick={openHelp}>
|
||||
<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={signOut}>
|
||||
<Icon type="signOut" className={iconClassName} />
|
||||
Sign out workspace
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(GeneralAccountMenu)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import { AccountMenuPane } from './AccountMenuPane'
|
||||
import ConfirmPassword from './ConfirmPassword'
|
||||
import CreateAccount from './CreateAccount'
|
||||
import GeneralAccountMenu from './GeneralAccountMenu'
|
||||
import SignInPane from './SignIn'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
menuPane: AccountMenuPane
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
closeMenu: () => void
|
||||
}
|
||||
|
||||
const MenuPaneSelector: FunctionComponent<Props> = ({
|
||||
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 default observer(MenuPaneSelector)
|
||||
@@ -2,16 +2,15 @@ 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 React, { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AccountMenuPane } from './AccountMenuPane'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Checkbox } from '@/Components/Checkbox/Checkbox'
|
||||
import { DecoratedInput } from '@/Components/Input/DecoratedInput'
|
||||
import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { IconButton } from '@/Components/Button/IconButton'
|
||||
import { AdvancedOptions } from './AdvancedOptions'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Checkbox from '@/Components/Checkbox/Checkbox'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
import AdvancedOptions from './AdvancedOptions'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
@@ -19,7 +18,7 @@ type Props = {
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
}
|
||||
|
||||
export const SignInPane: FunctionComponent<Props> = observer(({ application, appState, setMenuPane }) => {
|
||||
const SignInPane: FunctionComponent<Props> = ({ application, appState, setMenuPane }) => {
|
||||
const { notesAndTagsCount } = appState.accountMenu
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
@@ -111,7 +110,7 @@ export const SignInPane: FunctionComponent<Props> = observer(({ application, app
|
||||
)
|
||||
|
||||
const handleSignInFormSubmit = useCallback(
|
||||
(e: Event) => {
|
||||
(e: React.SyntheticEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
@@ -129,8 +128,8 @@ export const SignInPane: FunctionComponent<Props> = observer(({ application, app
|
||||
[email, password, signIn],
|
||||
)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSignInFormSubmit(e)
|
||||
}
|
||||
@@ -210,4 +209,6 @@ export const SignInPane: FunctionComponent<Props> = observer(({ application, app
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(SignInPane)
|
||||
|
||||
@@ -8,7 +8,7 @@ type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const User = observer(({ appState, application }: Props) => {
|
||||
const User = ({ appState, application }: Props) => {
|
||||
const { server } = appState.accountMenu
|
||||
const user = application.getUser() as UserType
|
||||
|
||||
@@ -39,6 +39,6 @@ const User = observer(({ appState, application }: Props) => {
|
||||
<div className="sk-panel-row" />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default User
|
||||
export default observer(User)
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { ApplicationDescriptor } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import {
|
||||
FocusEventHandler,
|
||||
FunctionComponent,
|
||||
KeyboardEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
type Props = {
|
||||
descriptor: ApplicationDescriptor
|
||||
@@ -13,7 +21,7 @@ type Props = {
|
||||
hideOptions: boolean
|
||||
}
|
||||
|
||||
export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||
const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||
descriptor,
|
||||
onClick,
|
||||
onDelete,
|
||||
@@ -29,15 +37,15 @@ export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||
}
|
||||
}, [isRenaming])
|
||||
|
||||
const handleInputKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
const handleInputKeyDown: KeyboardEventHandler = useCallback((event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
inputRef.current?.blur()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleInputBlur = useCallback(
|
||||
(event: FocusEvent) => {
|
||||
const name = (event.target as HTMLInputElement).value
|
||||
const handleInputBlur: FocusEventHandler<HTMLInputElement> = useCallback(
|
||||
(event) => {
|
||||
const name = event.target.value
|
||||
renameDescriptor(name)
|
||||
setIsRenaming(false)
|
||||
},
|
||||
@@ -65,7 +73,8 @@ export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||
)}
|
||||
{descriptor.primary && !hideOptions && (
|
||||
<div>
|
||||
<button
|
||||
<a
|
||||
role="button"
|
||||
className="w-5 h-5 p-0 mr-3 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -73,8 +82,9 @@ export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Icon type="pencil" className="sn-icon--mid color-neutral" />
|
||||
</button>
|
||||
<button
|
||||
</a>
|
||||
<a
|
||||
role="button"
|
||||
className="w-5 h-5 p-0 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -82,10 +92,12 @@ export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className="sn-icon--mid color-danger" />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceMenuItem
|
||||
|
||||
@@ -2,12 +2,13 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { ApplicationDescriptor, ApplicationGroupEvent, ButtonType } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Menu } from '@/Components/Menu/Menu'
|
||||
import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem'
|
||||
import { WorkspaceMenuItem } from './WorkspaceMenuItem'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import WorkspaceMenuItem from './WorkspaceMenuItem'
|
||||
|
||||
type Props = {
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
@@ -16,74 +17,79 @@ type Props = {
|
||||
hideWorkspaceOptions?: boolean
|
||||
}
|
||||
|
||||
export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
|
||||
({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }: Props) => {
|
||||
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([])
|
||||
const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
|
||||
mainApplicationGroup,
|
||||
appState,
|
||||
isOpen,
|
||||
hideWorkspaceOptions = false,
|
||||
}: Props) => {
|
||||
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const applicationDescriptors = mainApplicationGroup.getDescriptors()
|
||||
setApplicationDescriptors(applicationDescriptors)
|
||||
useEffect(() => {
|
||||
const applicationDescriptors = mainApplicationGroup.getDescriptors()
|
||||
setApplicationDescriptors(applicationDescriptors)
|
||||
|
||||
const removeAppGroupObserver = mainApplicationGroup.addEventObserver((event) => {
|
||||
if (event === ApplicationGroupEvent.DescriptorsDataChanged) {
|
||||
const applicationDescriptors = mainApplicationGroup.getDescriptors()
|
||||
setApplicationDescriptors(applicationDescriptors)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeAppGroupObserver()
|
||||
const removeAppGroupObserver = mainApplicationGroup.addEventObserver((event) => {
|
||||
if (event === ApplicationGroupEvent.DescriptorsDataChanged) {
|
||||
const applicationDescriptors = mainApplicationGroup.getDescriptors()
|
||||
setApplicationDescriptors(applicationDescriptors)
|
||||
}
|
||||
}, [mainApplicationGroup])
|
||||
})
|
||||
|
||||
const signoutAll = useCallback(async () => {
|
||||
const confirmed = await appState.application.alertService.confirm(
|
||||
'Are you sure you want to sign out of all workspaces on this device?',
|
||||
undefined,
|
||||
'Sign out all',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
mainApplicationGroup.signOutAllWorkspaces().catch(console.error)
|
||||
}, [mainApplicationGroup, appState])
|
||||
return () => {
|
||||
removeAppGroupObserver()
|
||||
}
|
||||
}, [mainApplicationGroup])
|
||||
|
||||
const destroyWorkspace = useCallback(() => {
|
||||
appState.accountMenu.setSigningOut(true)
|
||||
}, [appState])
|
||||
|
||||
return (
|
||||
<Menu a11yLabel="Workspace switcher menu" className="px-0 focus:shadow-none" isOpen={isOpen}>
|
||||
{applicationDescriptors.map((descriptor) => (
|
||||
<WorkspaceMenuItem
|
||||
key={descriptor.identifier}
|
||||
descriptor={descriptor}
|
||||
hideOptions={hideWorkspaceOptions}
|
||||
onDelete={destroyWorkspace}
|
||||
onClick={() => void mainApplicationGroup.unloadCurrentAndActivateDescriptor(descriptor)}
|
||||
renameDescriptor={(label: string) => mainApplicationGroup.renameDescriptor(descriptor, label)}
|
||||
/>
|
||||
))}
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem
|
||||
type={MenuItemType.IconButton}
|
||||
onClick={() => {
|
||||
void mainApplicationGroup.unloadCurrentAndCreateNewDescriptor()
|
||||
}}
|
||||
>
|
||||
<Icon type="user-add" className="color-neutral mr-2" />
|
||||
Add another workspace
|
||||
</MenuItem>
|
||||
|
||||
{!hideWorkspaceOptions && (
|
||||
<MenuItem type={MenuItemType.IconButton} onClick={signoutAll}>
|
||||
<Icon type="signOut" className="color-neutral mr-2" />
|
||||
Sign out all workspaces
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
const signoutAll = useCallback(async () => {
|
||||
const confirmed = await appState.application.alertService.confirm(
|
||||
'Are you sure you want to sign out of all workspaces on this device?',
|
||||
undefined,
|
||||
'Sign out all',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
},
|
||||
)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
mainApplicationGroup.signOutAllWorkspaces().catch(console.error)
|
||||
}, [mainApplicationGroup, appState])
|
||||
|
||||
const destroyWorkspace = useCallback(() => {
|
||||
appState.accountMenu.setSigningOut(true)
|
||||
}, [appState])
|
||||
|
||||
return (
|
||||
<Menu a11yLabel="Workspace switcher menu" className="px-0 focus:shadow-none" isOpen={isOpen}>
|
||||
{applicationDescriptors.map((descriptor) => (
|
||||
<WorkspaceMenuItem
|
||||
key={descriptor.identifier}
|
||||
descriptor={descriptor}
|
||||
hideOptions={hideWorkspaceOptions}
|
||||
onDelete={destroyWorkspace}
|
||||
onClick={() => void mainApplicationGroup.unloadCurrentAndActivateDescriptor(descriptor)}
|
||||
renameDescriptor={(label: string) => mainApplicationGroup.renameDescriptor(descriptor, label)}
|
||||
/>
|
||||
))}
|
||||
<MenuItemSeparator />
|
||||
|
||||
<MenuItem
|
||||
type={MenuItemType.IconButton}
|
||||
onClick={() => {
|
||||
void mainApplicationGroup.unloadCurrentAndCreateNewDescriptor()
|
||||
}}
|
||||
>
|
||||
<Icon type="user-add" className="color-neutral mr-2" />
|
||||
Add another workspace
|
||||
</MenuItem>
|
||||
|
||||
{!hideWorkspaceOptions && (
|
||||
<MenuItem type={MenuItemType.IconButton} onClick={signoutAll}>
|
||||
<Icon type="signOut" className="color-neutral mr-2" />
|
||||
Sign out all workspaces
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(WorkspaceSwitcherMenu)
|
||||
|
||||
@@ -3,17 +3,16 @@ 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 { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import WorkspaceSwitcherMenu from './WorkspaceSwitcherMenu'
|
||||
|
||||
type Props = {
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(({ mainApplicationGroup, appState }) => {
|
||||
const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGroup, appState }) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@@ -64,4 +63,6 @@ export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(({ mai
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(WorkspaceSwitcherOption)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { Component } from 'preact'
|
||||
import { ApplicationView } from '@/Components/ApplicationView/ApplicationView'
|
||||
import { Component } from 'react'
|
||||
import ApplicationView from '@/Components/ApplicationView/ApplicationView'
|
||||
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
|
||||
import { ApplicationGroupEvent, ApplicationGroupEventData, DeinitSource } from '@standardnotes/snjs'
|
||||
import { unmountComponentAtNode, findDOMNode } from 'preact/compat'
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog'
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
import DeallocateHandler from '../DeallocateHandler/DeallocateHandler'
|
||||
|
||||
type Props = {
|
||||
server: string
|
||||
@@ -23,7 +23,7 @@ type State = {
|
||||
deviceDestroyed?: boolean
|
||||
}
|
||||
|
||||
export class ApplicationGroupView extends Component<Props, State> {
|
||||
class ApplicationGroupView extends Component<Props, State> {
|
||||
applicationObserverRemover?: () => void
|
||||
private group?: ApplicationGroup
|
||||
private application?: WebApplication
|
||||
@@ -74,17 +74,15 @@ export class ApplicationGroupView extends Component<Props, State> {
|
||||
|
||||
const onDestroy = this.props.onDestroy
|
||||
|
||||
const node = findDOMNode(this) as Element
|
||||
unmountComponentAtNode(node)
|
||||
|
||||
onDestroy()
|
||||
}
|
||||
|
||||
render() {
|
||||
override render() {
|
||||
const renderDialog = (message: string) => {
|
||||
return (
|
||||
<DialogOverlay className={'sn-component challenge-modal-overlay'}>
|
||||
<DialogContent
|
||||
aria-label="Switching workspace"
|
||||
className={
|
||||
'challenge-modal flex flex-col items-center bg-default p-8 rounded relative shadow-overlay-light border-1 border-solid border-main'
|
||||
}
|
||||
@@ -116,12 +114,16 @@ export class ApplicationGroupView extends Component<Props, State> {
|
||||
|
||||
return (
|
||||
<div id={this.state.activeApplication.identifier} key={this.state.activeApplication.ephemeralIdentifier}>
|
||||
<ApplicationView
|
||||
key={this.state.activeApplication.ephemeralIdentifier}
|
||||
mainApplicationGroup={this.group}
|
||||
application={this.state.activeApplication}
|
||||
/>
|
||||
<DeallocateHandler application={this.state.activeApplication}>
|
||||
<ApplicationView
|
||||
key={this.state.activeApplication.ephemeralIdentifier}
|
||||
mainApplicationGroup={this.group}
|
||||
application={this.state.activeApplication}
|
||||
/>
|
||||
</DeallocateHandler>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationGroupView
|
||||
|
||||
@@ -1,56 +1,44 @@
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { getPlatformString, getWindowUrlParams } from '@/Utils'
|
||||
import { AppStateEvent, PanelResizedData } from '@/UIModels/AppState'
|
||||
import { ApplicationEvent, Challenge, PermissionDialog, removeFromArray } from '@standardnotes/snjs'
|
||||
import { ApplicationEvent, Challenge, removeFromArray } from '@standardnotes/snjs'
|
||||
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants'
|
||||
import { alertDialog } from '@/Services/AlertService'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { Navigation } from '@/Components/Navigation/Navigation'
|
||||
import { NoteGroupView } from '@/Components/NoteGroupView/NoteGroupView'
|
||||
import { Footer } from '@/Components/Footer/Footer'
|
||||
import { SessionsModal } from '@/Components/SessionsModal/SessionsModal'
|
||||
import { PreferencesViewWrapper } from '@/Components/Preferences/PreferencesViewWrapper'
|
||||
import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal'
|
||||
import { NotesContextMenu } from '@/Components/NotesContextMenu/NotesContextMenu'
|
||||
import { PurchaseFlowWrapper } from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
|
||||
import { render, FunctionComponent } from 'preact'
|
||||
import { PermissionsModal } from '@/Components/PermissionsModal/PermissionsModal'
|
||||
import { RevisionHistoryModalWrapper } from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper'
|
||||
import { PremiumModalProvider } from '@/Hooks/usePremiumModal'
|
||||
import { ConfirmSignoutContainer } from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||
import { TagsContextMenu } from '@/Components/Tags/TagContextMenu'
|
||||
import Navigation from '@/Components/Navigation/Navigation'
|
||||
import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView'
|
||||
import Footer from '@/Components/Footer/Footer'
|
||||
import SessionsModal from '@/Components/SessionsModal/SessionsModal'
|
||||
import PreferencesViewWrapper from '@/Components/Preferences/PreferencesViewWrapper'
|
||||
import ChallengeModal from '@/Components/ChallengeModal/ChallengeModal'
|
||||
import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu'
|
||||
import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import RevisionHistoryModalWrapper from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper'
|
||||
import PremiumModalProvider from '@/Hooks/usePremiumModal'
|
||||
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||
import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu'
|
||||
import { ToastContainer } from '@standardnotes/stylekit'
|
||||
import { FilePreviewModalWrapper } from '@/Components/Files/FilePreviewModal'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
|
||||
import { ContentListView } from '@/Components/ContentListView/ContentListView'
|
||||
import { FileContextMenu } from '@/Components/FileContextMenu/FileContextMenu'
|
||||
import FilePreviewModalWrapper from '@/Components/Files/FilePreviewModal'
|
||||
import ContentListView from '@/Components/ContentListView/ContentListView'
|
||||
import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu'
|
||||
import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
}
|
||||
|
||||
export const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicationGroup }) => {
|
||||
const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicationGroup }) => {
|
||||
const platformString = getPlatformString()
|
||||
const [appClass, setAppClass] = useState('')
|
||||
const [launched, setLaunched] = useState(false)
|
||||
const [needsUnlock, setNeedsUnlock] = useState(true)
|
||||
const [challenges, setChallenges] = useState<Challenge[]>([])
|
||||
const [dealloced, setDealloced] = useState(false)
|
||||
|
||||
const componentManager = application.componentManager
|
||||
const appState = application.getAppState()
|
||||
|
||||
useEffect(() => {
|
||||
setDealloced(application.dealloced)
|
||||
}, [application.dealloced])
|
||||
|
||||
useEffect(() => {
|
||||
if (dealloced) {
|
||||
return
|
||||
}
|
||||
|
||||
const desktopService = application.getDesktopService()
|
||||
|
||||
if (desktopService) {
|
||||
@@ -70,7 +58,7 @@ export const ApplicationView: FunctionComponent<Props> = ({ application, mainApp
|
||||
})
|
||||
.catch(console.error)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [application, dealloced])
|
||||
}, [application])
|
||||
|
||||
const removeChallenge = useCallback(
|
||||
(challenge: Challenge) => {
|
||||
@@ -81,29 +69,9 @@ export const ApplicationView: FunctionComponent<Props> = ({ application, mainApp
|
||||
[challenges],
|
||||
)
|
||||
|
||||
const presentPermissionsDialog = useCallback(
|
||||
(dialog: PermissionDialog) => {
|
||||
render(
|
||||
<PermissionsModal
|
||||
application={application}
|
||||
callback={dialog.callback}
|
||||
component={dialog.component}
|
||||
permissionsString={dialog.permissionsString}
|
||||
/>,
|
||||
document.body.appendChild(document.createElement('div')),
|
||||
)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const onAppStart = useCallback(() => {
|
||||
setNeedsUnlock(application.hasPasscode())
|
||||
componentManager.presentPermissionsDialog = presentPermissionsDialog
|
||||
|
||||
return () => {
|
||||
;(componentManager.presentPermissionsDialog as unknown) = undefined
|
||||
}
|
||||
}, [application, componentManager, presentPermissionsDialog])
|
||||
}, [application])
|
||||
|
||||
const handleDemoSignInFromParams = useCallback(() => {
|
||||
const token = getWindowUrlParams().get('demo-token')
|
||||
@@ -183,7 +151,7 @@ export const ApplicationView: FunctionComponent<Props> = ({ application, mainApp
|
||||
<>
|
||||
{challenges.map((challenge) => {
|
||||
return (
|
||||
<div className="sk-modal">
|
||||
<div className="sk-modal" key={`${challenge.id}${application.ephemeralIdentifier}`}>
|
||||
<ChallengeModal
|
||||
key={`${challenge.id}${application.ephemeralIdentifier}`}
|
||||
application={application}
|
||||
@@ -199,10 +167,6 @@ export const ApplicationView: FunctionComponent<Props> = ({ application, mainApp
|
||||
)
|
||||
}, [appState, challenges, mainApplicationGroup, removeChallenge, application])
|
||||
|
||||
if (dealloced || isStateDealloced(appState)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!renderAppContents) {
|
||||
return renderChallenges()
|
||||
}
|
||||
@@ -227,8 +191,8 @@ export const ApplicationView: FunctionComponent<Props> = ({ application, mainApp
|
||||
|
||||
<>
|
||||
<NotesContextMenu application={application} appState={appState} />
|
||||
<TagsContextMenu appState={appState} />
|
||||
<FileContextMenu appState={appState} />
|
||||
<TagsContextMenuWrapper appState={appState} />
|
||||
<FileContextMenuWrapper appState={appState} />
|
||||
<PurchaseFlowWrapper application={application} appState={appState} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
@@ -237,8 +201,11 @@ export const ApplicationView: FunctionComponent<Props> = ({ application, mainApp
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} appState={appState} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
</>
|
||||
</div>
|
||||
</PremiumModalProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApplicationView
|
||||
|
||||
@@ -4,20 +4,18 @@ import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { ChallengeReason, ContentType, FileItem, SNNote } from '@standardnotes/snjs'
|
||||
import { confirmDialog } from '@/Services/AlertService'
|
||||
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit'
|
||||
import { StreamingFileReader } from '@standardnotes/filepicker'
|
||||
import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
import { AttachedFilesPopover } from './AttachedFilesPopover'
|
||||
import AttachedFilesPopover from './AttachedFilesPopover'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { PopoverTabs } from './PopoverTabs'
|
||||
import { isHandlingFileDrag } from '@/Utils/DragTypeCheck'
|
||||
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -25,128 +23,109 @@ type Props = {
|
||||
onClickPreprocessing?: () => Promise<void>
|
||||
}
|
||||
|
||||
export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
||||
({ application, appState, onClickPreprocessing }: Props) => {
|
||||
if (isStateDealloced(appState)) {
|
||||
return null
|
||||
const AttachedFilesButton: FunctionComponent<Props> = ({ application, appState, onClickPreprocessing }: Props) => {
|
||||
const premiumModal = usePremiumModal()
|
||||
const note: SNNote | undefined = appState.notes.firstSelectedNote
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
})
|
||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen)
|
||||
|
||||
useEffect(() => {
|
||||
if (appState.filePreviewModal.isOpen) {
|
||||
keepMenuOpen(true)
|
||||
} else {
|
||||
keepMenuOpen(false)
|
||||
}
|
||||
}, [appState.filePreviewModal.isOpen, keepMenuOpen])
|
||||
|
||||
const premiumModal = usePremiumModal()
|
||||
const note: SNNote | undefined = appState.notes.firstSelectedNote
|
||||
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles)
|
||||
const [allFiles, setAllFiles] = useState<FileItem[]>([])
|
||||
const [attachedFiles, setAttachedFiles] = useState<FileItem[]>([])
|
||||
const attachedFilesCount = attachedFiles.length
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
useEffect(() => {
|
||||
const unregisterFileStream = application.streamItems(ContentType.File, () => {
|
||||
setAllFiles(application.items.getDisplayableFiles())
|
||||
if (note) {
|
||||
setAttachedFiles(application.items.getFilesForNote(note))
|
||||
}
|
||||
})
|
||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen)
|
||||
|
||||
useEffect(() => {
|
||||
if (appState.filePreviewModal.isOpen) {
|
||||
keepMenuOpen(true)
|
||||
} else {
|
||||
keepMenuOpen(false)
|
||||
return () => {
|
||||
unregisterFileStream()
|
||||
}
|
||||
}, [application, note])
|
||||
|
||||
const toggleAttachedFilesMenu = useCallback(async () => {
|
||||
const rect = buttonRef.current?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
const { clientHeight } = document.documentElement
|
||||
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
|
||||
const footerHeightInPx = footerElementRect?.height
|
||||
|
||||
if (footerHeightInPx) {
|
||||
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
|
||||
}
|
||||
}, [appState.filePreviewModal.isOpen, keepMenuOpen])
|
||||
|
||||
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles)
|
||||
const [allFiles, setAllFiles] = useState<FileItem[]>([])
|
||||
const [attachedFiles, setAttachedFiles] = useState<FileItem[]>([])
|
||||
const attachedFilesCount = attachedFiles.length
|
||||
|
||||
useEffect(() => {
|
||||
const unregisterFileStream = application.streamItems(ContentType.File, () => {
|
||||
setAllFiles(application.items.getDisplayableFiles())
|
||||
if (note) {
|
||||
setAttachedFiles(application.items.getFilesForNote(note))
|
||||
}
|
||||
setPosition({
|
||||
top: rect.bottom,
|
||||
right: document.body.clientWidth - rect.right,
|
||||
})
|
||||
|
||||
return () => {
|
||||
unregisterFileStream()
|
||||
const newOpenState = !open
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
}, [application, note])
|
||||
|
||||
const toggleAttachedFilesMenu = useCallback(async () => {
|
||||
const rect = buttonRef.current?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
const { clientHeight } = document.documentElement
|
||||
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
|
||||
const footerHeightInPx = footerElementRect?.height
|
||||
setOpen(newOpenState)
|
||||
}
|
||||
}, [onClickPreprocessing, open])
|
||||
|
||||
if (footerHeightInPx) {
|
||||
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
|
||||
}
|
||||
const prospectivelyShowFilesPremiumModal = useCallback(() => {
|
||||
if (!appState.features.hasFiles) {
|
||||
premiumModal.activate('Files')
|
||||
}
|
||||
}, [appState.features.hasFiles, premiumModal])
|
||||
|
||||
setPosition({
|
||||
top: rect.bottom,
|
||||
right: document.body.clientWidth - rect.right,
|
||||
})
|
||||
const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => {
|
||||
prospectivelyShowFilesPremiumModal()
|
||||
|
||||
const newOpenState = !open
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
await toggleAttachedFilesMenu()
|
||||
}, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal])
|
||||
|
||||
setOpen(newOpenState)
|
||||
}
|
||||
}, [onClickPreprocessing, open])
|
||||
|
||||
const prospectivelyShowFilesPremiumModal = useCallback(() => {
|
||||
if (!appState.features.hasFiles) {
|
||||
premiumModal.activate('Files')
|
||||
}
|
||||
}, [appState.features.hasFiles, premiumModal])
|
||||
|
||||
const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => {
|
||||
prospectivelyShowFilesPremiumModal()
|
||||
|
||||
await toggleAttachedFilesMenu()
|
||||
}, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal])
|
||||
|
||||
const deleteFile = async (file: FileItem) => {
|
||||
const shouldDelete = await confirmDialog({
|
||||
text: `Are you sure you want to permanently delete "${file.name}"?`,
|
||||
confirmButtonStyle: 'danger',
|
||||
const deleteFile = async (file: FileItem) => {
|
||||
const shouldDelete = await confirmDialog({
|
||||
text: `Are you sure you want to permanently delete "${file.name}"?`,
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
if (shouldDelete) {
|
||||
const deletingToastId = addToast({
|
||||
type: ToastType.Loading,
|
||||
message: `Deleting file "${file.name}"...`,
|
||||
})
|
||||
if (shouldDelete) {
|
||||
const deletingToastId = addToast({
|
||||
type: ToastType.Loading,
|
||||
message: `Deleting file "${file.name}"...`,
|
||||
})
|
||||
await application.files.deleteFile(file)
|
||||
addToast({
|
||||
type: ToastType.Success,
|
||||
message: `Deleted file "${file.name}"`,
|
||||
})
|
||||
dismissToast(deletingToastId)
|
||||
}
|
||||
await application.files.deleteFile(file)
|
||||
addToast({
|
||||
type: ToastType.Success,
|
||||
message: `Deleted file "${file.name}"`,
|
||||
})
|
||||
dismissToast(deletingToastId)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = async (file: FileItem) => {
|
||||
appState.files.downloadFile(file).catch(console.error)
|
||||
}
|
||||
const downloadFile = async (file: FileItem) => {
|
||||
appState.files.downloadFile(file).catch(console.error)
|
||||
}
|
||||
|
||||
const attachFileToNote = useCallback(
|
||||
async (file: FileItem) => {
|
||||
if (!note) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: 'Could not attach file because selected note was deleted',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await application.items.associateFileWithNote(file, note)
|
||||
},
|
||||
[application.items, note],
|
||||
)
|
||||
|
||||
const detachFileFromNote = async (file: FileItem) => {
|
||||
const attachFileToNote = useCallback(
|
||||
async (file: FileItem) => {
|
||||
if (!note) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
@@ -154,268 +133,283 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
||||
})
|
||||
return
|
||||
}
|
||||
await application.items.disassociateFileWithNote(file, note)
|
||||
|
||||
await application.items.associateFileWithNote(file, note)
|
||||
},
|
||||
[application.items, note],
|
||||
)
|
||||
|
||||
const detachFileFromNote = async (file: FileItem) => {
|
||||
if (!note) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: 'Could not attach file because selected note was deleted',
|
||||
})
|
||||
return
|
||||
}
|
||||
await application.items.disassociateFileWithNote(file, note)
|
||||
}
|
||||
|
||||
const toggleFileProtection = async (file: FileItem) => {
|
||||
let result: FileItem | undefined
|
||||
if (file.protected) {
|
||||
keepMenuOpen(true)
|
||||
result = await application.mutator.unprotectFile(file)
|
||||
keepMenuOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
} else {
|
||||
result = await application.mutator.protectFile(file)
|
||||
}
|
||||
const isProtected = result ? result.protected : file.protected
|
||||
return isProtected
|
||||
}
|
||||
|
||||
const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => {
|
||||
const authorizedFiles = await application.protections.authorizeProtectedActionForItems([file], challengeReason)
|
||||
const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file)
|
||||
return isAuthorized
|
||||
}
|
||||
|
||||
const renameFile = async (file: FileItem, fileName: string) => {
|
||||
await application.items.renameFile(file, fileName)
|
||||
}
|
||||
|
||||
const handleFileAction = async (action: PopoverFileItemAction) => {
|
||||
const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file
|
||||
let isAuthorizedForAction = true
|
||||
|
||||
if (file.protected && action.type !== PopoverFileItemActionType.ToggleFileProtection) {
|
||||
keepMenuOpen(true)
|
||||
isAuthorizedForAction = await authorizeProtectedActionForFile(file, ChallengeReason.AccessProtectedFile)
|
||||
keepMenuOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
}
|
||||
|
||||
const toggleFileProtection = async (file: FileItem) => {
|
||||
let result: FileItem | undefined
|
||||
if (file.protected) {
|
||||
if (!isAuthorizedForAction) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case PopoverFileItemActionType.AttachFileToNote:
|
||||
await attachFileToNote(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DetachFileToNote:
|
||||
await detachFileFromNote(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DeleteFile:
|
||||
await deleteFile(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DownloadFile:
|
||||
await downloadFile(file)
|
||||
break
|
||||
case PopoverFileItemActionType.ToggleFileProtection: {
|
||||
const isProtected = await toggleFileProtection(file)
|
||||
action.callback(isProtected)
|
||||
break
|
||||
}
|
||||
case PopoverFileItemActionType.RenameFile:
|
||||
await renameFile(file, action.payload.name)
|
||||
break
|
||||
case PopoverFileItemActionType.PreviewFile: {
|
||||
keepMenuOpen(true)
|
||||
result = await application.mutator.unprotectFile(file)
|
||||
keepMenuOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
} else {
|
||||
result = await application.mutator.protectFile(file)
|
||||
const otherFiles = currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles
|
||||
appState.filePreviewModal.activate(
|
||||
file,
|
||||
otherFiles.filter((file) => !file.protected),
|
||||
)
|
||||
break
|
||||
}
|
||||
const isProtected = result ? result.protected : file.protected
|
||||
return isProtected
|
||||
}
|
||||
|
||||
const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => {
|
||||
const authorizedFiles = await application.protections.authorizeProtectedActionForItems([file], challengeReason)
|
||||
const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file)
|
||||
return isAuthorized
|
||||
if (
|
||||
action.type !== PopoverFileItemActionType.DownloadFile &&
|
||||
action.type !== PopoverFileItemActionType.PreviewFile
|
||||
) {
|
||||
application.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
const renameFile = async (file: FileItem, fileName: string) => {
|
||||
await application.items.renameFile(file, fileName)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleFileAction = async (action: PopoverFileItemAction) => {
|
||||
const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file
|
||||
let isAuthorizedForAction = true
|
||||
|
||||
if (file.protected && action.type !== PopoverFileItemActionType.ToggleFileProtection) {
|
||||
keepMenuOpen(true)
|
||||
isAuthorizedForAction = await authorizeProtectedActionForFile(file, ChallengeReason.AccessProtectedFile)
|
||||
keepMenuOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
}
|
||||
|
||||
if (!isAuthorizedForAction) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case PopoverFileItemActionType.AttachFileToNote:
|
||||
await attachFileToNote(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DetachFileToNote:
|
||||
await detachFileFromNote(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DeleteFile:
|
||||
await deleteFile(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DownloadFile:
|
||||
await downloadFile(file)
|
||||
break
|
||||
case PopoverFileItemActionType.ToggleFileProtection: {
|
||||
const isProtected = await toggleFileProtection(file)
|
||||
action.callback(isProtected)
|
||||
break
|
||||
}
|
||||
case PopoverFileItemActionType.RenameFile:
|
||||
await renameFile(file, action.payload.name)
|
||||
break
|
||||
case PopoverFileItemActionType.PreviewFile: {
|
||||
keepMenuOpen(true)
|
||||
const otherFiles = currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles
|
||||
appState.filePreviewModal.activate(
|
||||
file,
|
||||
otherFiles.filter((file) => !file.protected),
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
action.type !== PopoverFileItemActionType.DownloadFile &&
|
||||
action.type !== PopoverFileItemActionType.PreviewFile
|
||||
) {
|
||||
application.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const [isDraggingFiles, setIsDraggingFiles] = useState(false)
|
||||
const dragCounter = useRef(0)
|
||||
|
||||
const handleDrag = useCallback(
|
||||
(event: DragEvent) => {
|
||||
if (isHandlingFileDrag(event, application)) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const handleDragIn = useCallback(
|
||||
(event: DragEvent) => {
|
||||
if (!isHandlingFileDrag(event, application)) {
|
||||
return
|
||||
}
|
||||
const [isDraggingFiles, setIsDraggingFiles] = useState(false)
|
||||
const dragCounter = useRef(0)
|
||||
|
||||
const handleDrag = useCallback(
|
||||
(event: DragEvent) => {
|
||||
if (isHandlingFileDrag(event, application)) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
switch ((event.target as HTMLElement).id) {
|
||||
case PopoverTabs.AllFiles:
|
||||
setCurrentTab(PopoverTabs.AllFiles)
|
||||
break
|
||||
case PopoverTabs.AttachedFiles:
|
||||
setCurrentTab(PopoverTabs.AttachedFiles)
|
||||
break
|
||||
const handleDragIn = useCallback(
|
||||
(event: DragEvent) => {
|
||||
if (!isHandlingFileDrag(event, application)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
switch ((event.target as HTMLElement).id) {
|
||||
case PopoverTabs.AllFiles:
|
||||
setCurrentTab(PopoverTabs.AllFiles)
|
||||
break
|
||||
case PopoverTabs.AttachedFiles:
|
||||
setCurrentTab(PopoverTabs.AttachedFiles)
|
||||
break
|
||||
}
|
||||
|
||||
dragCounter.current = dragCounter.current + 1
|
||||
|
||||
if (event.dataTransfer?.items.length) {
|
||||
setIsDraggingFiles(true)
|
||||
if (!open) {
|
||||
toggleAttachedFilesMenu().catch(console.error)
|
||||
}
|
||||
}
|
||||
},
|
||||
[open, toggleAttachedFilesMenu, application],
|
||||
)
|
||||
|
||||
dragCounter.current = dragCounter.current + 1
|
||||
const handleDragOut = useCallback(
|
||||
(event: DragEvent) => {
|
||||
if (!isHandlingFileDrag(event, application)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.dataTransfer?.items.length) {
|
||||
setIsDraggingFiles(true)
|
||||
if (!open) {
|
||||
toggleAttachedFilesMenu().catch(console.error)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
dragCounter.current = dragCounter.current - 1
|
||||
|
||||
if (dragCounter.current > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsDraggingFiles(false)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(event: DragEvent) => {
|
||||
if (!isHandlingFileDrag(event, application)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
setIsDraggingFiles(false)
|
||||
|
||||
if (!appState.features.hasFiles) {
|
||||
prospectivelyShowFilesPremiumModal()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.dataTransfer?.items.length) {
|
||||
Array.from(event.dataTransfer.items).forEach(async (item) => {
|
||||
const fileOrHandle = StreamingFileReader.available()
|
||||
? ((await item.getAsFileSystemHandle()) as FileSystemFileHandle)
|
||||
: item.getAsFile()
|
||||
|
||||
if (!fileOrHandle) {
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
[open, toggleAttachedFilesMenu, application],
|
||||
)
|
||||
|
||||
const handleDragOut = useCallback(
|
||||
(event: DragEvent) => {
|
||||
if (!isHandlingFileDrag(event, application)) {
|
||||
return
|
||||
}
|
||||
const uploadedFiles = await appState.files.uploadNewFile(fileOrHandle)
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!uploadedFiles) {
|
||||
return
|
||||
}
|
||||
|
||||
dragCounter.current = dragCounter.current - 1
|
||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||
uploadedFiles.forEach((file) => {
|
||||
attachFileToNote(file).catch(console.error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (dragCounter.current > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsDraggingFiles(false)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(event: DragEvent) => {
|
||||
if (!isHandlingFileDrag(event, application)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
setIsDraggingFiles(false)
|
||||
|
||||
if (!appState.features.hasFiles) {
|
||||
prospectivelyShowFilesPremiumModal()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.dataTransfer?.items.length) {
|
||||
Array.from(event.dataTransfer.items).forEach(async (item) => {
|
||||
const fileOrHandle = StreamingFileReader.available()
|
||||
? ((await item.getAsFileSystemHandle()) as FileSystemFileHandle)
|
||||
: item.getAsFile()
|
||||
|
||||
if (!fileOrHandle) {
|
||||
return
|
||||
}
|
||||
|
||||
const uploadedFiles = await appState.files.uploadNewFile(fileOrHandle)
|
||||
|
||||
if (!uploadedFiles) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||
uploadedFiles.forEach((file) => {
|
||||
attachFileToNote(file).catch(console.error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
event.dataTransfer.clearData()
|
||||
dragCounter.current = 0
|
||||
}
|
||||
},
|
||||
[
|
||||
appState.files,
|
||||
appState.features.hasFiles,
|
||||
attachFileToNote,
|
||||
currentTab,
|
||||
application,
|
||||
prospectivelyShowFilesPremiumModal,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('dragenter', handleDragIn)
|
||||
window.addEventListener('dragleave', handleDragOut)
|
||||
window.addEventListener('dragover', handleDrag)
|
||||
window.addEventListener('drop', handleDrop)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('dragenter', handleDragIn)
|
||||
window.removeEventListener('dragleave', handleDragOut)
|
||||
window.removeEventListener('dragover', handleDrag)
|
||||
window.removeEventListener('drop', handleDrop)
|
||||
event.dataTransfer.clearData()
|
||||
dragCounter.current = 0
|
||||
}
|
||||
}, [handleDragIn, handleDrop, handleDrag, handleDragOut])
|
||||
},
|
||||
[
|
||||
appState.files,
|
||||
appState.features.hasFiles,
|
||||
attachFileToNote,
|
||||
currentTab,
|
||||
application,
|
||||
prospectivelyShowFilesPremiumModal,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<Disclosure open={open} onChange={toggleAttachedFilesMenuWithEntitlementCheck}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
ref={buttonRef}
|
||||
className={`sn-icon-button border-contrast ${attachedFilesCount > 0 ? 'py-1 px-3' : ''}`}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<VisuallyHidden>Attached files</VisuallyHidden>
|
||||
<Icon type="attachment-file" className="block" />
|
||||
{attachedFilesCount > 0 && <span className="ml-2">{attachedFilesCount}</span>}
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight,
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
{open && (
|
||||
<AttachedFilesPopover
|
||||
application={application}
|
||||
appState={appState}
|
||||
attachedFiles={attachedFiles}
|
||||
allFiles={allFiles}
|
||||
closeOnBlur={closeOnBlur}
|
||||
currentTab={currentTab}
|
||||
handleFileAction={handleFileAction}
|
||||
isDraggingFiles={isDraggingFiles}
|
||||
setCurrentTab={setCurrentTab}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
useEffect(() => {
|
||||
window.addEventListener('dragenter', handleDragIn)
|
||||
window.addEventListener('dragleave', handleDragOut)
|
||||
window.addEventListener('dragover', handleDrag)
|
||||
window.addEventListener('drop', handleDrop)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('dragenter', handleDragIn)
|
||||
window.removeEventListener('dragleave', handleDragOut)
|
||||
window.removeEventListener('dragover', handleDrag)
|
||||
window.removeEventListener('drop', handleDrop)
|
||||
}
|
||||
}, [handleDragIn, handleDrop, handleDrag, handleDragOut])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<Disclosure open={open} onChange={toggleAttachedFilesMenuWithEntitlementCheck}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
ref={buttonRef}
|
||||
className={`sn-icon-button border-contrast ${attachedFilesCount > 0 ? 'py-1 px-3' : ''}`}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<VisuallyHidden>Attached files</VisuallyHidden>
|
||||
<Icon type="attachment-file" className="block" />
|
||||
{attachedFilesCount > 0 && <span className="ml-2">{attachedFilesCount}</span>}
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight,
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
{open && (
|
||||
<AttachedFilesPopover
|
||||
application={application}
|
||||
appState={appState}
|
||||
attachedFiles={attachedFiles}
|
||||
allFiles={allFiles}
|
||||
closeOnBlur={closeOnBlur}
|
||||
currentTab={currentTab}
|
||||
handleFileAction={handleFileAction}
|
||||
isDraggingFiles={isDraggingFiles}
|
||||
setCurrentTab={setCurrentTab}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(AttachedFilesButton)
|
||||
|
||||
@@ -4,11 +4,10 @@ import { AppState } from '@/UIModels/AppState'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FilesIllustration } from '@standardnotes/icons'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { StateUpdater, useRef, useState } from 'preact/hooks'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { PopoverFileItem } from './PopoverFileItem'
|
||||
import { Dispatch, FunctionComponent, SetStateAction, useRef, useState } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import PopoverFileItem from './PopoverFileItem'
|
||||
import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
import { PopoverTabs } from './PopoverTabs'
|
||||
|
||||
@@ -21,153 +20,153 @@ type Props = {
|
||||
currentTab: PopoverTabs
|
||||
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
|
||||
isDraggingFiles: boolean
|
||||
setCurrentTab: StateUpdater<PopoverTabs>
|
||||
setCurrentTab: Dispatch<SetStateAction<PopoverTabs>>
|
||||
}
|
||||
|
||||
export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
||||
({
|
||||
application,
|
||||
appState,
|
||||
allFiles,
|
||||
attachedFiles,
|
||||
closeOnBlur,
|
||||
currentTab,
|
||||
handleFileAction,
|
||||
isDraggingFiles,
|
||||
setCurrentTab,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
application,
|
||||
appState,
|
||||
allFiles,
|
||||
attachedFiles,
|
||||
closeOnBlur,
|
||||
currentTab,
|
||||
handleFileAction,
|
||||
isDraggingFiles,
|
||||
setCurrentTab,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const filesList = currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles
|
||||
const filesList = currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles
|
||||
|
||||
const filteredList =
|
||||
searchQuery.length > 0
|
||||
? filesList.filter((file) => file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1)
|
||||
: filesList
|
||||
const filteredList =
|
||||
searchQuery.length > 0
|
||||
? filesList.filter((file) => file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1)
|
||||
: filesList
|
||||
|
||||
const handleAttachFilesClick = async () => {
|
||||
const uploadedFiles = await appState.files.uploadNewFile()
|
||||
if (!uploadedFiles) {
|
||||
return
|
||||
}
|
||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||
uploadedFiles.forEach((file) => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.AttachFileToNote,
|
||||
payload: file,
|
||||
}).catch(console.error)
|
||||
})
|
||||
}
|
||||
const handleAttachFilesClick = async () => {
|
||||
const uploadedFiles = await appState.files.uploadNewFile()
|
||||
if (!uploadedFiles) {
|
||||
return
|
||||
}
|
||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||
uploadedFiles.forEach((file) => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.AttachFileToNote,
|
||||
payload: file,
|
||||
}).catch(console.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
style={{
|
||||
border: isDraggingFiles ? '2px dashed var(--sn-stylekit-info-color)' : '',
|
||||
}}
|
||||
>
|
||||
<div className="flex border-0 border-b-1 border-solid border-main">
|
||||
<button
|
||||
id={PopoverTabs.AttachedFiles}
|
||||
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
|
||||
currentTab === PopoverTabs.AttachedFiles ? 'color-info font-medium shadow-bottom' : 'color-text'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCurrentTab(PopoverTabs.AttachedFiles)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Attached
|
||||
</button>
|
||||
<button
|
||||
id={PopoverTabs.AllFiles}
|
||||
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
|
||||
currentTab === PopoverTabs.AllFiles ? 'color-info font-medium shadow-bottom' : 'color-text'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCurrentTab(PopoverTabs.AllFiles)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
All files
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 max-h-110 overflow-y-auto">
|
||||
{filteredList.length > 0 || searchQuery.length > 0 ? (
|
||||
<div className="sticky top-0 left-0 p-3 bg-default border-0 border-b-1 border-solid border-main">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
className="color-text w-full rounded py-1.5 px-3 text-input bg-default border-solid border-1 border-main"
|
||||
placeholder="Search files..."
|
||||
value={searchQuery}
|
||||
onInput={(e) => {
|
||||
setSearchQuery((e.target as HTMLInputElement).value)
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
style={{
|
||||
border: isDraggingFiles ? '2px dashed var(--sn-stylekit-info-color)' : '',
|
||||
}}
|
||||
>
|
||||
<div className="flex border-0 border-b-1 border-solid border-main">
|
||||
<button
|
||||
id={PopoverTabs.AttachedFiles}
|
||||
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
|
||||
currentTab === PopoverTabs.AttachedFiles ? 'color-info font-medium shadow-bottom' : 'color-text'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCurrentTab(PopoverTabs.AttachedFiles)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Attached
|
||||
</button>
|
||||
<button
|
||||
id={PopoverTabs.AllFiles}
|
||||
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
|
||||
currentTab === PopoverTabs.AllFiles ? 'color-info font-medium shadow-bottom' : 'color-text'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCurrentTab(PopoverTabs.AllFiles)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
All files
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 max-h-110 overflow-y-auto">
|
||||
{filteredList.length > 0 || searchQuery.length > 0 ? (
|
||||
<div className="sticky top-0 left-0 p-3 bg-default border-0 border-b-1 border-solid border-main">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
className="color-text w-full rounded py-1.5 px-3 text-input bg-default border-solid border-1 border-main"
|
||||
placeholder="Search files..."
|
||||
value={searchQuery}
|
||||
onInput={(e) => {
|
||||
setSearchQuery((e.target as HTMLInputElement).value)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={searchInputRef}
|
||||
/>
|
||||
{searchQuery.length > 0 && (
|
||||
<button
|
||||
className="flex absolute right-2 p-0 bg-transparent border-0 top-1/2 -translate-y-1/2 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSearchQuery('')
|
||||
searchInputRef.current?.focus()
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={searchInputRef}
|
||||
/>
|
||||
{searchQuery.length > 0 && (
|
||||
<button
|
||||
className="flex absolute right-2 p-0 bg-transparent border-0 top-1/2 -translate-y-1/2 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSearchQuery('')
|
||||
searchInputRef.current?.focus()
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<Icon type="clear-circle-filled" className="color-neutral" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<Icon type="clear-circle-filled" className="color-neutral" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{filteredList.length > 0 ? (
|
||||
filteredList.map((file: FileItem) => {
|
||||
return (
|
||||
<PopoverFileItem
|
||||
key={file.uuid}
|
||||
file={file}
|
||||
isAttachedToNote={attachedFiles.includes(file)}
|
||||
handleFileAction={handleFileAction}
|
||||
getIconType={application.iconsController.getIconForFileType}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center w-full py-8">
|
||||
<div className="w-18 h-18 mb-2">
|
||||
<FilesIllustration />
|
||||
</div>
|
||||
<div className="text-sm font-medium mb-3">
|
||||
{searchQuery.length > 0
|
||||
? 'No result found'
|
||||
: currentTab === PopoverTabs.AttachedFiles
|
||||
? 'No files attached to this note'
|
||||
: 'No files found in this account'}
|
||||
</div>
|
||||
<Button variant="normal" onClick={handleAttachFilesClick} onBlur={closeOnBlur}>
|
||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
|
||||
</Button>
|
||||
<div className="text-xs color-passive-0 mt-3">Or drop your files here</div>
|
||||
</div>
|
||||
) : null}
|
||||
{filteredList.length > 0 ? (
|
||||
filteredList.map((file: FileItem) => {
|
||||
return (
|
||||
<PopoverFileItem
|
||||
key={file.uuid}
|
||||
file={file}
|
||||
isAttachedToNote={attachedFiles.includes(file)}
|
||||
handleFileAction={handleFileAction}
|
||||
getIconType={application.iconsController.getIconForFileType}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center w-full py-8">
|
||||
<div className="w-18 h-18 mb-2">
|
||||
<FilesIllustration />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{filteredList.length > 0 && (
|
||||
<button
|
||||
className="sn-dropdown-item py-3 border-0 border-t-1px border-solid border-main focus:bg-info-backdrop"
|
||||
onClick={handleAttachFilesClick}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<Icon type="add" className="mr-2 color-neutral" />
|
||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
|
||||
</button>
|
||||
<div className="text-sm font-medium mb-3">
|
||||
{searchQuery.length > 0
|
||||
? 'No result found'
|
||||
: currentTab === PopoverTabs.AttachedFiles
|
||||
? 'No files attached to this note'
|
||||
: 'No files found in this account'}
|
||||
</div>
|
||||
<Button variant="normal" onClick={handleAttachFilesClick} onBlur={closeOnBlur}>
|
||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
|
||||
</Button>
|
||||
<div className="text-xs color-passive-0 mt-3">Or drop your files here</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
{filteredList.length > 0 && (
|
||||
<button
|
||||
className="sn-dropdown-item py-3 border-0 border-t-1px border-solid border-main focus:bg-info-backdrop"
|
||||
onClick={handleAttachFilesClick}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<Icon type="add" className="mr-2 color-neutral" />
|
||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(AttachedFilesPopover)
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { IconType, FileItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon, ICONS } from '@/Components/Icon/Icon'
|
||||
import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
import { PopoverFileSubmenu } from './PopoverFileSubmenu'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FormEventHandler, FunctionComponent, KeyboardEventHandler, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
import PopoverFileSubmenu from './PopoverFileSubmenu'
|
||||
import { getFileIconComponent } from './getFileIconComponent'
|
||||
import { PopoverFileItemProps } from './PopoverFileItemProps'
|
||||
|
||||
export const getFileIconComponent = (iconType: string, className: string) => {
|
||||
const IconComponent = ICONS[iconType as keyof typeof ICONS]
|
||||
|
||||
return <IconComponent className={className} />
|
||||
}
|
||||
|
||||
export type PopoverFileItemProps = {
|
||||
file: FileItem
|
||||
isAttachedToNote: boolean
|
||||
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
|
||||
getIconType(type: string): IconType
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
}
|
||||
|
||||
export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
||||
const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
||||
file,
|
||||
isAttachedToNote,
|
||||
handleFileAction,
|
||||
@@ -51,11 +38,11 @@ export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
||||
setIsRenamingFile(false)
|
||||
}
|
||||
|
||||
const handleFileNameInput = (event: Event) => {
|
||||
const handleFileNameInput: FormEventHandler<HTMLInputElement> = (event) => {
|
||||
setFileName((event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
const handleFileNameInputKeyDown = (event: KeyboardEvent) => {
|
||||
const handleFileNameInputKeyDown: KeyboardEventHandler = (event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
itemRef.current?.focus()
|
||||
}
|
||||
@@ -115,3 +102,5 @@ export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PopoverFileItem
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IconType, FileItem } from '@standardnotes/snjs'
|
||||
import { PopoverFileItemAction } from './PopoverFileItemAction'
|
||||
|
||||
export type PopoverFileItemProps = {
|
||||
file: FileItem
|
||||
isAttachedToNote: boolean
|
||||
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
|
||||
getIconType(type: string): IconType
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { StateUpdater, useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { Dispatch, FunctionComponent, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { PopoverFileItemProps } from './PopoverFileItem'
|
||||
import { PopoverFileItemProps } from './PopoverFileItemProps'
|
||||
import { PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
|
||||
type Props = Omit<PopoverFileItemProps, 'renameFile' | 'getIconType'> & {
|
||||
setIsRenamingFile: StateUpdater<boolean>
|
||||
setIsRenamingFile: Dispatch<SetStateAction<boolean>>
|
||||
previewHandler: () => void
|
||||
}
|
||||
|
||||
export const PopoverFileSubmenu: FunctionComponent<Props> = ({
|
||||
const PopoverFileSubmenu: FunctionComponent<Props> = ({
|
||||
file,
|
||||
isAttachedToNote,
|
||||
handleFileAction,
|
||||
@@ -197,3 +196,5 @@ export const PopoverFileSubmenu: FunctionComponent<Props> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PopoverFileSubmenu
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ICONS } from '@/Components/Icon/Icon'
|
||||
|
||||
export const getFileIconComponent = (iconType: string, className: string) => {
|
||||
const IconComponent = ICONS[iconType as keyof typeof ICONS]
|
||||
|
||||
return <IconComponent className={className} />
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
interface BubbleProperties {
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
type Props = {
|
||||
label: string
|
||||
selected: boolean
|
||||
onSelect: () => void
|
||||
@@ -10,7 +12,7 @@ const styles = {
|
||||
selected: 'border-info bg-info color-neutral-contrast',
|
||||
}
|
||||
|
||||
const Bubble = ({ label, selected, onSelect }: BubbleProperties) => (
|
||||
const Bubble: FunctionComponent<Props> = ({ label, selected, onSelect }) => (
|
||||
<span
|
||||
role="tab"
|
||||
className={`bubble ${styles.base} ${selected ? styles.selected : styles.unselected}`}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { ComponentChildren, FunctionComponent, Ref } from 'preact'
|
||||
import { forwardRef } from 'preact/compat'
|
||||
import { Ref, forwardRef, ReactNode, ComponentPropsWithoutRef } from 'react'
|
||||
|
||||
const baseClass = 'rounded px-4 py-1.75 font-bold text-sm fit-content'
|
||||
|
||||
@@ -32,15 +30,15 @@ const getClassName = (variant: ButtonVariant, danger: boolean, disabled: boolean
|
||||
return `${baseClass} ${colors} ${borders} ${focusHoverStates} ${cursor}`
|
||||
}
|
||||
|
||||
type ButtonProps = JSXInternal.HTMLAttributes<HTMLButtonElement> & {
|
||||
children?: ComponentChildren
|
||||
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
variant?: ButtonVariant
|
||||
dangerStyle?: boolean
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const Button: FunctionComponent<ButtonProps> = forwardRef(
|
||||
const Button = forwardRef(
|
||||
(
|
||||
{
|
||||
variant = 'normal',
|
||||
@@ -66,3 +64,5 @@ export const Button: FunctionComponent<ButtonProps> = forwardRef(
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default Button
|
||||
|
||||
@@ -1,34 +1,18 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent, MouseEventHandler } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* onClick - preventDefault is handled within the component
|
||||
*/
|
||||
type Props = {
|
||||
onClick: () => void
|
||||
|
||||
className?: string
|
||||
|
||||
icon: IconType
|
||||
|
||||
iconClassName?: string
|
||||
|
||||
/**
|
||||
* Button tooltip
|
||||
*/
|
||||
title: string
|
||||
|
||||
focusable: boolean
|
||||
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* IconButton component with an icon
|
||||
* preventDefault is already handled within the component
|
||||
*/
|
||||
export const IconButton: FunctionComponent<Props> = ({
|
||||
const IconButton: FunctionComponent<Props> = ({
|
||||
onClick,
|
||||
className = '',
|
||||
icon,
|
||||
@@ -37,7 +21,7 @@ export const IconButton: FunctionComponent<Props> = ({
|
||||
iconClassName = '',
|
||||
disabled = false,
|
||||
}) => {
|
||||
const click = (e: MouseEvent) => {
|
||||
const click: MouseEventHandler = (e) => {
|
||||
e.preventDefault()
|
||||
onClick()
|
||||
}
|
||||
@@ -55,3 +39,5 @@ export const IconButton: FunctionComponent<Props> = ({
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconButton
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent, MouseEventHandler } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
|
||||
type ButtonType = 'normal' | 'primary'
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* onClick - preventDefault is handled within the component
|
||||
*/
|
||||
type Props = {
|
||||
onClick: () => void
|
||||
|
||||
type: ButtonType
|
||||
|
||||
className?: string
|
||||
|
||||
icon: IconType
|
||||
}
|
||||
|
||||
/**
|
||||
* IconButton component with an icon
|
||||
* preventDefault is already handled within the component
|
||||
*/
|
||||
export const RoundIconButton: FunctionComponent<Props> = ({ onClick, type, className, icon: iconType }) => {
|
||||
const click = (e: MouseEvent) => {
|
||||
const RoundIconButton: FunctionComponent<Props> = ({ onClick, type, className, icon: iconType }) => {
|
||||
const click: MouseEventHandler = (e) => {
|
||||
e.preventDefault()
|
||||
onClick()
|
||||
}
|
||||
@@ -33,3 +23,5 @@ export const RoundIconButton: FunctionComponent<Props> = ({ onClick, type, class
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoundIconButton
|
||||
|
||||
@@ -9,22 +9,14 @@ import {
|
||||
removeFromArray,
|
||||
} from '@standardnotes/snjs'
|
||||
import { ProtectedIllustration } from '@standardnotes/icons'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { ChallengeModalPrompt } from './ChallengePrompt'
|
||||
import { LockscreenWorkspaceSwitcher } from './LockscreenWorkspaceSwitcher'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ChallengeModalPrompt from './ChallengePrompt'
|
||||
import LockscreenWorkspaceSwitcher from './LockscreenWorkspaceSwitcher'
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
|
||||
type InputValue = {
|
||||
prompt: ChallengePrompt
|
||||
value: string | number | boolean
|
||||
invalid: boolean
|
||||
}
|
||||
|
||||
export type ChallengeModalValues = Record<ChallengePrompt['id'], InputValue>
|
||||
import { ChallengeModalValues } from './ChallengeModalValues'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -50,7 +42,7 @@ const validateValues = (values: ChallengeModalValues, prompts: ChallengePrompt[]
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const ChallengeModal: FunctionComponent<Props> = ({
|
||||
const ChallengeModal: FunctionComponent<Props> = ({
|
||||
application,
|
||||
appState,
|
||||
mainApplicationGroup,
|
||||
@@ -191,6 +183,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
|
||||
key={challenge.id}
|
||||
>
|
||||
<DialogContent
|
||||
aria-label="Challenge modal"
|
||||
className={`challenge-modal flex flex-col items-center bg-default p-8 rounded relative ${
|
||||
challenge.reason !== ChallengeReason.ApplicationUnlock
|
||||
? 'shadow-overlay-light border-1 border-solid border-main'
|
||||
@@ -268,3 +261,5 @@ export const ChallengeModal: FunctionComponent<Props> = ({
|
||||
</DialogOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChallengeModal
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ChallengePrompt } from '@standardnotes/snjs'
|
||||
import { InputValue } from './InputValue'
|
||||
|
||||
export type ChallengeModalValues = Record<ChallengePrompt['id'], InputValue>
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ChallengePrompt, ChallengeValidation, ProtectionSessionDurations } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
import { DecoratedInput } from '@/Components/Input/DecoratedInput'
|
||||
import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { ChallengeModalValues } from './ChallengeModal'
|
||||
import { FunctionComponent, useEffect, useRef } from 'react'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { ChallengeModalValues } from './ChallengeModalValues'
|
||||
|
||||
type Props = {
|
||||
prompt: ChallengePrompt
|
||||
@@ -13,7 +12,7 @@ type Props = {
|
||||
isInvalid: boolean
|
||||
}
|
||||
|
||||
export const ChallengeModalPrompt: FunctionComponent<Props> = ({ prompt, values, index, onValueChange, isInvalid }) => {
|
||||
const ChallengeModalPrompt: FunctionComponent<Props> = ({ prompt, values, index, onValueChange, isInvalid }) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -38,6 +37,7 @@ export const ChallengeModalPrompt: FunctionComponent<Props> = ({ prompt, values,
|
||||
const selected = option.valueInSeconds === values[prompt.id].value
|
||||
return (
|
||||
<label
|
||||
key={option.label}
|
||||
className={`cursor-pointer px-2 py-1.5 rounded ${
|
||||
selected ? 'bg-default color-foreground font-semibold' : 'color-passive-0 hover:bg-passive-3'
|
||||
}`}
|
||||
@@ -80,3 +80,5 @@ export const ChallengeModalPrompt: FunctionComponent<Props> = ({ prompt, values,
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChallengeModalPrompt
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ChallengePrompt } from '@standardnotes/snjs'
|
||||
|
||||
export type InputValue = {
|
||||
prompt: ChallengePrompt
|
||||
value: string | number | boolean
|
||||
invalid: boolean
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { WorkspaceSwitcherMenu } from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import WorkspaceSwitcherMenu from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
|
||||
type Props = {
|
||||
@@ -13,7 +12,7 @@ type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplicationGroup, appState }) => {
|
||||
const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplicationGroup, appState }) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -65,3 +64,5 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainAppl
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LockscreenWorkspaceSwitcher
|
||||
|
||||
@@ -4,12 +4,10 @@ import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { ChangeEditorMenu } from './ChangeEditorMenu'
|
||||
import { FunctionComponent, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ChangeEditorMenu from './ChangeEditorMenu'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -17,98 +15,94 @@ type Props = {
|
||||
onClickPreprocessing?: () => Promise<void>
|
||||
}
|
||||
|
||||
export const ChangeEditorButton: FunctionComponent<Props> = observer(
|
||||
({ application, appState, onClickPreprocessing }: Props) => {
|
||||
if (isStateDealloced(appState)) {
|
||||
return null
|
||||
}
|
||||
const ChangeEditorButton: FunctionComponent<Props> = ({ application, appState, onClickPreprocessing }: Props) => {
|
||||
const note = appState.notes.firstSelectedNote
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
})
|
||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen)
|
||||
|
||||
const note = appState.notes.firstSelectedNote
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
})
|
||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen)
|
||||
const toggleChangeEditorMenu = async () => {
|
||||
const rect = buttonRef.current?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
const { clientHeight } = document.documentElement
|
||||
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
|
||||
const footerHeightInPx = footerElementRect?.height
|
||||
|
||||
const toggleChangeEditorMenu = async () => {
|
||||
const rect = buttonRef.current?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
const { clientHeight } = document.documentElement
|
||||
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
|
||||
const footerHeightInPx = footerElementRect?.height
|
||||
|
||||
if (footerHeightInPx) {
|
||||
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
|
||||
}
|
||||
|
||||
setPosition({
|
||||
top: rect.bottom,
|
||||
right: document.body.clientWidth - rect.right,
|
||||
})
|
||||
|
||||
const newOpenState = !isOpen
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
|
||||
setIsOpen(newOpenState)
|
||||
setTimeout(() => {
|
||||
setIsVisible(newOpenState)
|
||||
})
|
||||
if (footerHeightInPx) {
|
||||
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setPosition({
|
||||
top: rect.bottom,
|
||||
right: document.body.clientWidth - rect.right,
|
||||
})
|
||||
|
||||
const newOpenState = !isOpen
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
|
||||
setIsOpen(newOpenState)
|
||||
setTimeout(() => {
|
||||
setIsVisible(newOpenState)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
className="sn-icon-button border-contrast"
|
||||
>
|
||||
<VisuallyHidden>Change note type</VisuallyHidden>
|
||||
<Icon type="dashboard" className="block" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight,
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown--animated min-w-68 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
{isOpen && (
|
||||
<ChangeEditorMenu
|
||||
closeOnBlur={closeOnBlur}
|
||||
application={application}
|
||||
isVisible={isVisible}
|
||||
note={note}
|
||||
closeMenu={() => {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
className="sn-icon-button border-contrast"
|
||||
>
|
||||
<VisuallyHidden>Change note type</VisuallyHidden>
|
||||
<Icon type="dashboard" className="block" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight,
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown--animated min-w-68 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
{isOpen && (
|
||||
<ChangeEditorMenu
|
||||
closeOnBlur={closeOnBlur}
|
||||
application={application}
|
||||
isVisible={isVisible}
|
||||
note={note}
|
||||
closeMenu={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ChangeEditorButton)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Menu } from '@/Components/Menu/Menu'
|
||||
import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Strings'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
@@ -13,9 +14,9 @@ import {
|
||||
SNNote,
|
||||
TransactionalMutation,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Fragment, FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||
import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption'
|
||||
import { Fragment, FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
||||
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
||||
import { createEditorMenuGroups } from './createEditorMenuGroups'
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants'
|
||||
import {
|
||||
@@ -34,7 +35,7 @@ type ChangeEditorMenuProps = {
|
||||
|
||||
const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-')
|
||||
|
||||
export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
application,
|
||||
closeOnBlur,
|
||||
closeMenu,
|
||||
@@ -189,6 +190,7 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={onClickEditorItem}
|
||||
className={
|
||||
@@ -214,3 +216,5 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeEditorMenu
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
GetFeatures,
|
||||
NoteType,
|
||||
} from '@standardnotes/snjs'
|
||||
import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption'
|
||||
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
||||
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants'
|
||||
|
||||
type EditorGroup = NoteType | 'plain' | 'others'
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { ChangeEventHandler, FunctionComponent } from 'react'
|
||||
|
||||
type CheckboxProps = {
|
||||
name: string
|
||||
checked: boolean
|
||||
onChange: (e: Event) => void
|
||||
onChange: ChangeEventHandler<HTMLInputElement>
|
||||
disabled?: boolean
|
||||
label: string
|
||||
}
|
||||
|
||||
export const Checkbox: FunctionComponent<CheckboxProps> = ({ name, checked, onChange, disabled, label }) => {
|
||||
const Checkbox: FunctionComponent<CheckboxProps> = ({ name, checked, onChange, disabled, label }) => {
|
||||
return (
|
||||
<label htmlFor={name} className="flex items-center fit-content mb-2">
|
||||
<input
|
||||
@@ -24,3 +24,5 @@ export const Checkbox: FunctionComponent<CheckboxProps> = ({ name, checked, onCh
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export default Checkbox
|
||||
|
||||
@@ -8,14 +8,13 @@ import {
|
||||
ComponentViewerError,
|
||||
} from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { OfflineRestricted } from '@/Components/ComponentView/OfflineRestricted'
|
||||
import { UrlMissing } from '@/Components/ComponentView/UrlMissing'
|
||||
import { IsDeprecated } from '@/Components/ComponentView/IsDeprecated'
|
||||
import { IsExpired } from '@/Components/ComponentView/IsExpired'
|
||||
import { IssueOnLoading } from '@/Components/ComponentView/IssueOnLoading'
|
||||
import OfflineRestricted from '@/Components/ComponentView/OfflineRestricted'
|
||||
import UrlMissing from '@/Components/ComponentView/UrlMissing'
|
||||
import IsDeprecated from '@/Components/ComponentView/IsDeprecated'
|
||||
import IsExpired from '@/Components/ComponentView/IsExpired'
|
||||
import IssueOnLoading from '@/Components/ComponentView/IssueOnLoading'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
|
||||
|
||||
@@ -35,187 +34,187 @@ const MaxLoadThreshold = 4000
|
||||
const VisibilityChangeKey = 'visibilitychange'
|
||||
const MSToWaitAfterIframeLoadToAvoidFlicker = 35
|
||||
|
||||
export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
({ application, onLoad, componentViewer, requestReload }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const [loadTimeout, setLoadTimeout] = useState<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, componentViewer, requestReload }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const [loadTimeout, setLoadTimeout] = useState<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
|
||||
const [hasIssueLoading, setHasIssueLoading] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(componentViewer.getFeatureStatus())
|
||||
const [isComponentValid, setIsComponentValid] = useState(true)
|
||||
const [error, setError] = useState<ComponentViewerError | undefined>(undefined)
|
||||
const [deprecationMessage, setDeprecationMessage] = useState<string | undefined>(undefined)
|
||||
const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false)
|
||||
const [didAttemptReload, setDidAttemptReload] = useState(false)
|
||||
const [hasIssueLoading, setHasIssueLoading] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(componentViewer.getFeatureStatus())
|
||||
const [isComponentValid, setIsComponentValid] = useState(true)
|
||||
const [error, setError] = useState<ComponentViewerError | undefined>(undefined)
|
||||
const [deprecationMessage, setDeprecationMessage] = useState<string | undefined>(undefined)
|
||||
const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false)
|
||||
const [didAttemptReload, setDidAttemptReload] = useState(false)
|
||||
|
||||
const component: SNComponent = componentViewer.component
|
||||
const component: SNComponent = componentViewer.component
|
||||
|
||||
const manageSubscription = useCallback(() => {
|
||||
openSubscriptionDashboard(application)
|
||||
}, [application])
|
||||
const manageSubscription = useCallback(() => {
|
||||
openSubscriptionDashboard(application)
|
||||
}, [application])
|
||||
|
||||
const reloadValidityStatus = useCallback(() => {
|
||||
setFeatureStatus(componentViewer.getFeatureStatus())
|
||||
if (!componentViewer.lockReadonly) {
|
||||
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled)
|
||||
}
|
||||
setIsComponentValid(componentViewer.shouldRender())
|
||||
const reloadValidityStatus = useCallback(() => {
|
||||
setFeatureStatus(componentViewer.getFeatureStatus())
|
||||
if (!componentViewer.lockReadonly) {
|
||||
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled)
|
||||
}
|
||||
setIsComponentValid(componentViewer.shouldRender())
|
||||
|
||||
if (isLoading && !isComponentValid) {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
setError(componentViewer.getError())
|
||||
setDeprecationMessage(component.deprecationMessage)
|
||||
}, [componentViewer, component.deprecationMessage, featureStatus, isComponentValid, isLoading])
|
||||
|
||||
useEffect(() => {
|
||||
reloadValidityStatus()
|
||||
}, [reloadValidityStatus])
|
||||
|
||||
const dismissDeprecationMessage = () => {
|
||||
setIsDeprecationMessageDismissed(true)
|
||||
if (isLoading && !isComponentValid) {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const onVisibilityChange = useCallback(() => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
return
|
||||
}
|
||||
if (hasIssueLoading) {
|
||||
setError(componentViewer.getError())
|
||||
setDeprecationMessage(component.deprecationMessage)
|
||||
}, [componentViewer, component.deprecationMessage, featureStatus, isComponentValid, isLoading])
|
||||
|
||||
useEffect(() => {
|
||||
reloadValidityStatus()
|
||||
}, [reloadValidityStatus])
|
||||
|
||||
const dismissDeprecationMessage = () => {
|
||||
setIsDeprecationMessageDismissed(true)
|
||||
}
|
||||
|
||||
const onVisibilityChange = useCallback(() => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
return
|
||||
}
|
||||
if (hasIssueLoading) {
|
||||
requestReload?.(componentViewer)
|
||||
}
|
||||
}, [hasIssueLoading, componentViewer, requestReload])
|
||||
|
||||
useEffect(() => {
|
||||
const loadTimeout = setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
setHasIssueLoading(true)
|
||||
|
||||
if (!didAttemptReload) {
|
||||
setDidAttemptReload(true)
|
||||
requestReload?.(componentViewer)
|
||||
} else {
|
||||
document.addEventListener(VisibilityChangeKey, onVisibilityChange)
|
||||
}
|
||||
}, [hasIssueLoading, componentViewer, requestReload])
|
||||
}, MaxLoadThreshold)
|
||||
|
||||
useEffect(() => {
|
||||
const loadTimeout = setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
setHasIssueLoading(true)
|
||||
|
||||
if (!didAttemptReload) {
|
||||
setDidAttemptReload(true)
|
||||
requestReload?.(componentViewer)
|
||||
} else {
|
||||
document.addEventListener(VisibilityChangeKey, onVisibilityChange)
|
||||
}
|
||||
}, MaxLoadThreshold)
|
||||
|
||||
setLoadTimeout(loadTimeout)
|
||||
|
||||
return () => {
|
||||
if (loadTimeout) {
|
||||
clearTimeout(loadTimeout)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [componentViewer])
|
||||
|
||||
const onIframeLoad = useCallback(() => {
|
||||
const iframe = iframeRef.current as HTMLIFrameElement
|
||||
const contentWindow = iframe.contentWindow as Window
|
||||
setLoadTimeout(loadTimeout)
|
||||
|
||||
return () => {
|
||||
if (loadTimeout) {
|
||||
clearTimeout(loadTimeout)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [componentViewer])
|
||||
|
||||
try {
|
||||
componentViewer.setWindow(contentWindow)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const onIframeLoad = useCallback(() => {
|
||||
const iframe = iframeRef.current as HTMLIFrameElement
|
||||
const contentWindow = iframe.contentWindow as Window
|
||||
|
||||
if (loadTimeout) {
|
||||
clearTimeout(loadTimeout)
|
||||
}
|
||||
|
||||
try {
|
||||
componentViewer.setWindow(contentWindow)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
setHasIssueLoading(false)
|
||||
onLoad?.(component)
|
||||
}, MSToWaitAfterIframeLoadToAvoidFlicker)
|
||||
}, [componentViewer, onLoad, component, loadTimeout])
|
||||
|
||||
useEffect(() => {
|
||||
const removeFeaturesChangedObserver = componentViewer.addEventObserver((event) => {
|
||||
if (event === ComponentViewerEvent.FeatureStatusUpdated) {
|
||||
setFeatureStatus(componentViewer.getFeatureStatus())
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
setHasIssueLoading(false)
|
||||
onLoad?.(component)
|
||||
}, MSToWaitAfterIframeLoadToAvoidFlicker)
|
||||
}, [componentViewer, onLoad, component, loadTimeout])
|
||||
return () => {
|
||||
removeFeaturesChangedObserver()
|
||||
}
|
||||
}, [componentViewer])
|
||||
|
||||
useEffect(() => {
|
||||
const removeFeaturesChangedObserver = componentViewer.addEventObserver((event) => {
|
||||
if (event === ComponentViewerEvent.FeatureStatusUpdated) {
|
||||
setFeatureStatus(componentViewer.getFeatureStatus())
|
||||
useEffect(() => {
|
||||
const removeActionObserver = componentViewer.addActionObserver((action, data) => {
|
||||
switch (action) {
|
||||
case ComponentAction.KeyDown:
|
||||
application.io.handleComponentKeyDown(data.keyboardModifier)
|
||||
break
|
||||
case ComponentAction.KeyUp:
|
||||
application.io.handleComponentKeyUp(data.keyboardModifier)
|
||||
break
|
||||
case ComponentAction.Click:
|
||||
application.getAppState().notes.setContextMenuOpen(false)
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
removeActionObserver()
|
||||
}
|
||||
}, [componentViewer, application])
|
||||
|
||||
useEffect(() => {
|
||||
const unregisterDesktopObserver = application
|
||||
.getDesktopService()
|
||||
?.registerUpdateObserver((updatedComponent: SNComponent) => {
|
||||
if (updatedComponent.uuid === component.uuid && updatedComponent.active) {
|
||||
requestReload?.(componentViewer)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeFeaturesChangedObserver()
|
||||
}
|
||||
}, [componentViewer])
|
||||
return () => {
|
||||
unregisterDesktopObserver?.()
|
||||
}
|
||||
}, [application, requestReload, componentViewer, component.uuid])
|
||||
|
||||
useEffect(() => {
|
||||
const removeActionObserver = componentViewer.addActionObserver((action, data) => {
|
||||
switch (action) {
|
||||
case ComponentAction.KeyDown:
|
||||
application.io.handleComponentKeyDown(data.keyboardModifier)
|
||||
break
|
||||
case ComponentAction.KeyUp:
|
||||
application.io.handleComponentKeyUp(data.keyboardModifier)
|
||||
break
|
||||
case ComponentAction.Click:
|
||||
application.getAppState().notes.setContextMenuOpen(false)
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
removeActionObserver()
|
||||
}
|
||||
}, [componentViewer, application])
|
||||
return (
|
||||
<>
|
||||
{hasIssueLoading && (
|
||||
<IssueOnLoading
|
||||
componentName={component.displayName}
|
||||
reloadIframe={() => {
|
||||
reloadValidityStatus(), requestReload?.(componentViewer, true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
useEffect(() => {
|
||||
const unregisterDesktopObserver = application
|
||||
.getDesktopService()
|
||||
?.registerUpdateObserver((updatedComponent: SNComponent) => {
|
||||
if (updatedComponent.uuid === component.uuid && updatedComponent.active) {
|
||||
requestReload?.(componentViewer)
|
||||
}
|
||||
})
|
||||
{featureStatus !== FeatureStatus.Entitled && (
|
||||
<IsExpired
|
||||
expiredDate={dateToLocalizedString(component.valid_until)}
|
||||
featureStatus={featureStatus}
|
||||
componentName={component.displayName}
|
||||
manageSubscription={manageSubscription}
|
||||
/>
|
||||
)}
|
||||
{deprecationMessage && !isDeprecationMessageDismissed && (
|
||||
<IsDeprecated deprecationMessage={deprecationMessage} dismissDeprecationMessage={dismissDeprecationMessage} />
|
||||
)}
|
||||
{error === ComponentViewerError.OfflineRestricted && <OfflineRestricted />}
|
||||
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.displayName} />}
|
||||
{component.uuid && isComponentValid && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
onLoad={onIframeLoad}
|
||||
data-component-viewer-id={componentViewer.identifier}
|
||||
frameBorder={0}
|
||||
src={componentViewer.url || ''}
|
||||
sandbox="allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms allow-downloads"
|
||||
>
|
||||
Loading
|
||||
</iframe>
|
||||
)}
|
||||
{isLoading && <div className={'loading-overlay'} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
unregisterDesktopObserver?.()
|
||||
}
|
||||
}, [application, requestReload, componentViewer, component.uuid])
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasIssueLoading && (
|
||||
<IssueOnLoading
|
||||
componentName={component.displayName}
|
||||
reloadIframe={() => {
|
||||
reloadValidityStatus(), requestReload?.(componentViewer, true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{featureStatus !== FeatureStatus.Entitled && (
|
||||
<IsExpired
|
||||
expiredDate={dateToLocalizedString(component.valid_until)}
|
||||
featureStatus={featureStatus}
|
||||
componentName={component.displayName}
|
||||
manageSubscription={manageSubscription}
|
||||
/>
|
||||
)}
|
||||
{deprecationMessage && !isDeprecationMessageDismissed && (
|
||||
<IsDeprecated deprecationMessage={deprecationMessage} dismissDeprecationMessage={dismissDeprecationMessage} />
|
||||
)}
|
||||
{error === ComponentViewerError.OfflineRestricted && <OfflineRestricted />}
|
||||
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.displayName} />}
|
||||
{component.uuid && isComponentValid && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
onLoad={onIframeLoad}
|
||||
data-component-viewer-id={componentViewer.identifier}
|
||||
frameBorder={0}
|
||||
src={componentViewer.url || ''}
|
||||
sandbox="allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms allow-downloads"
|
||||
>
|
||||
Loading
|
||||
</iframe>
|
||||
)}
|
||||
{isLoading && <div className={'loading-overlay'} />}
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
export default observer(ComponentView)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
interface IProps {
|
||||
type Props = {
|
||||
deprecationMessage: string | undefined
|
||||
dismissDeprecationMessage: () => void
|
||||
}
|
||||
|
||||
export const IsDeprecated: FunctionalComponent<IProps> = ({ deprecationMessage, dismissDeprecationMessage }) => {
|
||||
const IsDeprecated: FunctionComponent<Props> = ({ deprecationMessage, dismissDeprecationMessage }) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-app-bar no-edges no-top-edge dynamic-height'}>
|
||||
@@ -23,3 +23,5 @@ export const IsDeprecated: FunctionalComponent<IProps> = ({ deprecationMessage,
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IsDeprecated
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FeatureStatus } from '@standardnotes/snjs'
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
interface IProps {
|
||||
type Props = {
|
||||
expiredDate: string
|
||||
componentName: string
|
||||
featureStatus: FeatureStatus
|
||||
@@ -21,12 +21,7 @@ const statusString = (featureStatus: FeatureStatus, expiredDate: string, compone
|
||||
}
|
||||
}
|
||||
|
||||
export const IsExpired: FunctionalComponent<IProps> = ({
|
||||
expiredDate,
|
||||
featureStatus,
|
||||
componentName,
|
||||
manageSubscription,
|
||||
}) => {
|
||||
const IsExpired: FunctionComponent<Props> = ({ expiredDate, featureStatus, componentName, manageSubscription }) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-app-bar no-edges no-top-edge dynamic-height'}>
|
||||
@@ -52,3 +47,5 @@ export const IsExpired: FunctionalComponent<IProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IsExpired
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
interface IProps {
|
||||
type Props = {
|
||||
componentName: string
|
||||
reloadIframe: () => void
|
||||
}
|
||||
|
||||
export const IssueOnLoading: FunctionalComponent<IProps> = ({ componentName, reloadIframe }) => {
|
||||
const IssueOnLoading: FunctionComponent<Props> = ({ componentName, reloadIframe }) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-app-bar no-edges no-top-edge dynamic-height'}>
|
||||
@@ -23,3 +23,5 @@ export const IssueOnLoading: FunctionalComponent<IProps> = ({ componentName, rel
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IssueOnLoading
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
export const OfflineRestricted: FunctionalComponent = () => {
|
||||
const OfflineRestricted: FunctionComponent = () => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-panel static'}>
|
||||
@@ -29,3 +29,5 @@ export const OfflineRestricted: FunctionalComponent = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OfflineRestricted
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
interface IProps {
|
||||
type Props = {
|
||||
componentName: string
|
||||
}
|
||||
|
||||
export const UrlMissing: FunctionalComponent<IProps> = ({ componentName }) => {
|
||||
const UrlMissing: FunctionComponent<Props> = ({ componentName }) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-panel static'}>
|
||||
@@ -20,3 +20,5 @@ export const UrlMissing: FunctionalComponent<IProps> = ({ componentName }) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UrlMissing
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { FunctionComponent, useEffect, useRef, useState } from 'react'
|
||||
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog'
|
||||
import { STRING_SIGN_OUT_CONFIRMATION } from '@/Strings'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
@@ -13,14 +13,7 @@ type Props = {
|
||||
applicationGroup: ApplicationGroup
|
||||
}
|
||||
|
||||
export const ConfirmSignoutContainer = observer((props: Props) => {
|
||||
if (!props.appState.accountMenu.signingOut) {
|
||||
return null
|
||||
}
|
||||
return <ConfirmSignoutModal {...props} />
|
||||
})
|
||||
|
||||
export const ConfirmSignoutModal = observer(({ application, appState, applicationGroup }: Props) => {
|
||||
const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, appState, applicationGroup }) => {
|
||||
const [deleteLocalBackups, setDeleteLocalBackups] = useState(false)
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -114,4 +107,15 @@ export const ConfirmSignoutModal = observer(({ application, appState, applicatio
|
||||
</div>
|
||||
</AlertDialog>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
ConfirmSignoutModal.displayName = 'ConfirmSignoutModal'
|
||||
|
||||
const ConfirmSignoutContainer = (props: Props) => {
|
||||
if (!props.appState.accountMenu.signingOut) {
|
||||
return null
|
||||
}
|
||||
return <ConfirmSignoutModal {...props} />
|
||||
}
|
||||
|
||||
export default observer(ConfirmSignoutContainer)
|
||||
|
||||
@@ -3,11 +3,10 @@ import { KeyboardKey } from '@/Services/IOService'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { UuidString } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
import { ContentListItem } from './ContentListItem'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import ContentListItem from './ContentListItem'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -17,59 +16,59 @@ type Props = {
|
||||
paginate: () => void
|
||||
}
|
||||
|
||||
export const ContentList: FunctionComponent<Props> = observer(
|
||||
({ application, appState, items, selectedItems, paginate }) => {
|
||||
const { selectPreviousItem, selectNextItem } = appState.contentListView
|
||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = appState.contentListView.webDisplayOptions
|
||||
const { sortBy } = appState.contentListView.displayOptions
|
||||
const ContentList: FunctionComponent<Props> = ({ application, appState, items, selectedItems, paginate }) => {
|
||||
const { selectPreviousItem, selectNextItem } = appState.contentListView
|
||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = appState.contentListView.webDisplayOptions
|
||||
const { sortBy } = appState.contentListView.displayOptions
|
||||
|
||||
const onScroll = useCallback(
|
||||
(e: Event) => {
|
||||
const offset = NOTES_LIST_SCROLL_THRESHOLD
|
||||
const element = e.target as HTMLElement
|
||||
if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) {
|
||||
paginate()
|
||||
}
|
||||
},
|
||||
[paginate],
|
||||
)
|
||||
const onScroll: UIEventHandler = useCallback(
|
||||
(e) => {
|
||||
const offset = NOTES_LIST_SCROLL_THRESHOLD
|
||||
const element = e.target as HTMLElement
|
||||
if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) {
|
||||
paginate()
|
||||
}
|
||||
},
|
||||
[paginate],
|
||||
)
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === KeyboardKey.Up) {
|
||||
e.preventDefault()
|
||||
selectPreviousItem()
|
||||
} else if (e.key === KeyboardKey.Down) {
|
||||
e.preventDefault()
|
||||
selectNextItem()
|
||||
}
|
||||
},
|
||||
[selectNextItem, selectPreviousItem],
|
||||
)
|
||||
const onKeyDown: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === KeyboardKey.Up) {
|
||||
e.preventDefault()
|
||||
selectPreviousItem()
|
||||
} else if (e.key === KeyboardKey.Down) {
|
||||
e.preventDefault()
|
||||
selectNextItem()
|
||||
}
|
||||
},
|
||||
[selectNextItem, selectPreviousItem],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="infinite-scroll focus:shadow-none focus:outline-none"
|
||||
id="notes-scrollable"
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<ContentListItem
|
||||
key={item.uuid}
|
||||
application={application}
|
||||
appState={appState}
|
||||
item={item}
|
||||
selected={!!selectedItems[item.uuid]}
|
||||
hideDate={hideDate}
|
||||
hidePreview={hideNotePreview}
|
||||
hideTags={hideTags}
|
||||
hideIcon={hideEditorIcon}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className="infinite-scroll focus:shadow-none focus:outline-none"
|
||||
id="notes-scrollable"
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<ContentListItem
|
||||
key={item.uuid}
|
||||
application={application}
|
||||
appState={appState}
|
||||
item={item}
|
||||
selected={!!selectedItems[item.uuid]}
|
||||
hideDate={hideDate}
|
||||
hidePreview={hideNotePreview}
|
||||
hideTags={hideTags}
|
||||
hideIcon={hideEditorIcon}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ContentList)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ContentType, SNTag } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { FileListItem } from './FileListItem'
|
||||
import { NoteListItem } from './NoteListItem'
|
||||
import { FunctionComponent } from 'react'
|
||||
import FileListItem from './FileListItem'
|
||||
import NoteListItem from './NoteListItem'
|
||||
import { AbstractListItemProps } from './Types/AbstractListItemProps'
|
||||
|
||||
export const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
||||
const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
||||
const getTags = () => {
|
||||
if (props.hideTags) {
|
||||
return []
|
||||
@@ -34,3 +34,5 @@ export const ContentListItem: FunctionComponent<AbstractListItemProps> = (props)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default ContentListItem
|
||||
|
||||
@@ -4,27 +4,29 @@ import { AppState } from '@/UIModels/AppState'
|
||||
import { PANEL_NAME_NOTES } from '@/Constants'
|
||||
import { PrefKey } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { ContentList } from '@/Components/ContentListView/ContentList'
|
||||
import { NotesListOptionsMenu } from '@/Components/ContentListView/NotesListOptionsMenu'
|
||||
import { NoAccountWarning } from '@/Components/NoAccountWarning/NoAccountWarning'
|
||||
import { SearchOptions } from '@/Components/SearchOptions/SearchOptions'
|
||||
import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
FunctionComponent,
|
||||
KeyboardEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import ContentList from '@/Components/ContentListView/ContentList'
|
||||
import NotesListOptionsMenu from '@/Components/ContentListView/NotesListOptionsMenu'
|
||||
import NoAccountWarningWrapper from '@/Components/NoAccountWarning/NoAccountWarning'
|
||||
import SearchOptions from '@/Components/SearchOptions/SearchOptions'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const ContentListView: FunctionComponent<Props> = observer(({ application, appState }) => {
|
||||
if (isStateDealloced(appState)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const ContentListView: FunctionComponent<Props> = ({ application, appState }) => {
|
||||
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
|
||||
const displayOptionsMenuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -104,9 +106,9 @@ export const ContentListView: FunctionComponent<Props> = observer(({ application
|
||||
}
|
||||
}, [application.io, createNewNote, searchBarElement, selectNextItem, selectPreviousItem])
|
||||
|
||||
const onNoteFilterTextChange = useCallback(
|
||||
(e: Event) => {
|
||||
setNoteFilterText((e.target as HTMLInputElement).value)
|
||||
const onNoteFilterTextChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
setNoteFilterText(e.target.value)
|
||||
},
|
||||
[setNoteFilterText],
|
||||
)
|
||||
@@ -114,8 +116,8 @@ export const ContentListView: FunctionComponent<Props> = observer(({ application
|
||||
const onSearchFocused = useCallback(() => setFocusedSearch(true), [])
|
||||
const onSearchBlurred = useCallback(() => setFocusedSearch(false), [])
|
||||
|
||||
const onNoteFilterKeyUp = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const onNoteFilterKeyUp: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === KeyboardKey.Enter) {
|
||||
onFilterEnter()
|
||||
}
|
||||
@@ -176,10 +178,10 @@ export const ContentListView: FunctionComponent<Props> = observer(({ application
|
||||
onKeyUp={onNoteFilterKeyUp}
|
||||
onFocus={onSearchFocused}
|
||||
onBlur={onSearchBlurred}
|
||||
autocomplete="off"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{noteFilterText && (
|
||||
<button onClick={clearFilterText} aria-role="button" id="search-clear-button">
|
||||
<button onClick={clearFilterText} id="search-clear-button">
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
@@ -191,7 +193,7 @@ export const ContentListView: FunctionComponent<Props> = observer(({ application
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NoAccountWarning appState={appState} />
|
||||
<NoAccountWarningWrapper appState={appState} />
|
||||
</div>
|
||||
<div id="items-menu-bar" className="sn-component" ref={displayOptionsMenuRef}>
|
||||
<div className="sk-app-bar no-edges">
|
||||
@@ -253,4 +255,6 @@ export const ContentListView: FunctionComponent<Props> = observer(({ application
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(ContentListView)
|
||||
|
||||
@@ -1,81 +1,89 @@
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import { getFileIconComponent } from '../AttachedFilesPopover/PopoverFileItem'
|
||||
import { ListItemConflictIndicator } from './ListItemConflictIndicator'
|
||||
import { ListItemFlagIcons } from './ListItemFlagIcons'
|
||||
import { ListItemTags } from './ListItemTags'
|
||||
import { ListItemMetadata } from './ListItemMetadata'
|
||||
import { FunctionComponent, useCallback } from 'react'
|
||||
import { getFileIconComponent } from '../AttachedFilesPopover/getFileIconComponent'
|
||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
import ListItemTags from './ListItemTags'
|
||||
import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
|
||||
export const FileListItem: FunctionComponent<DisplayableListItemProps> = observer(
|
||||
({ application, appState, hideDate, hideIcon, hideTags, item, selected, sortBy, tags }) => {
|
||||
const openFileContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
appState.files.setFileContextMenuLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
appState.files.setShowFileContextMenu(true)
|
||||
},
|
||||
[appState.files],
|
||||
)
|
||||
|
||||
const openContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
void appState.contentListView.selectItemWithScrollHandling(item, {
|
||||
userTriggered: true,
|
||||
scrollIntoView: false,
|
||||
})
|
||||
openFileContextMenu(posX, posY)
|
||||
},
|
||||
[appState.contentListView, item, openFileContextMenu],
|
||||
)
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
void appState.selectedItems.selectItem(item.uuid, true).then(({ didSelect }) => {
|
||||
if (didSelect && appState.selectedItems.selectedItemsCount < 2) {
|
||||
appState.filePreviewModal.activate(item as FileItem, appState.files.allFiles)
|
||||
}
|
||||
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
appState,
|
||||
hideDate,
|
||||
hideIcon,
|
||||
hideTags,
|
||||
item,
|
||||
selected,
|
||||
sortBy,
|
||||
tags,
|
||||
}) => {
|
||||
const openFileContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
appState.files.setFileContextMenuLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
}, [appState.filePreviewModal, appState.files.allFiles, appState.selectedItems, item])
|
||||
appState.files.setShowFileContextMenu(true)
|
||||
},
|
||||
[appState.files],
|
||||
)
|
||||
|
||||
const IconComponent = () =>
|
||||
getFileIconComponent(
|
||||
application.iconsController.getIconForFileType((item as FileItem).mimeType),
|
||||
'w-5 h-5 flex-shrink-0',
|
||||
)
|
||||
const openContextMenu = useCallback(
|
||||
async (posX: number, posY: number) => {
|
||||
const { didSelect } = await appState.selectedItems.selectItem(item.uuid)
|
||||
if (didSelect) {
|
||||
openFileContextMenu(posX, posY)
|
||||
}
|
||||
},
|
||||
[appState.selectedItems, item.uuid, openFileContextMenu],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`content-list-item flex items-stretch w-full cursor-pointer ${
|
||||
selected && 'selected border-0 border-l-2px border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={onClick}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="flex flex-col items-center justify-between p-4.5 pr-3 mr-0">
|
||||
<IconComponent />
|
||||
</div>
|
||||
) : (
|
||||
<div className="pr-4" />
|
||||
)}
|
||||
<div className="flex-grow min-w-0 py-4 px-0 border-0 border-b-1 border-solid border-main">
|
||||
<div className="flex items-start justify-between font-semibold text-base leading-1.3 overflow-hidden">
|
||||
<div className="break-word mr-2">{item.title}</div>
|
||||
</div>
|
||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||
<ListItemConflictIndicator item={item} />
|
||||
</div>
|
||||
<ListItemFlagIcons item={item} />
|
||||
</div>
|
||||
const onClick = useCallback(() => {
|
||||
void appState.selectedItems.selectItem(item.uuid, true).then(({ didSelect }) => {
|
||||
if (didSelect && appState.selectedItems.selectedItemsCount < 2) {
|
||||
appState.filePreviewModal.activate(item as FileItem, appState.files.allFiles)
|
||||
}
|
||||
})
|
||||
}, [appState.filePreviewModal, appState.files.allFiles, appState.selectedItems, item])
|
||||
|
||||
const IconComponent = () =>
|
||||
getFileIconComponent(
|
||||
application.iconsController.getIconForFileType((item as FileItem).mimeType),
|
||||
'w-5 h-5 flex-shrink-0',
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`content-list-item flex items-stretch w-full cursor-pointer ${
|
||||
selected && 'selected border-0 border-l-2px border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={onClick}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
void openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="flex flex-col items-center justify-between p-4.5 pr-3 mr-0">
|
||||
<IconComponent />
|
||||
</div>
|
||||
) : (
|
||||
<div className="pr-4" />
|
||||
)}
|
||||
<div className="flex-grow min-w-0 py-4 px-0 border-0 border-b-1 border-solid border-main">
|
||||
<div className="flex items-start justify-between font-semibold text-base leading-1.3 overflow-hidden">
|
||||
<div className="break-word mr-2">{item.title}</div>
|
||||
</div>
|
||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||
<ListItemConflictIndicator item={item} />
|
||||
</div>
|
||||
<ListItemFlagIcons item={item} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(FileListItem)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
|
||||
export const ListItemConflictIndicator: FunctionComponent<{
|
||||
type Props = {
|
||||
item: {
|
||||
conflictOf?: ListableContentItem['conflictOf']
|
||||
}
|
||||
}> = ({ item }) => {
|
||||
}
|
||||
|
||||
const ListItemConflictIndicator: FunctionComponent<Props> = ({ item }) => {
|
||||
return item.conflictOf ? (
|
||||
<div className="flex flex-wrap items-center mt-0.5">
|
||||
<div className={'py-1 px-1.5 rounded mr-1 mt-2 bg-danger color-danger-contrast'}>
|
||||
@@ -14,3 +16,5 @@ export const ListItemConflictIndicator: FunctionComponent<{
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default ListItemConflictIndicator
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
|
||||
type Props = {
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
hasFiles?: boolean
|
||||
}
|
||||
|
||||
export const ListItemFlagIcons: FunctionComponent<Props> = ({ item, hasFiles = false }) => {
|
||||
const ListItemFlagIcons: FunctionComponent<Props> = ({ item, hasFiles = false }) => {
|
||||
return (
|
||||
<div className="flex items-start p-4 pl-0 border-0 border-b-1 border-solid border-main">
|
||||
{item.locked && (
|
||||
@@ -43,3 +43,5 @@ export const ListItemFlagIcons: FunctionComponent<Props> = ({ item, hasFiles = f
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListItemFlagIcons
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CollectionSort, SortableItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
|
||||
type Props = {
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
sortBy: keyof SortableItem | undefined
|
||||
}
|
||||
|
||||
export const ListItemMetadata: FunctionComponent<Props> = ({ item, hideDate, sortBy }) => {
|
||||
const ListItemMetadata: FunctionComponent<Props> = ({ item, hideDate, sortBy }) => {
|
||||
const showModifiedDate = sortBy === CollectionSort.UpdatedAt
|
||||
|
||||
if (hideDate && !item.protected) {
|
||||
@@ -27,3 +27,5 @@ export const ListItemMetadata: FunctionComponent<Props> = ({ item, hideDate, sor
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListItemMetadata
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
export const ListItemTags: FunctionComponent<{
|
||||
type Props = {
|
||||
hideTags: boolean
|
||||
tags: string[]
|
||||
}> = ({ hideTags, tags }) => {
|
||||
}
|
||||
|
||||
const ListItemTags: FunctionComponent<Props> = ({ hideTags, tags }) => {
|
||||
if (hideTags || !tags.length) {
|
||||
return null
|
||||
}
|
||||
@@ -12,7 +14,10 @@ export const ListItemTags: FunctionComponent<{
|
||||
return (
|
||||
<div className="flex flex-wrap mt-1.5 text-xs gap-2">
|
||||
{tags.map((tag) => (
|
||||
<span className="inline-flex items-center py-1 px-1.5 bg-passive-4-opacity-variant color-foreground rounded-0.5">
|
||||
<span
|
||||
className="inline-flex items-center py-1 px-1.5 bg-passive-4-opacity-variant color-foreground rounded-0.5"
|
||||
key={tag}
|
||||
>
|
||||
<Icon type="hashtag" className="sn-icon--small color-passive-1 mr-1" />
|
||||
<span>{tag}</span>
|
||||
</span>
|
||||
@@ -20,3 +25,5 @@ export const ListItemTags: FunctionComponent<{
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListItemTags
|
||||
|
||||
@@ -1,84 +1,97 @@
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants'
|
||||
import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { ListItemConflictIndicator } from './ListItemConflictIndicator'
|
||||
import { ListItemFlagIcons } from './ListItemFlagIcons'
|
||||
import { ListItemTags } from './ListItemTags'
|
||||
import { ListItemMetadata } from './ListItemMetadata'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
import ListItemTags from './ListItemTags'
|
||||
import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
|
||||
export const NoteListItem: FunctionComponent<DisplayableListItemProps> = observer(
|
||||
({ application, appState, hideDate, hideIcon, hideTags, hidePreview, item, selected, sortBy, tags }) => {
|
||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
||||
const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
appState,
|
||||
hideDate,
|
||||
hideIcon,
|
||||
hideTags,
|
||||
hidePreview,
|
||||
item,
|
||||
selected,
|
||||
sortBy,
|
||||
tags,
|
||||
}) => {
|
||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
||||
const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0
|
||||
|
||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||
appState.notes.setContextMenuClickLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
appState.notes.reloadContextMenuLayout()
|
||||
appState.notes.setContextMenuOpen(true)
|
||||
}
|
||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||
appState.notes.setContextMenuClickLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
appState.notes.reloadContextMenuLayout()
|
||||
appState.notes.setContextMenuOpen(true)
|
||||
}
|
||||
|
||||
const openContextMenu = (posX: number, posY: number) => {
|
||||
void appState.selectedItems.selectItem(item.uuid, true)
|
||||
const openContextMenu = async (posX: number, posY: number) => {
|
||||
const { didSelect } = await appState.selectedItems.selectItem(item.uuid, true)
|
||||
if (didSelect) {
|
||||
openNoteContextMenu(posX, posY)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`content-list-item flex items-stretch w-full cursor-pointer ${
|
||||
selected && 'selected border-0 border-l-2px border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={() => {
|
||||
void appState.selectedItems.selectItem(item.uuid, true)
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="flex flex-col items-center justify-between p-4 pr-3 mr-0">
|
||||
<Icon ariaLabel={`Icon for ${editorName}`} type={icon} className={`color-accessory-tint-${tint}`} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="pr-4" />
|
||||
)}
|
||||
<div className="flex-grow min-w-0 py-4 px-0 border-0 border-b-1 border-solid border-main">
|
||||
<div className="flex items-start justify-between font-semibold text-base leading-1.3 overflow-hidden">
|
||||
<div className="break-word mr-2">{item.title}</div>
|
||||
</div>
|
||||
{!hidePreview && !item.hidePreview && !item.protected && (
|
||||
<div className="overflow-hidden overflow-ellipsis text-sm">
|
||||
{item.preview_html && (
|
||||
<div
|
||||
className="my-1"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtmlString(item.preview_html),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{!item.preview_html && item.preview_plain && (
|
||||
<div className="leading-1.3 overflow-hidden line-clamp-1 mt-1">{item.preview_plain}</div>
|
||||
)}
|
||||
{!item.preview_html && !item.preview_plain && item.text && (
|
||||
<div className="leading-1.3 overflow-hidden line-clamp-1 mt-1">{item.text}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||
<ListItemConflictIndicator item={item} />
|
||||
return (
|
||||
<div
|
||||
className={`content-list-item flex items-stretch w-full cursor-pointer ${
|
||||
selected && 'selected border-0 border-l-2px border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={() => {
|
||||
void appState.selectedItems.selectItem(item.uuid, true)
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
void openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="flex flex-col items-center justify-between p-4 pr-3 mr-0">
|
||||
<Icon ariaLabel={`Icon for ${editorName}`} type={icon} className={`color-accessory-tint-${tint}`} />
|
||||
</div>
|
||||
<ListItemFlagIcons item={item} hasFiles={hasFiles} />
|
||||
) : (
|
||||
<div className="pr-4" />
|
||||
)}
|
||||
<div className="flex-grow min-w-0 py-4 px-0 border-0 border-b-1 border-solid border-main">
|
||||
<div className="flex items-start justify-between font-semibold text-base leading-1.3 overflow-hidden">
|
||||
<div className="break-word mr-2">{item.title}</div>
|
||||
</div>
|
||||
{!hidePreview && !item.hidePreview && !item.protected && (
|
||||
<div className="overflow-hidden overflow-ellipsis text-sm">
|
||||
{item.preview_html && (
|
||||
<div
|
||||
className="my-1"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtmlString(item.preview_html),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{!item.preview_html && item.preview_plain && (
|
||||
<div className="leading-1.3 overflow-hidden line-clamp-1 mt-1">{item.preview_plain}</div>
|
||||
)}
|
||||
{!item.preview_html && !item.preview_plain && item.text && (
|
||||
<div className="leading-1.3 overflow-hidden line-clamp-1 mt-1">{item.text}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||
<ListItemConflictIndicator item={item} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
<ListItemFlagIcons item={item} hasFiles={hasFiles} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(NoteListItem)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { CollectionSort, CollectionSortProperty, PrefKey } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Menu } from '@/Components/Menu/Menu'
|
||||
import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem'
|
||||
import { FunctionComponent, useCallback, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -14,235 +15,238 @@ type Props = {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
export const NotesListOptionsMenu: FunctionComponent<Props> = observer(
|
||||
({ closeDisplayOptionsMenu, closeOnBlur, application, isOpen }) => {
|
||||
const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt))
|
||||
const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false))
|
||||
const [hidePreview, setHidePreview] = useState(() => application.getPreference(PrefKey.NotesHideNotePreview, false))
|
||||
const [hideDate, setHideDate] = useState(() => application.getPreference(PrefKey.NotesHideDate, false))
|
||||
const [hideTags, setHideTags] = useState(() => application.getPreference(PrefKey.NotesHideTags, true))
|
||||
const [hidePinned, setHidePinned] = useState(() => application.getPreference(PrefKey.NotesHidePinned, false))
|
||||
const [showArchived, setShowArchived] = useState(() => application.getPreference(PrefKey.NotesShowArchived, false))
|
||||
const [showTrashed, setShowTrashed] = useState(() => application.getPreference(PrefKey.NotesShowTrashed, false))
|
||||
const [hideProtected, setHideProtected] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideProtected, false),
|
||||
)
|
||||
const [hideEditorIcon, setHideEditorIcon] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideEditorIcon, false),
|
||||
)
|
||||
const NotesListOptionsMenu: FunctionComponent<Props> = ({
|
||||
closeDisplayOptionsMenu,
|
||||
closeOnBlur,
|
||||
application,
|
||||
isOpen,
|
||||
}) => {
|
||||
const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt))
|
||||
const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false))
|
||||
const [hidePreview, setHidePreview] = useState(() => application.getPreference(PrefKey.NotesHideNotePreview, false))
|
||||
const [hideDate, setHideDate] = useState(() => application.getPreference(PrefKey.NotesHideDate, false))
|
||||
const [hideTags, setHideTags] = useState(() => application.getPreference(PrefKey.NotesHideTags, true))
|
||||
const [hidePinned, setHidePinned] = useState(() => application.getPreference(PrefKey.NotesHidePinned, false))
|
||||
const [showArchived, setShowArchived] = useState(() => application.getPreference(PrefKey.NotesShowArchived, false))
|
||||
const [showTrashed, setShowTrashed] = useState(() => application.getPreference(PrefKey.NotesShowTrashed, false))
|
||||
const [hideProtected, setHideProtected] = useState(() => application.getPreference(PrefKey.NotesHideProtected, false))
|
||||
const [hideEditorIcon, setHideEditorIcon] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideEditorIcon, false),
|
||||
)
|
||||
|
||||
const toggleSortReverse = useCallback(() => {
|
||||
application.setPreference(PrefKey.SortNotesReverse, !sortReverse).catch(console.error)
|
||||
setSortReverse(!sortReverse)
|
||||
}, [application, sortReverse])
|
||||
const toggleSortReverse = useCallback(() => {
|
||||
application.setPreference(PrefKey.SortNotesReverse, !sortReverse).catch(console.error)
|
||||
setSortReverse(!sortReverse)
|
||||
}, [application, sortReverse])
|
||||
|
||||
const toggleSortBy = useCallback(
|
||||
(sort: CollectionSortProperty) => {
|
||||
if (sortBy === sort) {
|
||||
toggleSortReverse()
|
||||
} else {
|
||||
setSortBy(sort)
|
||||
application.setPreference(PrefKey.SortNotesBy, sort).catch(console.error)
|
||||
}
|
||||
},
|
||||
[application, sortBy, toggleSortReverse],
|
||||
)
|
||||
const toggleSortBy = useCallback(
|
||||
(sort: CollectionSortProperty) => {
|
||||
if (sortBy === sort) {
|
||||
toggleSortReverse()
|
||||
} else {
|
||||
setSortBy(sort)
|
||||
application.setPreference(PrefKey.SortNotesBy, sort).catch(console.error)
|
||||
}
|
||||
},
|
||||
[application, sortBy, toggleSortReverse],
|
||||
)
|
||||
|
||||
const toggleSortByDateModified = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.UpdatedAt)
|
||||
}, [toggleSortBy])
|
||||
const toggleSortByDateModified = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.UpdatedAt)
|
||||
}, [toggleSortBy])
|
||||
|
||||
const toggleSortByCreationDate = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.CreatedAt)
|
||||
}, [toggleSortBy])
|
||||
const toggleSortByCreationDate = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.CreatedAt)
|
||||
}, [toggleSortBy])
|
||||
|
||||
const toggleSortByTitle = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.Title)
|
||||
}, [toggleSortBy])
|
||||
const toggleSortByTitle = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.Title)
|
||||
}, [toggleSortBy])
|
||||
|
||||
const toggleHidePreview = useCallback(() => {
|
||||
setHidePreview(!hidePreview)
|
||||
application.setPreference(PrefKey.NotesHideNotePreview, !hidePreview).catch(console.error)
|
||||
}, [application, hidePreview])
|
||||
const toggleHidePreview = useCallback(() => {
|
||||
setHidePreview(!hidePreview)
|
||||
application.setPreference(PrefKey.NotesHideNotePreview, !hidePreview).catch(console.error)
|
||||
}, [application, hidePreview])
|
||||
|
||||
const toggleHideDate = useCallback(() => {
|
||||
setHideDate(!hideDate)
|
||||
application.setPreference(PrefKey.NotesHideDate, !hideDate).catch(console.error)
|
||||
}, [application, hideDate])
|
||||
const toggleHideDate = useCallback(() => {
|
||||
setHideDate(!hideDate)
|
||||
application.setPreference(PrefKey.NotesHideDate, !hideDate).catch(console.error)
|
||||
}, [application, hideDate])
|
||||
|
||||
const toggleHideTags = useCallback(() => {
|
||||
setHideTags(!hideTags)
|
||||
application.setPreference(PrefKey.NotesHideTags, !hideTags).catch(console.error)
|
||||
}, [application, hideTags])
|
||||
const toggleHideTags = useCallback(() => {
|
||||
setHideTags(!hideTags)
|
||||
application.setPreference(PrefKey.NotesHideTags, !hideTags).catch(console.error)
|
||||
}, [application, hideTags])
|
||||
|
||||
const toggleHidePinned = useCallback(() => {
|
||||
setHidePinned(!hidePinned)
|
||||
application.setPreference(PrefKey.NotesHidePinned, !hidePinned).catch(console.error)
|
||||
}, [application, hidePinned])
|
||||
const toggleHidePinned = useCallback(() => {
|
||||
setHidePinned(!hidePinned)
|
||||
application.setPreference(PrefKey.NotesHidePinned, !hidePinned).catch(console.error)
|
||||
}, [application, hidePinned])
|
||||
|
||||
const toggleShowArchived = useCallback(() => {
|
||||
setShowArchived(!showArchived)
|
||||
application.setPreference(PrefKey.NotesShowArchived, !showArchived).catch(console.error)
|
||||
}, [application, showArchived])
|
||||
const toggleShowArchived = useCallback(() => {
|
||||
setShowArchived(!showArchived)
|
||||
application.setPreference(PrefKey.NotesShowArchived, !showArchived).catch(console.error)
|
||||
}, [application, showArchived])
|
||||
|
||||
const toggleShowTrashed = useCallback(() => {
|
||||
setShowTrashed(!showTrashed)
|
||||
application.setPreference(PrefKey.NotesShowTrashed, !showTrashed).catch(console.error)
|
||||
}, [application, showTrashed])
|
||||
const toggleShowTrashed = useCallback(() => {
|
||||
setShowTrashed(!showTrashed)
|
||||
application.setPreference(PrefKey.NotesShowTrashed, !showTrashed).catch(console.error)
|
||||
}, [application, showTrashed])
|
||||
|
||||
const toggleHideProtected = useCallback(() => {
|
||||
setHideProtected(!hideProtected)
|
||||
application.setPreference(PrefKey.NotesHideProtected, !hideProtected).catch(console.error)
|
||||
}, [application, hideProtected])
|
||||
const toggleHideProtected = useCallback(() => {
|
||||
setHideProtected(!hideProtected)
|
||||
application.setPreference(PrefKey.NotesHideProtected, !hideProtected).catch(console.error)
|
||||
}, [application, hideProtected])
|
||||
|
||||
const toggleEditorIcon = useCallback(() => {
|
||||
setHideEditorIcon(!hideEditorIcon)
|
||||
application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon).catch(console.error)
|
||||
}, [application, hideEditorIcon])
|
||||
const toggleEditorIcon = useCallback(() => {
|
||||
setHideEditorIcon(!hideEditorIcon)
|
||||
application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon).catch(console.error)
|
||||
}, [application, hideEditorIcon])
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={
|
||||
'sn-dropdown sn-dropdown--animated min-w-70 overflow-y-auto \
|
||||
return (
|
||||
<Menu
|
||||
className={
|
||||
'sn-dropdown sn-dropdown--animated min-w-70 overflow-y-auto \
|
||||
border-1 border-solid border-main text-sm z-index-dropdown-menu \
|
||||
flex flex-col py-2 top-full left-2 absolute'
|
||||
}
|
||||
a11yLabel="Notes list options menu"
|
||||
closeMenu={closeDisplayOptionsMenu}
|
||||
isOpen={isOpen}
|
||||
}
|
||||
a11yLabel="Notes list options menu"
|
||||
closeMenu={closeDisplayOptionsMenu}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<div className="px-3 my-1 text-xs font-semibold color-text uppercase">Sort by</div>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByDateModified}
|
||||
checked={sortBy === CollectionSort.UpdatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="px-3 my-1 text-xs font-semibold color-text uppercase">Sort by</div>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByDateModified}
|
||||
checked={sortBy === CollectionSort.UpdatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Date modified</span>
|
||||
{sortBy === CollectionSort.UpdatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByCreationDate}
|
||||
checked={sortBy === CollectionSort.CreatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Creation date</span>
|
||||
{sortBy === CollectionSort.CreatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByTitle}
|
||||
checked={sortBy === CollectionSort.Title}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Title</span>
|
||||
{sortBy === CollectionSort.Title ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItemSeparator />
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">View</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePreview}
|
||||
onChange={toggleHidePreview}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-col max-w-3/4">Show note preview</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideDate}
|
||||
onChange={toggleHideDate}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideTags}
|
||||
onChange={toggleHideTags}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show tags
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideEditorIcon}
|
||||
onChange={toggleEditorIcon}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show editor icon
|
||||
</MenuItem>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">Other</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePinned}
|
||||
onChange={toggleHidePinned}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show pinned notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideProtected}
|
||||
onChange={toggleHideProtected}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show protected notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showArchived}
|
||||
onChange={toggleShowArchived}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show archived notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showTrashed}
|
||||
onChange={toggleShowTrashed}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show trashed notes
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)
|
||||
},
|
||||
)
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Date modified</span>
|
||||
{sortBy === CollectionSort.UpdatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByCreationDate}
|
||||
checked={sortBy === CollectionSort.CreatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Creation date</span>
|
||||
{sortBy === CollectionSort.CreatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByTitle}
|
||||
checked={sortBy === CollectionSort.Title}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Title</span>
|
||||
{sortBy === CollectionSort.Title ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItemSeparator />
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">View</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePreview}
|
||||
onChange={toggleHidePreview}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-col max-w-3/4">Show note preview</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideDate}
|
||||
onChange={toggleHideDate}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideTags}
|
||||
onChange={toggleHideTags}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show tags
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideEditorIcon}
|
||||
onChange={toggleEditorIcon}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show editor icon
|
||||
</MenuItem>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">Other</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePinned}
|
||||
onChange={toggleHidePinned}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show pinned notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideProtected}
|
||||
onChange={toggleHideProtected}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show protected notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showArchived}
|
||||
onChange={toggleShowArchived}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show archived notes
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showTrashed}
|
||||
onChange={toggleShowTrashed}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show trashed notes
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(NotesListOptionsMenu)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const DeallocateHandler: FunctionComponent<Props> = ({ application, children }) => {
|
||||
if (application.dealloced) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export default observer(DeallocateHandler)
|
||||
@@ -1,16 +1,8 @@
|
||||
import { ListboxArrow, ListboxButton, ListboxInput, ListboxList, ListboxOption, ListboxPopover } from '@reach/listbox'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
|
||||
export type DropdownItem = {
|
||||
icon?: IconType
|
||||
iconClassName?: string
|
||||
label: string
|
||||
value: string
|
||||
disabled?: boolean
|
||||
}
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { DropdownItem } from './DropdownItem'
|
||||
|
||||
type DropdownProps = {
|
||||
id: string
|
||||
@@ -46,7 +38,7 @@ const CustomDropdownButton: FunctionComponent<ListboxButtonProps> = ({
|
||||
</>
|
||||
)
|
||||
|
||||
export const Dropdown: FunctionComponent<DropdownProps> = ({ id, label, items, value, onChange, disabled }) => {
|
||||
const Dropdown: FunctionComponent<DropdownProps> = ({ id, label, items, value, onChange, disabled }) => {
|
||||
const labelId = `${id}-label`
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
@@ -79,6 +71,7 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({ id, label, items, v
|
||||
<ListboxList>
|
||||
{items.map((item) => (
|
||||
<ListboxOption
|
||||
key={item.value}
|
||||
className="sn-dropdown-item"
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
@@ -99,3 +92,5 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({ id, label, items, v
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dropdown
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
|
||||
export type DropdownItem = {
|
||||
icon?: IconType
|
||||
iconClassName?: string
|
||||
label: string
|
||||
value: string
|
||||
disabled?: boolean
|
||||
}
|
||||
@@ -3,18 +3,16 @@ import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import React from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { PopoverFileItemAction } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { PopoverTabs } from '../AttachedFilesPopover/PopoverTabs'
|
||||
import { FileMenuOptions } from './FileMenuOptions'
|
||||
import FileMenuOptions from './FileMenuOptions'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const FileContextMenu: FunctionComponent<Props> = observer(({ appState }) => {
|
||||
const FileContextMenu: FunctionComponent<Props> = observer(({ appState }) => {
|
||||
const { selectedFiles, showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = appState.files
|
||||
|
||||
const [contextMenuStyle, setContextMenuStyle] = useState<React.CSSProperties>({
|
||||
@@ -28,9 +26,6 @@ export const FileContextMenu: FunctionComponent<Props> = observer(({ appState })
|
||||
useCloseOnClickOutside(contextMenuRef, () => appState.files.setShowFileContextMenu(false))
|
||||
|
||||
const selectedFile = selectedFiles[0]
|
||||
if (!showFileContextMenu || !selectedFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
const reloadContextMenuLayout = useCallback(() => {
|
||||
const { clientHeight } = document.documentElement
|
||||
@@ -118,3 +113,19 @@ export const FileContextMenu: FunctionComponent<Props> = observer(({ appState })
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
FileContextMenu.displayName = 'FileContextMenu'
|
||||
|
||||
const FileContextMenuWrapper: FunctionComponent<Props> = ({ appState }) => {
|
||||
const { selectedFiles, showFileContextMenu } = appState.files
|
||||
|
||||
const selectedFile = selectedFiles[0]
|
||||
|
||||
if (!showFileContextMenu || !selectedFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <FileContextMenu appState={appState} />
|
||||
}
|
||||
|
||||
export default observer(FileContextMenuWrapper)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { PopoverFileItemAction, PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
|
||||
type Props = {
|
||||
closeMenu: () => void
|
||||
@@ -17,7 +17,7 @@ type Props = {
|
||||
shouldShowAttachOption: boolean
|
||||
}
|
||||
|
||||
export const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
closeMenu,
|
||||
closeOnBlur,
|
||||
file,
|
||||
@@ -139,3 +139,5 @@ export const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileMenuOptions
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
type Props = {
|
||||
file: FileItem
|
||||
}
|
||||
|
||||
export const FilePreviewInfoPanel: FunctionComponent<Props> = ({ file }) => {
|
||||
const FilePreviewInfoPanel: FunctionComponent<Props> = ({ file }) => {
|
||||
return (
|
||||
<div className="flex flex-col min-w-70 p-4 border-0 border-l-1px border-solid border-main">
|
||||
<div className="flex items-center mb-4">
|
||||
@@ -35,3 +35,5 @@ export const FilePreviewInfoPanel: FunctionComponent<Props> = ({ file }) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilePreviewInfoPanel
|
||||
|
||||
@@ -3,14 +3,13 @@ import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog'
|
||||
import { addToast, ToastType } from '@standardnotes/stylekit'
|
||||
import { NoPreviewIllustration } from '@standardnotes/icons'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { getFileIconComponent } from '@/Components/AttachedFilesPopover/PopoverFileItem'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FilePreviewInfoPanel } from './FilePreviewInfoPanel'
|
||||
import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { getFileIconComponent } from '@/Components/AttachedFilesPopover/getFileIconComponent'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import FilePreviewInfoPanel from './FilePreviewInfoPanel'
|
||||
import { isFileTypePreviewable } from './isFilePreviewable'
|
||||
import { PreviewComponent } from './PreviewComponent'
|
||||
import PreviewComponent from './PreviewComponent'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
@@ -21,10 +20,6 @@ type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const FilePreviewModalWrapper: FunctionComponent<Props> = observer(({ application, appState }) => {
|
||||
return appState.filePreviewModal.isOpen ? <FilePreviewModal application={application} appState={appState} /> : null
|
||||
})
|
||||
|
||||
const FilePreviewModal: FunctionComponent<Props> = observer(({ application, appState }) => {
|
||||
const { currentFile, setCurrentFile, otherFiles, dismiss } = appState.filePreviewModal
|
||||
|
||||
@@ -91,8 +86,8 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, appS
|
||||
}
|
||||
}, [currentFile, getObjectUrl, objectUrl])
|
||||
|
||||
const keyDownHandler = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const keyDownHandler: KeyboardEventHandler = useCallback(
|
||||
(event) => {
|
||||
if (event.key !== KeyboardKey.Left && event.key !== KeyboardKey.Right) {
|
||||
return
|
||||
}
|
||||
@@ -141,6 +136,7 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, appS
|
||||
dangerouslyBypassScrollLock
|
||||
>
|
||||
<DialogContent
|
||||
aria-label="File preview modal"
|
||||
className="flex flex-col rounded shadow-overlay"
|
||||
style={{
|
||||
width: '90%',
|
||||
@@ -256,3 +252,11 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, appS
|
||||
</DialogOverlay>
|
||||
)
|
||||
})
|
||||
|
||||
FilePreviewModal.displayName = 'FilePreviewModal'
|
||||
|
||||
const FilePreviewModalWrapper: FunctionComponent<Props> = ({ application, appState }) => {
|
||||
return appState.filePreviewModal.isOpen ? <FilePreviewModal application={application} appState={appState} /> : null
|
||||
}
|
||||
|
||||
export default observer(FilePreviewModalWrapper)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useRef, useState } from 'preact/hooks'
|
||||
import { IconButton } from '../Button/IconButton'
|
||||
import { FunctionComponent, useRef, useState } from 'react'
|
||||
import IconButton from '../Button/IconButton'
|
||||
|
||||
type Props = {
|
||||
objectUrl: string
|
||||
}
|
||||
|
||||
export const ImagePreview: FunctionComponent<Props> = ({ objectUrl }) => {
|
||||
const ImagePreview: FunctionComponent<Props> = ({ objectUrl }) => {
|
||||
const initialImgHeightRef = useRef<number>()
|
||||
|
||||
const [imageZoomPercent, setImageZoomPercent] = useState(100)
|
||||
@@ -21,8 +20,8 @@ export const ImagePreview: FunctionComponent<Props> = ({ objectUrl }) => {
|
||||
height: `${imageZoomPercent}%`,
|
||||
...(imageZoomPercent <= 100
|
||||
? {
|
||||
'min-width': '100%',
|
||||
'object-fit': 'contain',
|
||||
minWidth: '100%',
|
||||
objectFit: 'contain',
|
||||
}
|
||||
: {
|
||||
position: 'absolute',
|
||||
@@ -69,3 +68,5 @@ export const ImagePreview: FunctionComponent<Props> = ({ objectUrl }) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImagePreview
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { ImagePreview } from './ImagePreview'
|
||||
import { FunctionComponent } from 'react'
|
||||
import ImagePreview from './ImagePreview'
|
||||
|
||||
type Props = {
|
||||
file: FileItem
|
||||
objectUrl: string
|
||||
}
|
||||
|
||||
export const PreviewComponent: FunctionComponent<Props> = ({ file, objectUrl }) => {
|
||||
const PreviewComponent: FunctionComponent<Props> = ({ file, objectUrl }) => {
|
||||
if (file.mimeType.startsWith('image/')) {
|
||||
return <ImagePreview objectUrl={objectUrl} />
|
||||
}
|
||||
@@ -22,3 +22,5 @@ export const PreviewComponent: FunctionComponent<Props> = ({ file, objectUrl })
|
||||
|
||||
return <object className="w-full h-full" data={objectUrl} />
|
||||
}
|
||||
|
||||
export default PreviewComponent
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
|
||||
} from '@/Strings'
|
||||
import { alertDialog, confirmDialog } from '@/Services/AlertService'
|
||||
import { AccountMenu } from '@/Components/AccountMenu/AccountMenu'
|
||||
import AccountMenu from '@/Components/AccountMenu/AccountMenu'
|
||||
import { AppStateEvent, EventSource } from '@/UIModels/AppState'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { QuickSettingsMenu } from '@/Components/QuickSettingsMenu/QuickSettingsMenu'
|
||||
import { SyncResolutionMenu } from '@/Components/SyncResolutionMenu/SyncResolutionMenu'
|
||||
import { Fragment } from 'preact'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import QuickSettingsMenu from '@/Components/QuickSettingsMenu/QuickSettingsMenu'
|
||||
import SyncResolutionMenu from '@/Components/SyncResolutionMenu/SyncResolutionMenu'
|
||||
import { Fragment } from 'react'
|
||||
import { AccountMenuPane } from '../AccountMenu/AccountMenuPane'
|
||||
|
||||
type Props = {
|
||||
@@ -39,7 +39,7 @@ type State = {
|
||||
arbitraryStatusMessage?: string
|
||||
}
|
||||
|
||||
export class Footer extends PureComponent<Props, State> {
|
||||
class Footer extends PureComponent<Props, State> {
|
||||
public user?: unknown
|
||||
private didCheckForOffline = false
|
||||
private completedInitialSync = false
|
||||
@@ -455,3 +455,5 @@ export class Footer extends PureComponent<Props, State> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Footer
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
|
||||
import {
|
||||
@@ -187,7 +187,7 @@ type Props = {
|
||||
ariaLabel?: string
|
||||
}
|
||||
|
||||
export const Icon: FunctionalComponent<Props> = ({ type, className = '', ariaLabel }) => {
|
||||
const Icon: FunctionComponent<Props> = ({ type, className = '', ariaLabel }) => {
|
||||
const IconComponent = ICONS[type as keyof typeof ICONS]
|
||||
if (!IconComponent) {
|
||||
return null
|
||||
@@ -200,3 +200,5 @@ export const Icon: FunctionalComponent<Props> = ({ type, className = '', ariaLab
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Icon
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FunctionalComponent, Ref } from 'preact'
|
||||
import { forwardRef } from 'preact/compat'
|
||||
import { forwardRef, Fragment, Ref } from 'react'
|
||||
import { DecoratedInputProps } from './DecoratedInputProps'
|
||||
|
||||
const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean) => {
|
||||
@@ -17,7 +16,7 @@ const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean
|
||||
/**
|
||||
* Input that can be decorated on the left and right side
|
||||
*/
|
||||
export const DecoratedInput: FunctionalComponent<DecoratedInputProps> = forwardRef(
|
||||
const DecoratedInput = forwardRef(
|
||||
(
|
||||
{
|
||||
type = 'text',
|
||||
@@ -42,8 +41,8 @@ export const DecoratedInput: FunctionalComponent<DecoratedInputProps> = forwardR
|
||||
<div className={`${classNames.container} ${disabled ? classNames.disabled : ''} ${className}`}>
|
||||
{left && (
|
||||
<div className="flex items-center px-2 py-1.5">
|
||||
{left.map((leftChild) => (
|
||||
<>{leftChild}</>
|
||||
{left.map((leftChild, index) => (
|
||||
<Fragment key={index}>{leftChild}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -58,14 +57,16 @@ export const DecoratedInput: FunctionalComponent<DecoratedInputProps> = forwardR
|
||||
onFocus={onFocus}
|
||||
onKeyDown={onKeyDown}
|
||||
data-lpignore={type !== 'password' ? true : false}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
autoComplete={autocomplete ? 'on' : 'off'}
|
||||
ref={ref}
|
||||
/>
|
||||
|
||||
{right && (
|
||||
<div className="flex items-center px-2 py-1.5">
|
||||
{right.map((rightChild, index) => (
|
||||
<div className={index > 0 ? 'ml-3' : ''}>{rightChild}</div>
|
||||
<div className={index > 0 ? 'ml-3' : ''} key={index}>
|
||||
{rightChild}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -73,3 +74,5 @@ export const DecoratedInput: FunctionalComponent<DecoratedInputProps> = forwardR
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default DecoratedInput
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ComponentChild } from 'preact'
|
||||
import { FocusEventHandler, KeyboardEventHandler, ReactNode } from 'react'
|
||||
|
||||
export type DecoratedInputProps = {
|
||||
type?: 'text' | 'email' | 'password'
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
left?: ComponentChild[]
|
||||
right?: ComponentChild[]
|
||||
left?: ReactNode[]
|
||||
right?: ReactNode[]
|
||||
value?: string
|
||||
placeholder?: string
|
||||
onChange?: (text: string) => void
|
||||
onFocus?: (event: FocusEvent) => void
|
||||
onKeyDown?: (event: KeyboardEvent) => void
|
||||
onFocus?: FocusEventHandler
|
||||
onKeyDown?: KeyboardEventHandler
|
||||
autocomplete?: boolean
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { FunctionComponent, Ref } from 'preact'
|
||||
import { forwardRef } from 'preact/compat'
|
||||
import { StateUpdater, useState } from 'preact/hooks'
|
||||
import { DecoratedInput } from './DecoratedInput'
|
||||
import { IconButton } from '@/Components/Button/IconButton'
|
||||
import { Dispatch, FunctionComponent, Ref, SetStateAction, forwardRef, useState } from 'react'
|
||||
import DecoratedInput from './DecoratedInput'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
import { DecoratedInputProps } from './DecoratedInputProps'
|
||||
|
||||
const Toggle: FunctionComponent<{
|
||||
isToggled: boolean
|
||||
setIsToggled: StateUpdater<boolean>
|
||||
setIsToggled: Dispatch<SetStateAction<boolean>>
|
||||
}> = ({ isToggled, setIsToggled }) => (
|
||||
<IconButton
|
||||
className="w-5 h-5 p-0 justify-center sk-circle hover:bg-passive-4 color-neutral"
|
||||
@@ -22,19 +20,19 @@ const Toggle: FunctionComponent<{
|
||||
/**
|
||||
* Password input that has a toggle to show/hide password and can be decorated on the left and right side
|
||||
*/
|
||||
export const DecoratedPasswordInput: FunctionComponent<Omit<DecoratedInputProps, 'type'>> = forwardRef(
|
||||
(props, ref: Ref<HTMLInputElement>) => {
|
||||
const [isToggled, setIsToggled] = useState(false)
|
||||
const DecoratedPasswordInput = forwardRef((props: DecoratedInputProps, ref: Ref<HTMLInputElement>) => {
|
||||
const [isToggled, setIsToggled] = useState(false)
|
||||
|
||||
const rightSideDecorations = props.right ? [...props.right] : []
|
||||
const rightSideDecorations = props.right ? [...props.right] : []
|
||||
|
||||
return (
|
||||
<DecoratedInput
|
||||
{...props}
|
||||
ref={ref}
|
||||
type={isToggled ? 'text' : 'password'}
|
||||
right={[...rightSideDecorations, <Toggle isToggled={isToggled} setIsToggled={setIsToggled} />]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<DecoratedInput
|
||||
{...props}
|
||||
ref={ref}
|
||||
type={isToggled ? 'text' : 'password'}
|
||||
right={[...rightSideDecorations, <Toggle isToggled={isToggled} setIsToggled={setIsToggled} />]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export default DecoratedPasswordInput
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { FunctionComponent, Ref } from 'preact'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { forwardRef } from 'preact/compat'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { ChangeEventHandler, Ref, forwardRef, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
type: 'text' | 'email' | 'password'
|
||||
label: string
|
||||
value: string
|
||||
onChange: JSXInternal.GenericEventHandler<HTMLInputElement>
|
||||
onChange: ChangeEventHandler<HTMLInputElement>
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
labelClassName?: string
|
||||
@@ -16,7 +13,7 @@ type Props = {
|
||||
isInvalid?: boolean
|
||||
}
|
||||
|
||||
export const FloatingLabelInput: FunctionComponent<Props> = forwardRef(
|
||||
const FloatingLabelInput = forwardRef(
|
||||
(
|
||||
{
|
||||
id,
|
||||
@@ -71,3 +68,5 @@ export const FloatingLabelInput: FunctionComponent<Props> = forwardRef(
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default FloatingLabelInput
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
interface Props {
|
||||
text?: string
|
||||
@@ -6,9 +6,11 @@ interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Input: FunctionalComponent<Props> = ({ className = '', disabled = false, text }) => {
|
||||
const Input: FunctionComponent<Props> = ({ className = '', disabled = false, text }) => {
|
||||
const base = 'rounded py-1.5 px-3 text-input my-1 h-8 bg-contrast'
|
||||
const stateClasses = disabled ? 'no-border' : 'border-solid border-1 border-main'
|
||||
const classes = `${base} ${stateClasses} ${className}`
|
||||
return <input type="text" className={classes} disabled={disabled} value={text} />
|
||||
}
|
||||
|
||||
export default Input
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { JSX, FunctionComponent, ComponentChildren, VNode, RefCallback, ComponentChild, toChildArray } from 'preact'
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { MenuItem, MenuItemListElement } from './MenuItem'
|
||||
import {
|
||||
CSSProperties,
|
||||
FunctionComponent,
|
||||
KeyboardEventHandler,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||
|
||||
type MenuProps = {
|
||||
className?: string
|
||||
style?: string | JSX.CSSProperties | undefined
|
||||
style?: CSSProperties | undefined
|
||||
a11yLabel: string
|
||||
children: ComponentChildren
|
||||
children: ReactNode
|
||||
closeMenu?: () => void
|
||||
isOpen: boolean
|
||||
initialFocus?: number
|
||||
}
|
||||
|
||||
export const Menu: FunctionComponent<MenuProps> = ({
|
||||
const Menu: FunctionComponent<MenuProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
style,
|
||||
@@ -24,16 +29,10 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
isOpen,
|
||||
initialFocus,
|
||||
}: MenuProps) => {
|
||||
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([])
|
||||
|
||||
const menuElementRef = useRef<HTMLMenuElement>(null)
|
||||
|
||||
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLMenuElement> = useCallback(
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLMenuElement> = useCallback(
|
||||
(event) => {
|
||||
if (!menuItemRefs.current) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
closeMenu?.()
|
||||
return
|
||||
@@ -45,58 +44,13 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
useListKeyboardNavigation(menuElementRef, initialFocus)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && menuItemRefs.current.length > 0) {
|
||||
if (isOpen) {
|
||||
setTimeout(() => {
|
||||
menuElementRef.current?.focus()
|
||||
})
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const pushRefToArray: RefCallback<HTMLLIElement> = useCallback((instance) => {
|
||||
if (instance && instance.children) {
|
||||
Array.from(instance.children).forEach((child) => {
|
||||
if (
|
||||
child.getAttribute('role')?.includes('menuitem') &&
|
||||
!menuItemRefs.current.includes(child as HTMLButtonElement)
|
||||
) {
|
||||
menuItemRefs.current.push(child as HTMLButtonElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const mapMenuItems = useCallback(
|
||||
(child: ComponentChild, index: number, array: ComponentChild[]): ComponentChild => {
|
||||
if (!child || (Array.isArray(child) && child.length < 1)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(child)) {
|
||||
return child.map(mapMenuItems)
|
||||
}
|
||||
|
||||
const _child = child as VNode<unknown>
|
||||
const isFirstMenuItem = index === array.findIndex((child) => (child as VNode<unknown>).type === MenuItem)
|
||||
|
||||
const hasMultipleItems = Array.isArray(_child.props.children)
|
||||
? Array.from(_child.props.children as ComponentChild[]).some(
|
||||
(child) => (child as VNode<unknown>).type === MenuItem,
|
||||
)
|
||||
: false
|
||||
|
||||
const items = hasMultipleItems ? [...(_child.props.children as ComponentChild[])] : [_child]
|
||||
|
||||
return items.map((child) => {
|
||||
return (
|
||||
<MenuItemListElement isFirstMenuItem={isFirstMenuItem} ref={pushRefToArray}>
|
||||
{child}
|
||||
</MenuItemListElement>
|
||||
)
|
||||
})
|
||||
},
|
||||
[pushRefToArray],
|
||||
)
|
||||
|
||||
return (
|
||||
<menu
|
||||
className={`m-0 p-0 list-style-none focus:shadow-none ${className}`}
|
||||
@@ -105,7 +59,9 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
||||
style={style}
|
||||
aria-label={a11yLabel}
|
||||
>
|
||||
{toChildArray(children).map(mapMenuItems)}
|
||||
{children}
|
||||
</menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default Menu
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { ComponentChildren, FunctionComponent, VNode } from 'preact'
|
||||
import { forwardRef, Ref } from 'preact/compat'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch, SwitchProps } from '@/Components/Switch/Switch'
|
||||
import { forwardRef, MouseEventHandler, ReactNode, Ref } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { SwitchProps } from '@/Components/Switch/SwitchProps'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
|
||||
export enum MenuItemType {
|
||||
IconButton,
|
||||
RadioButton,
|
||||
SwitchButton,
|
||||
}
|
||||
import { MenuItemType } from './MenuItemType'
|
||||
|
||||
type MenuItemProps = {
|
||||
type: MenuItemType
|
||||
children: ComponentChildren
|
||||
onClick?: JSXInternal.MouseEventHandler<HTMLButtonElement>
|
||||
children: ReactNode
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>
|
||||
onChange?: SwitchProps['onChange']
|
||||
onBlur?: (event: { relatedTarget: EventTarget | null }) => void
|
||||
className?: string
|
||||
@@ -25,7 +19,7 @@ type MenuItemProps = {
|
||||
tabIndex?: number
|
||||
}
|
||||
|
||||
export const MenuItem: FunctionComponent<MenuItemProps> = forwardRef(
|
||||
const MenuItem = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
@@ -42,63 +36,42 @@ export const MenuItem: FunctionComponent<MenuItemProps> = forwardRef(
|
||||
ref: Ref<HTMLButtonElement>,
|
||||
) => {
|
||||
return type === MenuItemType.SwitchButton && typeof onChange === 'function' ? (
|
||||
<button
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between"
|
||||
onClick={() => {
|
||||
onChange(!checked)
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={checked}
|
||||
>
|
||||
<span className="flex flex-grow items-center">{children}</span>
|
||||
<Switch className="px-0" checked={checked} />
|
||||
</button>
|
||||
<li className="list-style-none" role="none">
|
||||
<button
|
||||
ref={ref}
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between"
|
||||
onClick={() => {
|
||||
onChange(!checked)
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={checked}
|
||||
>
|
||||
<span className="flex flex-grow items-center">{children}</span>
|
||||
<Switch className="px-0" checked={checked} />
|
||||
</button>
|
||||
</li>
|
||||
) : (
|
||||
<button
|
||||
ref={ref}
|
||||
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
className={`sn-dropdown-item focus:bg-info-backdrop focus:shadow-none ${className}`}
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
||||
>
|
||||
{type === MenuItemType.IconButton && icon ? <Icon type={icon} className={iconClassName} /> : null}
|
||||
{type === MenuItemType.RadioButton && typeof checked === 'boolean' ? (
|
||||
<div className={`pseudo-radio-btn ${checked ? 'pseudo-radio-btn--checked' : ''} flex-shrink-0`}></div>
|
||||
) : null}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const MenuItemSeparator: FunctionComponent = () => <div role="separator" className="h-1px my-2 bg-border"></div>
|
||||
|
||||
type ListElementProps = {
|
||||
isFirstMenuItem: boolean
|
||||
children: ComponentChildren
|
||||
}
|
||||
|
||||
export const MenuItemListElement: FunctionComponent<ListElementProps> = forwardRef(
|
||||
({ children, isFirstMenuItem }: ListElementProps, ref: Ref<HTMLLIElement>) => {
|
||||
const child = children as VNode<unknown>
|
||||
return (
|
||||
<li className="list-style-none" role="none" ref={ref}>
|
||||
{{
|
||||
...child,
|
||||
props: {
|
||||
...(child.props ? { ...child.props } : {}),
|
||||
...(child.type === MenuItem
|
||||
? {
|
||||
tabIndex: isFirstMenuItem ? 0 : -1,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}}
|
||||
<li className="list-style-none" role="none">
|
||||
<button
|
||||
ref={ref}
|
||||
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
className={`sn-dropdown-item focus:bg-info-backdrop focus:shadow-none ${className}`}
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
||||
>
|
||||
{type === MenuItemType.IconButton && icon ? <Icon type={icon} className={iconClassName} /> : null}
|
||||
{type === MenuItemType.RadioButton && typeof checked === 'boolean' ? (
|
||||
<div className={`pseudo-radio-btn ${checked ? 'pseudo-radio-btn--checked' : ''} flex-shrink-0`}></div>
|
||||
) : null}
|
||||
{children}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default MenuItem
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
const MenuItemSeparator: FunctionComponent = () => (
|
||||
<li className="list-style-none" role="none">
|
||||
<div role="separator" className="h-1px my-2 bg-border" />
|
||||
</li>
|
||||
)
|
||||
|
||||
export default MenuItemSeparator
|
||||
5
app/assets/javascripts/Components/Menu/MenuItemType.ts
Normal file
5
app/assets/javascripts/Components/Menu/MenuItemType.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum MenuItemType {
|
||||
IconButton,
|
||||
RadioButton,
|
||||
SwitchButton,
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { IlNotesIcon } from '@standardnotes/icons'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { PinNoteButton } from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import { Button } from '../Button/Button'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import Button from '../Button/Button'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const MultipleSelectedNotes = observer(({ application, appState }: Props) => {
|
||||
const MultipleSelectedNotes = ({ application, appState }: Props) => {
|
||||
const count = appState.notes.selectedNotesCount
|
||||
|
||||
const cancelMultipleSelection = useCallback(() => {
|
||||
@@ -40,4 +40,6 @@ export const MultipleSelectedNotes = observer(({ application, appState }: Props)
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(MultipleSelectedNotes)
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { SmartViewsSection } from '@/Components/Tags/SmartViewsSection'
|
||||
import { TagsSection } from '@/Components/Tags/TagsSection'
|
||||
import SmartViewsSection from '@/Components/Tags/SmartViewsSection'
|
||||
import TagsSection from '@/Components/Tags/TagsSection'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { PANEL_NAME_NAVIGATION } from '@/Constants'
|
||||
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
export const Navigation: FunctionComponent<Props> = observer(({ application }) => {
|
||||
const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
const appState = useMemo(() => application.getAppState(), [application])
|
||||
const [ref, setRef] = useState<HTMLDivElement | null>()
|
||||
const [panelWidth, setPanelWidth] = useState<number>(0)
|
||||
@@ -79,4 +78,6 @@ export const Navigation: FunctionComponent<Props> = observer(({ application }) =
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(Navigation)
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import { MouseEventHandler, useCallback } from 'react'
|
||||
|
||||
type Props = { appState: AppState }
|
||||
|
||||
export const NoAccountWarning = observer(({ appState }: Props) => {
|
||||
const canShow = appState.noAccountWarning.show
|
||||
if (!canShow) {
|
||||
return null
|
||||
}
|
||||
|
||||
const showAccountMenu = useCallback(
|
||||
(event: Event) => {
|
||||
const NoAccountWarning = observer(({ appState }: Props) => {
|
||||
const showAccountMenu: MouseEventHandler = useCallback(
|
||||
(event) => {
|
||||
event.stopPropagation()
|
||||
appState.accountMenu.setShow(true)
|
||||
},
|
||||
@@ -32,9 +27,9 @@ export const NoAccountWarning = observer(({ appState }: Props) => {
|
||||
</button>
|
||||
<button
|
||||
onClick={hideWarning}
|
||||
title="Ignore"
|
||||
label="Ignore"
|
||||
style="height: 20px"
|
||||
title="Ignore warning"
|
||||
aria-label="Ignore warning"
|
||||
style={{ height: '20px' }}
|
||||
className="border-0 m-0 p-0 bg-transparent cursor-pointer rounded-md col-start-2 row-start-1 color-neutral hover:color-info"
|
||||
>
|
||||
<Icon type="close" className="block" />
|
||||
@@ -42,3 +37,13 @@ export const NoAccountWarning = observer(({ appState }: Props) => {
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
NoAccountWarning.displayName = 'NoAccountWarning'
|
||||
|
||||
const NoAccountWarningWrapper = ({ appState }: Props) => {
|
||||
const canShow = appState.noAccountWarning.show
|
||||
|
||||
return canShow ? <NoAccountWarning appState={appState} /> : null
|
||||
}
|
||||
|
||||
export default observer(NoAccountWarningWrapper)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NoteViewController } from '@standardnotes/snjs'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { MultipleSelectedNotes } from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
||||
import { NoteView } from '@/Components/NoteView/NoteView'
|
||||
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
||||
import NoteView from '@/Components/NoteView/NoteView'
|
||||
import { ElementIds } from '@/ElementIDs'
|
||||
|
||||
type State = {
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
export class NoteGroupView extends PureComponent<Props, State> {
|
||||
class NoteGroupView extends PureComponent<Props, State> {
|
||||
private removeChangeObserver!: () => void
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -70,3 +70,5 @@ export class NoteGroupView extends PureComponent<Props, State> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteGroupView
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import {
|
||||
FocusEventHandler,
|
||||
KeyboardEventHandler,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -9,7 +17,7 @@ type Props = {
|
||||
tag: SNTag
|
||||
}
|
||||
|
||||
export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
const NoteTag = ({ appState, tag }: Props) => {
|
||||
const noteTags = appState.noteTags
|
||||
|
||||
const { autocompleteInputFocused, focusedTagUuid, tags } = noteTags
|
||||
@@ -29,17 +37,16 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
appState.noteTags.removeTagFromActiveNote(tag).catch(console.error)
|
||||
}, [appState, tag])
|
||||
|
||||
const onDeleteTagClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.stopImmediatePropagation()
|
||||
const onDeleteTagClick: MouseEventHandler = useCallback(
|
||||
(event) => {
|
||||
event.stopPropagation()
|
||||
deleteTag()
|
||||
},
|
||||
[deleteTag],
|
||||
)
|
||||
|
||||
const onTagClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
const onTagClick: MouseEventHandler = useCallback(
|
||||
(event) => {
|
||||
if (tagClicked && event.target !== deleteTagRef.current) {
|
||||
setTagClicked(false)
|
||||
appState.tags.selected = tag
|
||||
@@ -55,8 +62,8 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
setShowDeleteButton(true)
|
||||
}, [appState, tag])
|
||||
|
||||
const onBlur = useCallback(
|
||||
(event: FocusEvent) => {
|
||||
const onBlur: FocusEventHandler = useCallback(
|
||||
(event) => {
|
||||
const relatedTarget = event.relatedTarget as Node
|
||||
if (relatedTarget !== deleteTagRef.current) {
|
||||
appState.noteTags.setFocusedTagUuid(undefined)
|
||||
@@ -76,8 +83,8 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
return tags[0].uuid === tag.uuid ? 0 : -1
|
||||
}, [autocompleteInputFocused, tags, tag, focusedTagUuid])
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const onKeyDown: KeyboardEventHandler = useCallback(
|
||||
(event) => {
|
||||
const tagIndex = appState.noteTags.getTagIndex(tag, tags)
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
@@ -136,4 +143,6 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(NoteTag)
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { AutocompleteTagInput } from '@/Components/TagAutocomplete/AutocompleteTagInput'
|
||||
import { NoteTag } from './NoteTag'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
|
||||
import AutocompleteTagInput from '@/Components/TagAutocomplete/AutocompleteTagInput'
|
||||
import NoteTag from './NoteTag'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const NoteTagsContainer = observer(({ appState }: Props) => {
|
||||
if (isStateDealloced(appState)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const NoteTagsContainer = ({ appState }: Props) => {
|
||||
const { tags, tagsContainerMaxWidth } = appState.noteTags
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,4 +28,6 @@ export const NoteTagsContainer = observer(({ appState }: Props) => {
|
||||
<AutocompleteTagInput appState={appState} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(NoteTagsContainer)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '../Icon/Icon'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
type Props = {
|
||||
onMouseLeave: () => void
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
lockText: string
|
||||
}
|
||||
|
||||
export const EditingDisabledBanner: FunctionComponent<Props> = ({
|
||||
const EditingDisabledBanner: FunctionComponent<Props> = ({
|
||||
onMouseLeave,
|
||||
onMouseOver,
|
||||
onClick,
|
||||
@@ -36,3 +36,5 @@ export const EditingDisabledBanner: FunctionComponent<Props> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditingDisabledBanner
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs'
|
||||
|
||||
import { NoteView } from './NoteView'
|
||||
import NoteView from './NoteView'
|
||||
|
||||
describe('NoteView', () => {
|
||||
let noteViewController: NoteViewController
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createRef, JSX, RefObject } from 'preact'
|
||||
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
isPayloadSourceRetrieved,
|
||||
@@ -19,16 +19,16 @@ import { KeyboardModifier, KeyboardKey } from '@/Services/IOService'
|
||||
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Strings'
|
||||
import { confirmDialog } from '@/Services/AlertService'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { ProtectedNoteOverlay } from '@/Components/ProtectedNoteOverlay/ProtectedNoteOverlay'
|
||||
import { PinNoteButton } from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import { NoteTagsContainer } from '@/Components/NoteTags/NoteTagsContainer'
|
||||
import { ComponentView } from '@/Components/ComponentView/ComponentView'
|
||||
import { PanelSide, PanelResizer, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import ProtectedNoteOverlay from '@/Components/ProtectedNoteOverlay/ProtectedNoteOverlay'
|
||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import NoteTagsContainer from '@/Components/NoteTags/NoteTagsContainer'
|
||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||
import PanelResizer, { PanelSide, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { ElementIds } from '@/ElementIDs'
|
||||
import { ChangeEditorButton } from '@/Components/ChangeEditor/ChangeEditorButton'
|
||||
import { AttachedFilesButton } from '@/Components/AttachedFilesPopover/AttachedFilesButton'
|
||||
import { EditingDisabledBanner } from './EditingDisabledBanner'
|
||||
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
|
||||
import AttachedFilesButton from '@/Components/AttachedFilesPopover/AttachedFilesButton'
|
||||
import EditingDisabledBanner from './EditingDisabledBanner'
|
||||
import {
|
||||
transactionForAssociateComponentWithCurrentNote,
|
||||
transactionForDisassociateComponentWithCurrentNote,
|
||||
@@ -78,7 +78,7 @@ type State = {
|
||||
rightResizerOffset: number
|
||||
}
|
||||
|
||||
export class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
readonly controller!: NoteViewController
|
||||
|
||||
private statusTimeout?: NodeJS.Timeout
|
||||
@@ -528,7 +528,7 @@ export class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
}
|
||||
|
||||
onTextAreaChange = ({ currentTarget }: JSX.TargetedEvent<HTMLTextAreaElement, Event>) => {
|
||||
onTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> = ({ currentTarget }) => {
|
||||
const text = currentTarget.value
|
||||
this.setState({
|
||||
editorText: text,
|
||||
@@ -548,12 +548,16 @@ export class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
onTitleEnter = ({ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
||||
onTitleEnter: KeyboardEventHandler<HTMLInputElement> = ({ key, currentTarget }) => {
|
||||
if (key !== KeyboardKey.Enter) {
|
||||
return
|
||||
}
|
||||
|
||||
currentTarget.blur()
|
||||
this.focusEditor()
|
||||
}
|
||||
|
||||
onTitleChange = ({ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
||||
onTitleChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||
const title = currentTarget.value
|
||||
this.setState({
|
||||
editorTitle: title,
|
||||
@@ -911,12 +915,12 @@ export class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
id={ElementIds.NoteTitleEditor}
|
||||
onChange={this.onTitleChange}
|
||||
onFocus={(event) => {
|
||||
;(event.target as HTMLTextAreaElement).select()
|
||||
event.target.select()
|
||||
}}
|
||||
onKeyUp={(event) => event.keyCode == 13 && this.onTitleEnter(event)}
|
||||
spellcheck={false}
|
||||
onKeyUp={this.onTitleEnter}
|
||||
spellCheck={false}
|
||||
value={this.state.editorTitle}
|
||||
autocomplete="off"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -996,15 +1000,15 @@ export class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
|
||||
{this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading && (
|
||||
<textarea
|
||||
autocomplete="off"
|
||||
autoComplete="off"
|
||||
className="editable font-editor"
|
||||
dir="auto"
|
||||
id={ElementIds.NoteTextEditor}
|
||||
onChange={this.onTextAreaChange}
|
||||
value={this.state.editorText}
|
||||
readonly={this.state.noteLocked}
|
||||
readOnly={this.state.noteLocked}
|
||||
onFocus={this.onContentFocus}
|
||||
spellcheck={this.state.spellcheck}
|
||||
spellCheck={this.state.spellcheck}
|
||||
ref={(ref) => ref && this.onSystemEditorLoad(ref)}
|
||||
></textarea>
|
||||
)}
|
||||
@@ -1059,7 +1063,7 @@ export class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
<div className="sn-component">
|
||||
{this.state.stackComponentViewers.map((viewer) => {
|
||||
return (
|
||||
<div className="component-view component-stack-item">
|
||||
<div className="component-view component-stack-item" key={viewer.identifier}>
|
||||
<ComponentView
|
||||
key={viewer.identifier}
|
||||
componentViewer={viewer}
|
||||
@@ -1076,3 +1080,5 @@ export class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteView
|
||||
|
||||
@@ -2,8 +2,8 @@ import { AppState } from '@/UIModels/AppState'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { NotesOptions } from '@/Components/NotesOptions/NotesOptions'
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
||||
import NotesOptions from '@/Components/NotesOptions/NotesOptions'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
|
||||
type Props = {
|
||||
@@ -11,7 +11,7 @@ type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const NotesContextMenu = observer(({ application, appState }: Props) => {
|
||||
const NotesContextMenu = ({ application, appState }: Props) => {
|
||||
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = appState.notes
|
||||
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||
@@ -42,4 +42,6 @@ export const NotesContextMenu = observer(({ application, appState }: Props) => {
|
||||
<NotesOptions application={application} appState={appState} closeOnBlur={closeOnBlur} />
|
||||
</div>
|
||||
) : null
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(NotesContextMenu)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
|
||||
export type AccordionMenuGroup<T> = {
|
||||
icon?: IconType
|
||||
iconClassName?: string
|
||||
title: string
|
||||
items: Array<T>
|
||||
}
|
||||
@@ -2,16 +2,15 @@ import { AppState } from '@/UIModels/AppState'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const AddTagOption: FunctionComponent<Props> = observer(({ appState }) => {
|
||||
const AddTagOption: FunctionComponent<Props> = ({ appState }) => {
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -87,7 +86,7 @@ export const AddTagOption: FunctionComponent<Props> = observer(({ appState }) =>
|
||||
>
|
||||
{appState.tags.tags.map((tag) => (
|
||||
<button
|
||||
key={tag.title}
|
||||
key={tag.uuid}
|
||||
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
|
||||
onBlur={closeOnBlur}
|
||||
onClick={() => {
|
||||
@@ -108,4 +107,6 @@ export const AddTagOption: FunctionComponent<Props> = observer(({ appState }) =>
|
||||
</Disclosure>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(AddTagOption)
|
||||
|
||||
@@ -2,11 +2,10 @@ import { KeyboardKey } from '@/Services/IOService'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { IconType, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { ChangeEditorMenu } from '@/Components/ChangeEditor/ChangeEditorMenu'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ChangeEditorMenu from '@/Components/ChangeEditor/ChangeEditorMenu'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
|
||||
@@ -16,22 +15,7 @@ type ChangeEditorOptionProps = {
|
||||
note: SNNote
|
||||
}
|
||||
|
||||
type AccordionMenuGroup<T> = {
|
||||
icon?: IconType
|
||||
iconClassName?: string
|
||||
title: string
|
||||
items: Array<T>
|
||||
}
|
||||
|
||||
export type EditorMenuItem = {
|
||||
name: string
|
||||
component?: SNComponent
|
||||
isEntitled: boolean
|
||||
}
|
||||
|
||||
export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem>
|
||||
|
||||
export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note }) => {
|
||||
const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
|
||||
@@ -121,3 +105,5 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeEditorOption
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { EditorMenuItem } from './EditorMenuItem'
|
||||
import { AccordionMenuGroup } from './AccordionMenuGroup'
|
||||
|
||||
export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SNComponent } from '@standardnotes/snjs'
|
||||
|
||||
export type EditorMenuItem = {
|
||||
name: string
|
||||
component?: SNComponent
|
||||
isEntitled: boolean
|
||||
}
|
||||
@@ -2,9 +2,8 @@ import { WebApplication } from '@/UIModels/Application'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { Action, ListedAccount, SNNote } from '@standardnotes/snjs'
|
||||
import { Fragment, FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Fragment, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
|
||||
type Props = {
|
||||
@@ -206,7 +205,7 @@ const ListedActionsMenu: FunctionComponent<ListedActionsMenuProps> = ({ applicat
|
||||
)
|
||||
}
|
||||
|
||||
export const ListedActionsOption: FunctionComponent<Props> = ({ application, note }) => {
|
||||
const ListedActionsOption: FunctionComponent<Props> = ({ application, note }) => {
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -273,3 +272,5 @@ export const ListedActionsOption: FunctionComponent<Props> = ({ application, not
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListedActionsOption
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useState, useEffect, useMemo, useCallback } from 'preact/hooks'
|
||||
import { useState, useEffect, useMemo, useCallback, FunctionComponent } from 'react'
|
||||
import { SNApplication, SNNote } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { KeyboardModifier } from '@/Services/IOService'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { ChangeEditorOption } from './ChangeEditorOption'
|
||||
import ChangeEditorOption from './ChangeEditorOption'
|
||||
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants'
|
||||
import { ListedActionsOption } from './ListedActionsOption'
|
||||
import { AddTagOption } from './AddTagOption'
|
||||
import ListedActionsOption from './ListedActionsOption'
|
||||
import AddTagOption from './AddTagOption'
|
||||
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit'
|
||||
|
||||
export type NotesOptionsProps = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
}
|
||||
import { NotesOptionsProps } from './NotesOptionsProps'
|
||||
|
||||
type DeletePermanentlyButtonProps = {
|
||||
closeOnBlur: NotesOptionsProps['closeOnBlur']
|
||||
@@ -176,7 +169,7 @@ const NoteSizeWarning: FunctionComponent<{
|
||||
) : null
|
||||
}
|
||||
|
||||
export const NotesOptions = observer(({ application, appState, closeOnBlur }: NotesOptionsProps) => {
|
||||
const NotesOptions = ({ application, appState, closeOnBlur }: NotesOptionsProps) => {
|
||||
const [altKeyDown, setAltKeyDown] = useState(false)
|
||||
|
||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
||||
@@ -440,4 +433,6 @@ export const NotesOptions = observer(({ application, appState, closeOnBlur }: No
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(NotesOptions)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { useRef, useState } from 'preact/hooks'
|
||||
import { useRef, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { NotesOptions } from './NotesOptions'
|
||||
import NotesOptions from './NotesOptions'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
onClickPreprocessing?: () => Promise<void>
|
||||
}
|
||||
|
||||
export const NotesOptionsPanel = observer(({ application, appState, onClickPreprocessing }: Props) => {
|
||||
const NotesOptionsPanel = ({ application, appState, onClickPreprocessing }: Props) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
@@ -83,4 +83,6 @@ export const NotesOptionsPanel = observer(({ application, appState, onClickPrepr
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(NotesOptionsPanel)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
|
||||
export type NotesOptionsProps = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef } from 'preact/hooks'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
@@ -9,13 +9,6 @@ type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const OtherSessionsSignOutContainer = observer((props: Props) => {
|
||||
if (!props.appState.accountMenu.otherSessionsSignOut) {
|
||||
return null
|
||||
}
|
||||
return <ConfirmOtherSessionsSignOut {...props} />
|
||||
})
|
||||
|
||||
const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props) => {
|
||||
const cancelRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -65,3 +58,14 @@ const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props)
|
||||
</AlertDialog>
|
||||
)
|
||||
})
|
||||
|
||||
ConfirmOtherSessionsSignOut.displayName = 'ConfirmOtherSessionsSignOut'
|
||||
|
||||
const OtherSessionsSignOutContainer = (props: Props) => {
|
||||
if (!props.appState.accountMenu.otherSessionsSignOut) {
|
||||
return null
|
||||
}
|
||||
return <ConfirmOtherSessionsSignOut {...props} />
|
||||
}
|
||||
|
||||
export default observer(OtherSessionsSignOutContainer)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, createRef } from 'preact'
|
||||
import { Component, createRef, MouseEventHandler } from 'react'
|
||||
import { debounce } from '@/Utils'
|
||||
|
||||
export type ResizeFinishCallback = (
|
||||
@@ -38,7 +38,7 @@ type State = {
|
||||
pressed: boolean
|
||||
}
|
||||
|
||||
export class PanelResizer extends Component<Props, State> {
|
||||
class PanelResizer extends Component<Props, State> {
|
||||
private overlay?: HTMLDivElement
|
||||
private resizerElementRef = createRef<HTMLDivElement>()
|
||||
private debouncedResizeHandler: () => void
|
||||
@@ -76,6 +76,10 @@ export class PanelResizer extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
override componentDidMount() {
|
||||
this.resizerElementRef.current?.addEventListener('dblclick', this.onDblClick)
|
||||
}
|
||||
|
||||
override componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.width != prevProps.width) {
|
||||
this.setWidth(this.props.width)
|
||||
@@ -92,6 +96,7 @@ export class PanelResizer extends Component<Props, State> {
|
||||
}
|
||||
|
||||
override componentWillUnmount() {
|
||||
this.resizerElementRef.current?.removeEventListener('dblclick', this.onDblClick)
|
||||
document.removeEventListener('mouseup', this.onMouseUp)
|
||||
document.removeEventListener('mousemove', this.onMouseMove)
|
||||
window.removeEventListener('resize', this.debouncedResizeHandler)
|
||||
@@ -241,7 +246,7 @@ export class PanelResizer extends Component<Props, State> {
|
||||
this.finishSettingWidth()
|
||||
}
|
||||
|
||||
onMouseDown = (event: MouseEvent) => {
|
||||
onMouseDown: MouseEventHandler = (event) => {
|
||||
this.addInvisibleOverlay()
|
||||
this.lastDownX = event.clientX
|
||||
this.startWidth = this.props.panel.scrollWidth
|
||||
@@ -299,16 +304,17 @@ export class PanelResizer extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
override render() {
|
||||
return (
|
||||
<div
|
||||
className={`panel-resizer ${this.props.side} ${this.props.hoverable ? 'hoverable' : ''} ${
|
||||
this.props.alwaysVisible ? 'alwaysVisible' : ''
|
||||
} ${this.state.pressed ? 'dragging' : ''} ${this.state.collapsed ? 'collapsed' : ''}`}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onDblClick={this.onDblClick}
|
||||
ref={this.resizerElementRef}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default PanelResizer
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { createRef, JSX } from 'preact'
|
||||
import { ChangeEventHandler, createRef } from 'react'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
|
||||
interface Props {
|
||||
application: WebApplication
|
||||
dismissModal: () => void
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -31,7 +32,7 @@ type FormData = {
|
||||
status?: string
|
||||
}
|
||||
|
||||
export class PasswordWizard extends PureComponent<Props, State> {
|
||||
class PasswordWizard extends PureComponent<Props, State> {
|
||||
private currentPasswordInput = createRef<HTMLInputElement>()
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -188,7 +189,7 @@ export class PasswordWizard extends PureComponent<Props, State> {
|
||||
if (this.state.lockContinue) {
|
||||
this.application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
|
||||
} else {
|
||||
this.dismissModal()
|
||||
this.props.dismissModal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,19 +202,19 @@ export class PasswordWizard extends PureComponent<Props, State> {
|
||||
})
|
||||
}
|
||||
|
||||
handleCurrentPasswordInputChange = ({ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
||||
handleCurrentPasswordInputChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||
this.setFormDataState({
|
||||
currentPassword: currentTarget.value,
|
||||
}).catch(console.error)
|
||||
}
|
||||
|
||||
handleNewPasswordInputChange = ({ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
||||
handleNewPasswordInputChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||
this.setFormDataState({
|
||||
newPassword: currentTarget.value,
|
||||
}).catch(console.error)
|
||||
}
|
||||
|
||||
handleNewPasswordConfirmationInputChange = ({ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
||||
handleNewPasswordConfirmationInputChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||
this.setFormDataState({
|
||||
newPasswordConfirmation: currentTarget.value,
|
||||
}).catch(console.error)
|
||||
@@ -310,3 +311,5 @@ export class PasswordWizard extends PureComponent<Props, State> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default PasswordWizard
|
||||
|
||||
@@ -1,45 +1,27 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { SNComponent } from '@standardnotes/snjs'
|
||||
import { Component } from 'preact'
|
||||
import { findDOMNode, unmountComponentAtNode } from 'preact/compat'
|
||||
import { Component } from 'react'
|
||||
|
||||
interface Props {
|
||||
application: WebApplication
|
||||
callback: (approved: boolean) => void
|
||||
dismiss: () => void
|
||||
component: SNComponent
|
||||
permissionsString: string
|
||||
}
|
||||
|
||||
export class PermissionsModal extends Component<Props> {
|
||||
getElement(): Element | null {
|
||||
return findDOMNode(this)
|
||||
}
|
||||
|
||||
dismiss = () => {
|
||||
const elem = this.getElement()
|
||||
if (!elem) {
|
||||
return
|
||||
}
|
||||
|
||||
const parent = elem.parentElement
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
parent.remove()
|
||||
unmountComponentAtNode(parent)
|
||||
}
|
||||
|
||||
class PermissionsModal extends Component<Props> {
|
||||
accept = () => {
|
||||
this.props.callback(true)
|
||||
this.dismiss()
|
||||
this.props.dismiss()
|
||||
}
|
||||
|
||||
deny = () => {
|
||||
this.props.callback(false)
|
||||
this.dismiss()
|
||||
this.props.dismiss()
|
||||
}
|
||||
|
||||
render() {
|
||||
override render() {
|
||||
return (
|
||||
<div className="sk-modal">
|
||||
<div onClick={this.deny} className="sk-modal-background" />
|
||||
@@ -88,3 +70,5 @@ export class PermissionsModal extends Component<Props> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default PermissionsModal
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { ApplicationEvent, PermissionDialog } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import PermissionsModal from './PermissionsModal'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const PermissionsModalWrapper: FunctionComponent<Props> = ({ application }) => {
|
||||
const [dialog, setDialog] = useState<PermissionDialog>()
|
||||
|
||||
const presentPermissionsDialog = useCallback((permissionDialog: PermissionDialog) => {
|
||||
setDialog(permissionDialog)
|
||||
}, [])
|
||||
|
||||
const dismissPermissionsDialog = useCallback(() => {
|
||||
setDialog(undefined)
|
||||
}, [])
|
||||
|
||||
const onAppStart = useCallback(() => {
|
||||
application.componentManager.presentPermissionsDialog = presentPermissionsDialog
|
||||
|
||||
return () => {
|
||||
;(application.componentManager.presentPermissionsDialog as unknown) = undefined
|
||||
}
|
||||
}, [application, presentPermissionsDialog])
|
||||
|
||||
useEffect(() => {
|
||||
if (application.isStarted()) {
|
||||
onAppStart()
|
||||
}
|
||||
|
||||
const removeAppObserver = application.addEventObserver(async (eventName) => {
|
||||
if (eventName === ApplicationEvent.Started) {
|
||||
onAppStart()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeAppObserver()
|
||||
}
|
||||
}, [application, onAppStart])
|
||||
|
||||
return dialog ? (
|
||||
<PermissionsModal
|
||||
application={application}
|
||||
callback={dialog.callback}
|
||||
dismiss={dismissPermissionsDialog}
|
||||
component={dialog.component}
|
||||
permissionsString={dialog.permissionsString}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default PermissionsModalWrapper
|
||||
@@ -1,10 +1,8 @@
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
|
||||
import { FunctionComponent, useCallback } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
@@ -12,34 +10,27 @@ type Props = {
|
||||
onClickPreprocessing?: () => Promise<void>
|
||||
}
|
||||
|
||||
export const PinNoteButton: FunctionComponent<Props> = observer(
|
||||
({ appState, className = '', onClickPreprocessing }: Props) => {
|
||||
if (isStateDealloced(appState)) {
|
||||
return null
|
||||
const PinNoteButton: FunctionComponent<Props> = ({ appState, className = '', onClickPreprocessing }: Props) => {
|
||||
const notes = appState.notes.selectedNotes
|
||||
const pinned = notes.some((note) => note.pinned)
|
||||
|
||||
const togglePinned = useCallback(async () => {
|
||||
if (onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
if (!pinned) {
|
||||
appState.notes.setPinSelectedNotes(true)
|
||||
} else {
|
||||
appState.notes.setPinSelectedNotes(false)
|
||||
}
|
||||
}, [appState, onClickPreprocessing, pinned])
|
||||
|
||||
const notes = appState.notes.selectedNotes
|
||||
const pinned = notes.some((note) => note.pinned)
|
||||
return (
|
||||
<button className={`sn-icon-button border-contrast ${pinned ? 'toggled' : ''} ${className}`} onClick={togglePinned}>
|
||||
<VisuallyHidden>Pin selected notes</VisuallyHidden>
|
||||
<Icon type="pin" className="block" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const togglePinned = useCallback(async () => {
|
||||
if (onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
if (!pinned) {
|
||||
appState.notes.setPinSelectedNotes(true)
|
||||
} else {
|
||||
appState.notes.setPinSelectedNotes(false)
|
||||
}
|
||||
}, [appState, onClickPreprocessing, pinned])
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`sn-icon-button border-contrast ${pinned ? 'toggled' : ''} ${className}`}
|
||||
onClick={togglePinned}
|
||||
>
|
||||
<VisuallyHidden>Pin selected notes</VisuallyHidden>
|
||||
<Icon type="pin" className="block" />
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
export default observer(PinNoteButton)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import Backups from '@/Components/Preferences/Panes/Backups/Backups'
|
||||
import Appearance from './Panes/Appearance'
|
||||
import General from './Panes/General/General'
|
||||
import AccountPreferences from './Panes/Account/AccountPreferences'
|
||||
import Security from './Panes/Security/Security'
|
||||
import Listed from './Panes/Listed/Listed'
|
||||
import HelpAndFeedback from './Panes/HelpFeedback'
|
||||
import { PreferencesProps } from './PreferencesProps'
|
||||
|
||||
const PaneSelector: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = ({
|
||||
menu,
|
||||
appState,
|
||||
application,
|
||||
mfaProvider,
|
||||
userProvider,
|
||||
}) => {
|
||||
switch (menu.selectedPaneId) {
|
||||
case 'general':
|
||||
return (
|
||||
<General
|
||||
appState={appState}
|
||||
application={application}
|
||||
extensionsLatestVersions={menu.extensionsLatestVersions}
|
||||
/>
|
||||
)
|
||||
case 'account':
|
||||
return <AccountPreferences application={application} appState={appState} />
|
||||
case 'appearance':
|
||||
return <Appearance application={application} />
|
||||
case 'security':
|
||||
return (
|
||||
<Security mfaProvider={mfaProvider} userProvider={userProvider} appState={appState} application={application} />
|
||||
)
|
||||
case 'backups':
|
||||
return <Backups application={application} appState={appState} />
|
||||
case 'listed':
|
||||
return <Listed application={application} />
|
||||
case 'shortcuts':
|
||||
return null
|
||||
case 'accessibility':
|
||||
return null
|
||||
case 'get-free-month':
|
||||
return null
|
||||
case 'help-feedback':
|
||||
return <HelpAndFeedback />
|
||||
default:
|
||||
return (
|
||||
<General
|
||||
appState={appState}
|
||||
application={application}
|
||||
extensionsLatestVersions={menu.extensionsLatestVersions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default observer(PaneSelector)
|
||||
@@ -1,20 +1,20 @@
|
||||
import { PreferencesPane } from '@/Components/Preferences/PreferencesComponents'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { Authentication } from './Authentication'
|
||||
import { Credentials } from './Credentials'
|
||||
import { Sync } from './Sync'
|
||||
import { Subscription } from './Subscription/Subscription'
|
||||
import { SignOutWrapper } from './SignOutView'
|
||||
import { FilesSection } from './Files'
|
||||
import Authentication from './Authentication'
|
||||
import Credentials from './Credentials'
|
||||
import Sync from './Sync'
|
||||
import Subscription from './Subscription/Subscription'
|
||||
import SignOutWrapper from './SignOutView'
|
||||
import FilesSection from './Files'
|
||||
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const AccountPreferences = observer(({ application, appState }: Props) => (
|
||||
const AccountPreferences = ({ application, appState }: Props) => (
|
||||
<PreferencesPane>
|
||||
{!application.hasAccount() ? (
|
||||
<Authentication application={application} appState={appState} />
|
||||
@@ -28,4 +28,6 @@ export const AccountPreferences = observer(({ application, appState }: Props) =>
|
||||
{application.hasAccount() && appState.features.hasFiles && <FilesSection application={application} />}
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
))
|
||||
)
|
||||
|
||||
export default observer(AccountPreferences)
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { FunctionalComponent } from 'preact'
|
||||
import { PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
|
||||
import { OfflineSubscription } from '@/Components/Preferences/Panes/Account/OfflineSubscription'
|
||||
import { FunctionComponent } from 'react'
|
||||
import OfflineSubscription from '@/Components/Preferences/Panes/Account/OfflineSubscription'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { Extensions } from '@/Components/Preferences/Panes/Extensions/Extensions'
|
||||
import Extensions from '@/Components/Preferences/Panes/Extensions/Extensions'
|
||||
import { ExtensionsLatestVersions } from '@/Components/Preferences/Panes/Extensions/ExtensionsLatestVersions'
|
||||
import { AccordionItem } from '@/Components/Shared/AccordionItem'
|
||||
import AccordionItem from '@/Components/Shared/AccordionItem'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
|
||||
interface IProps {
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
extensionsLatestVersions: ExtensionsLatestVersions
|
||||
}
|
||||
|
||||
export const Advanced: FunctionalComponent<IProps> = observer(({ application, appState, extensionsLatestVersions }) => {
|
||||
const Advanced: FunctionComponent<Props> = ({ application, appState, extensionsLatestVersions }) => {
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
@@ -33,4 +34,6 @@ export const Advanced: FunctionalComponent<IProps> = observer(({ application, ap
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(Advanced)
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/Components/Preferences/PreferencesComponents'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { AccountIllustration } from '@standardnotes/icons'
|
||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
|
||||
export const Authentication: FunctionComponent<{
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
}> = observer(({ appState }) => {
|
||||
}
|
||||
|
||||
const Authentication: FunctionComponent<Props> = ({ appState }) => {
|
||||
const clickSignIn = () => {
|
||||
appState.preferences.closePreferences()
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.SignIn)
|
||||
@@ -43,4 +47,6 @@ export const Authentication: FunctionComponent<{
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default observer(Authentication)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user