feat: add workspace switcher to lock screen (#969)
This commit is contained in:
@@ -10,6 +10,7 @@ type Props = {
|
|||||||
onClick: () => void
|
onClick: () => void
|
||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
renameDescriptor: (label: string) => void
|
renameDescriptor: (label: string) => void
|
||||||
|
hideOptions: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||||
@@ -17,6 +18,7 @@ export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
onDelete,
|
onDelete,
|
||||||
renameDescriptor,
|
renameDescriptor,
|
||||||
|
hideOptions,
|
||||||
}) => {
|
}) => {
|
||||||
const [isRenaming, setIsRenaming] = useState(false)
|
const [isRenaming, setIsRenaming] = useState(false)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -58,7 +60,7 @@ export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div>{descriptor.label}</div>
|
<div>{descriptor.label}</div>
|
||||||
)}
|
)}
|
||||||
{descriptor.primary && (
|
{descriptor.primary && !hideOptions && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="w-5 h-5 p-0 mr-3 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
className="w-5 h-5 p-0 mr-3 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ type Props = {
|
|||||||
mainApplicationGroup: ApplicationGroup
|
mainApplicationGroup: ApplicationGroup
|
||||||
appState: AppState
|
appState: AppState
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
hideWorkspaceOptions?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
|
export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
|
||||||
({ mainApplicationGroup, appState, isOpen }) => {
|
({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }) => {
|
||||||
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>(
|
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>(
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
@@ -37,6 +38,7 @@ export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
|
|||||||
{applicationDescriptors.map((descriptor) => (
|
{applicationDescriptors.map((descriptor) => (
|
||||||
<WorkspaceMenuItem
|
<WorkspaceMenuItem
|
||||||
descriptor={descriptor}
|
descriptor={descriptor}
|
||||||
|
hideOptions={hideWorkspaceOptions}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
appState.accountMenu.setSigningOut(true)
|
appState.accountMenu.setSigningOut(true)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ export class ApplicationView extends PureComponent<Props, State> {
|
|||||||
<ChallengeModal
|
<ChallengeModal
|
||||||
key={challenge.id}
|
key={challenge.id}
|
||||||
application={this.application}
|
application={this.application}
|
||||||
|
appState={this.appState}
|
||||||
|
mainApplicationGroup={this.props.mainApplicationGroup}
|
||||||
challenge={challenge}
|
challenge={challenge}
|
||||||
onDismiss={this.removeChallenge}
|
onDismiss={this.removeChallenge}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import { useCallback, useEffect, useState } from 'preact/hooks'
|
|||||||
import { Button } from '@/Components/Button/Button'
|
import { Button } from '@/Components/Button/Button'
|
||||||
import { Icon } from '@/Components/Icon'
|
import { Icon } from '@/Components/Icon'
|
||||||
import { ChallengeModalPrompt } from './ChallengePrompt'
|
import { ChallengeModalPrompt } from './ChallengePrompt'
|
||||||
|
import { LockscreenWorkspaceSwitcher } from './LockscreenWorkspaceSwitcher'
|
||||||
|
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||||
|
import { AppState } from '@/UIModels/AppState'
|
||||||
|
|
||||||
type InputValue = {
|
type InputValue = {
|
||||||
prompt: ChallengePrompt
|
prompt: ChallengePrompt
|
||||||
@@ -25,6 +28,8 @@ export type ChallengeModalValues = Record<ChallengePrompt['id'], InputValue>
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
|
appState: AppState
|
||||||
|
mainApplicationGroup: ApplicationGroup
|
||||||
challenge: Challenge
|
challenge: Challenge
|
||||||
onDismiss: (challenge: Challenge) => Promise<void>
|
onDismiss: (challenge: Challenge) => Promise<void>
|
||||||
}
|
}
|
||||||
@@ -48,7 +53,13 @@ const validateValues = (
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChallengeModal: FunctionComponent<Props> = ({ application, challenge, onDismiss }) => {
|
export const ChallengeModal: FunctionComponent<Props> = ({
|
||||||
|
application,
|
||||||
|
appState,
|
||||||
|
mainApplicationGroup,
|
||||||
|
challenge,
|
||||||
|
onDismiss,
|
||||||
|
}) => {
|
||||||
const [values, setValues] = useState<ChallengeModalValues>(() => {
|
const [values, setValues] = useState<ChallengeModalValues>(() => {
|
||||||
const values = {} as ChallengeModalValues
|
const values = {} as ChallengeModalValues
|
||||||
for (const prompt of challenge.prompts) {
|
for (const prompt of challenge.prompts) {
|
||||||
@@ -68,6 +79,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({ application, challeng
|
|||||||
ChallengeReason.ApplicationUnlock,
|
ChallengeReason.ApplicationUnlock,
|
||||||
ChallengeReason.Migration,
|
ChallengeReason.Migration,
|
||||||
].includes(challenge.reason)
|
].includes(challenge.reason)
|
||||||
|
const shouldShowWorkspaceSwitcher = challenge.reason === ChallengeReason.ApplicationUnlock
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const validatedValues = validateValues(values, challenge.prompts)
|
const validatedValues = validateValues(values, challenge.prompts)
|
||||||
@@ -252,6 +264,12 @@ export const ChallengeModal: FunctionComponent<Props> = ({ application, challeng
|
|||||||
Forgot passcode?
|
Forgot passcode?
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{shouldShowWorkspaceSwitcher && (
|
||||||
|
<LockscreenWorkspaceSwitcher
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogOverlay>
|
</DialogOverlay>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||||
|
import { AppState } from '@/UIModels/AppState'
|
||||||
|
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||||
|
import { FunctionComponent } from 'preact'
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||||
|
import { WorkspaceSwitcherMenu } from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu'
|
||||||
|
import { Button } from '@/Components/Button/Button'
|
||||||
|
import { Icon } from '@/Components/Icon'
|
||||||
|
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
mainApplicationGroup: ApplicationGroup
|
||||||
|
appState: AppState
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({
|
||||||
|
mainApplicationGroup,
|
||||||
|
appState,
|
||||||
|
}) => {
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
|
||||||
|
|
||||||
|
useCloseOnClickOutside(containerRef, () => setIsOpen(false))
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
if (!isOpen) {
|
||||||
|
const menuPosition = calculateSubmenuStyle(buttonRef.current)
|
||||||
|
if (menuPosition) {
|
||||||
|
setMenuStyle(menuPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen(!isOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
const timeToWaitBeforeCheckingMenuCollision = 0
|
||||||
|
setTimeout(() => {
|
||||||
|
const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current)
|
||||||
|
|
||||||
|
if (newMenuPosition) {
|
||||||
|
setMenuStyle(newMenuPosition)
|
||||||
|
}
|
||||||
|
}, timeToWaitBeforeCheckingMenuCollision)
|
||||||
|
}
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef}>
|
||||||
|
<Button
|
||||||
|
ref={buttonRef}
|
||||||
|
onClick={toggleMenu}
|
||||||
|
className="flex items-center justify-center min-w-76 mt-2"
|
||||||
|
>
|
||||||
|
<Icon type="user-switch" className="color-neutral mr-2" />
|
||||||
|
Switch workspace
|
||||||
|
</Button>
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto"
|
||||||
|
style={menuStyle}
|
||||||
|
>
|
||||||
|
<WorkspaceSwitcherMenu
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
|
appState={appState}
|
||||||
|
isOpen={isOpen}
|
||||||
|
hideWorkspaceOptions={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,12 @@ import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
|
|||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent } from 'preact'
|
import { FunctionComponent } from 'preact'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||||
import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer'
|
import {
|
||||||
|
PanelSide,
|
||||||
|
ResizeFinishCallback,
|
||||||
|
PanelResizer,
|
||||||
|
PanelResizeType,
|
||||||
|
} from '@/Components/PanelResizer'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import { NoAccountWarning } from '@/Components/NoAccountWarning'
|
|||||||
import { NotesList } from '@/Components/NotesList'
|
import { NotesList } from '@/Components/NotesList'
|
||||||
import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu'
|
import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu'
|
||||||
import { SearchOptions } from '@/Components/SearchOptions'
|
import { SearchOptions } from '@/Components/SearchOptions'
|
||||||
import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer'
|
import {
|
||||||
|
PanelSide,
|
||||||
|
ResizeFinishCallback,
|
||||||
|
PanelResizer,
|
||||||
|
PanelResizeType,
|
||||||
|
} from '@/Components/PanelResizer'
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { FunctionalComponent } from 'preact'
|
import { FunctionalComponent } from 'preact'
|
||||||
import { PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
} from '@/Components/Preferences/PreferencesComponents'
|
||||||
import { OfflineSubscription } from '@/Components/Preferences/Panes/Account/OfflineSubscription'
|
import { OfflineSubscription } from '@/Components/Preferences/Panes/Account/OfflineSubscription'
|
||||||
import { WebApplication } from '@/UIModels/Application'
|
import { WebApplication } from '@/UIModels/Application'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { PreferencesGroup, PreferencesSegment, Title } from '@/Components/Preferences/PreferencesComponents'
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
Title,
|
||||||
|
} from '@/Components/Preferences/PreferencesComponents'
|
||||||
import { WebApplication } from '@/UIModels/Application'
|
import { WebApplication } from '@/UIModels/Application'
|
||||||
import { SubscriptionInformation } from './SubscriptionInformation'
|
import { SubscriptionInformation } from './SubscriptionInformation'
|
||||||
import { NoSubscription } from './NoSubscription'
|
import { NoSubscription } from './NoSubscription'
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ import { JSXInternal } from 'preact/src/jsx'
|
|||||||
import TargetedEvent = JSXInternal.TargetedEvent
|
import TargetedEvent = JSXInternal.TargetedEvent
|
||||||
import { AppState } from '@/UIModels/AppState'
|
import { AppState } from '@/UIModels/AppState'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { PreferencesGroup, PreferencesSegment, Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents'
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Subtitle,
|
||||||
|
} from '@/Components/Preferences/PreferencesComponents'
|
||||||
import { Button } from '@/Components/Button/Button'
|
import { Button } from '@/Components/Button/Button'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Strings'
|
|||||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||||
import { WebApplication } from '@/UIModels/Application'
|
import { WebApplication } from '@/UIModels/Application'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { PreferencesGroup, PreferencesSegment, Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents'
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
Subtitle,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@/Components/Preferences/PreferencesComponents'
|
||||||
import { Dropdown, DropdownItem } from '@/Components/Dropdown'
|
import { Dropdown, DropdownItem } from '@/Components/Dropdown'
|
||||||
import { Switch } from '@/Components/Switch'
|
import { Switch } from '@/Components/Switch'
|
||||||
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { DisplayStringForContentType, SNComponent } from '@standardnotes/snjs'
|
import { DisplayStringForContentType, SNComponent } from '@standardnotes/snjs'
|
||||||
import { Button } from '@/Components/Button/Button'
|
import { Button } from '@/Components/Button/Button'
|
||||||
import { FunctionComponent } from 'preact'
|
import { FunctionComponent } from 'preact'
|
||||||
import { Title, Text, Subtitle, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
|
import {
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Subtitle,
|
||||||
|
PreferencesSegment,
|
||||||
|
} from '@/Components/Preferences/PreferencesComponents'
|
||||||
|
|
||||||
export const ConfirmCustomExtension: FunctionComponent<{
|
export const ConfirmCustomExtension: FunctionComponent<{
|
||||||
component: SNComponent
|
component: SNComponent
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { FunctionComponent } from 'preact'
|
import { FunctionComponent } from 'preact'
|
||||||
import { SNComponent } from '@standardnotes/snjs'
|
import { SNComponent } from '@standardnotes/snjs'
|
||||||
import { PreferencesSegment, SubtitleLight, Title } from '@/Components/Preferences/PreferencesComponents'
|
import {
|
||||||
|
PreferencesSegment,
|
||||||
|
SubtitleLight,
|
||||||
|
Title,
|
||||||
|
} from '@/Components/Preferences/PreferencesComponents'
|
||||||
import { Switch } from '@/Components/Switch'
|
import { Switch } from '@/Components/Switch'
|
||||||
import { WebApplication } from '@/UIModels/Application'
|
import { WebApplication } from '@/UIModels/Application'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_LOCAL_ENC_ENABLED }
|
|||||||
import { AppState } from '@/UIModels/AppState'
|
import { AppState } from '@/UIModels/AppState'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { ComponentChild, FunctionComponent } from 'preact'
|
import { ComponentChild, FunctionComponent } from 'preact'
|
||||||
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/Components/Preferences/PreferencesComponents'
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@/Components/Preferences/PreferencesComponents'
|
||||||
|
|
||||||
const formatCount = (count: number, itemType: string) => `${count} / ${count} ${itemType}`
|
const formatCount = (count: number, itemType: string) => `${count} / ${count} ${itemType}`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { FunctionComponent } from 'preact'
|
import { FunctionComponent } from 'preact'
|
||||||
import { Title, Text, PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
|
import {
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
} from '@/Components/Preferences/PreferencesComponents'
|
||||||
import { Switch } from '@/Components/Switch'
|
import { Switch } from '@/Components/Switch'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth'
|
import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth'
|
||||||
|
|||||||
Reference in New Issue
Block a user