diff --git a/app/assets/javascripts/components/AccountMenu/GeneralAccountMenu.tsx b/app/assets/javascripts/components/AccountMenu/GeneralAccountMenu.tsx index f27a910e5..d3feda9a7 100644 --- a/app/assets/javascripts/components/AccountMenu/GeneralAccountMenu.tsx +++ b/app/assets/javascripts/components/AccountMenu/GeneralAccountMenu.tsx @@ -10,10 +10,13 @@ import { AccountMenuPane } from '.'; import { FunctionComponent } from 'preact'; import { Menu } from '../Menu/Menu'; import { MenuItem, MenuItemSeparator, MenuItemType } from '../Menu/MenuItem'; +import { WorkspaceSwitcherOption } from './WorkspaceSwitcher/WorkspaceSwitcherOption'; +import { ApplicationGroup } from '@/ui_models/application_group'; type Props = { appState: AppState; application: WebApplication; + mainApplicationGroup: ApplicationGroup; setMenuPane: (pane: AccountMenuPane) => void; closeMenu: () => void; }; @@ -21,7 +24,7 @@ type Props = { const iconClassName = 'color-neutral mr-2'; export const GeneralAccountMenu: FunctionComponent = observer( - ({ application, appState, setMenuPane, closeMenu }) => { + ({ application, appState, setMenuPane, closeMenu, mainApplicationGroup }) => { const [isSyncingInProgress, setIsSyncingInProgress] = useState(false); const [lastSyncDate, setLastSyncDate] = useState( formatLastSyncDate(application.sync.getLastSyncDate() as Date) @@ -56,7 +59,7 @@ export const GeneralAccountMenu: FunctionComponent = observer( return ( <> -
+
Account
@@ -69,7 +72,7 @@ export const GeneralAccountMenu: FunctionComponent = observer(
{user.email}
{application.getHost()}
-
+
{isSyncingInProgress ? (
@@ -106,12 +109,17 @@ export const GeneralAccountMenu: FunctionComponent = observer(
)} -
+ + + {user ? ( void; + onDelete: () => void; + renameDescriptor: (label: string) => void; +}; + +export const WorkspaceMenuItem: FunctionComponent = ({ + descriptor, + onClick, + onDelete, + renameDescriptor, +}) => { + const [isRenaming, setIsRenaming] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (isRenaming) { + inputRef.current?.focus(); + } + }, [isRenaming]); + + const handleInputKeyDown = (event: KeyboardEvent) => { + if (event.key === KeyboardKey.Enter) { + inputRef.current?.blur(); + } + }; + + const handleInputBlur = (event: FocusEvent) => { + const name = (event.target as HTMLInputElement).value; + renameDescriptor(name); + setIsRenaming(false); + }; + + return ( + +
+ {isRenaming ? ( + + ) : ( +
{descriptor.label}
+ )} + {descriptor.primary && ( +
+ + +
+ )} +
+
+ ); +}; diff --git a/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx b/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx new file mode 100644 index 000000000..1679cf80f --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx @@ -0,0 +1,69 @@ +import { ApplicationGroup } from '@/ui_models/application_group'; +import { AppState } from '@/ui_models/app_state'; +import { ApplicationDescriptor } from '@standardnotes/snjs'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; +import { Icon } from '../../Icon'; +import { Menu } from '../../Menu/Menu'; +import { MenuItem, MenuItemSeparator, MenuItemType } from '../../Menu/MenuItem'; +import { WorkspaceMenuItem } from './WorkspaceMenuItem'; + +type Props = { + mainApplicationGroup: ApplicationGroup; + appState: AppState; + isOpen: boolean; +}; + +export const WorkspaceSwitcherMenu: FunctionComponent = observer( + ({ mainApplicationGroup, appState, isOpen }) => { + const [applicationDescriptors, setApplicationDescriptors] = useState< + ApplicationDescriptor[] + >([]); + + useEffect(() => { + const removeAppGroupObserver = + mainApplicationGroup.addApplicationChangeObserver(() => { + const applicationDescriptors = mainApplicationGroup.getDescriptors(); + setApplicationDescriptors(applicationDescriptors); + }); + + return () => { + removeAppGroupObserver(); + }; + }, [mainApplicationGroup]); + + return ( + + {applicationDescriptors.map((descriptor) => ( + { + appState.accountMenu.setSigningOut(true); + }} + onClick={() => { + mainApplicationGroup.loadApplicationForDescriptor(descriptor); + }} + renameDescriptor={(label: string) => + mainApplicationGroup.renameDescriptor(descriptor, label) + } + /> + ))} + + { + mainApplicationGroup.addNewApplication(); + }} + > + + Add another workspace + + + ); + } +); diff --git a/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx b/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx new file mode 100644 index 000000000..d369c3977 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx @@ -0,0 +1,83 @@ +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants'; +import { ApplicationGroup } from '@/ui_models/application_group'; +import { AppState } from '@/ui_models/app_state'; +import { + calculateSubmenuStyle, + SubmenuStyle, +} from '@/utils/calculateSubmenuStyle'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import { Icon } from '../../Icon'; +import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu'; + +type Props = { + mainApplicationGroup: ApplicationGroup; + appState: AppState; +}; + +export const WorkspaceSwitcherOption: FunctionComponent = observer( + ({ mainApplicationGroup, appState }) => { + const buttonRef = useRef(null); + const menuRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [menuStyle, setMenuStyle] = useState(); + + const toggleMenu = () => { + if (!isOpen) { + const menuPosition = calculateSubmenuStyle(buttonRef.current); + if (menuPosition) { + setMenuStyle(menuPosition); + } + } + + setIsOpen(!isOpen); + }; + + useEffect(() => { + if (isOpen) { + setTimeout(() => { + const newMenuPosition = calculateSubmenuStyle( + buttonRef.current, + menuRef.current + ); + + if (newMenuPosition) { + setMenuStyle(newMenuPosition); + } + }); + } + }, [isOpen]); + + return ( + <> + + {isOpen && ( +
+ +
+ )} + + ); + } +); diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/components/AccountMenu/index.tsx index 8a99b0b93..9e2e1381a 100644 --- a/app/assets/javascripts/components/AccountMenu/index.tsx +++ b/app/assets/javascripts/components/AccountMenu/index.tsx @@ -9,6 +9,7 @@ import { SignInPane } from './SignIn'; import { CreateAccount } from './CreateAccount'; import { ConfirmPassword } from './ConfirmPassword'; import { JSXInternal } from 'preact/src/jsx'; +import { ApplicationGroup } from '@/ui_models/application_group'; export enum AccountMenuPane { GeneralMenu, @@ -21,18 +22,27 @@ type Props = { appState: AppState; application: WebApplication; onClickOutside: () => void; + mainApplicationGroup: ApplicationGroup; }; type PaneSelectorProps = { appState: AppState; application: WebApplication; + mainApplicationGroup: ApplicationGroup; menuPane: AccountMenuPane; setMenuPane: (pane: AccountMenuPane) => void; closeMenu: () => void; }; const MenuPaneSelector: FunctionComponent = observer( - ({ application, appState, menuPane, setMenuPane, closeMenu }) => { + ({ + application, + appState, + menuPane, + setMenuPane, + closeMenu, + mainApplicationGroup, + }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -42,6 +52,7 @@ const MenuPaneSelector: FunctionComponent = observer( @@ -81,7 +92,7 @@ const MenuPaneSelector: FunctionComponent = observer( ); export const AccountMenu: FunctionComponent = observer( - ({ application, appState, onClickOutside }) => { + ({ application, appState, onClickOutside, mainApplicationGroup }) => { const { currentPane, setCurrentPane, @@ -123,6 +134,7 @@ export const AccountMenu: FunctionComponent = observer( { - private removeAppGroupObserver: any; - activeApplication!: WebApplication; - - constructor(props: Props) { - super(props, props.application); - this.removeAppGroupObserver = - props.mainApplicationGroup.addApplicationChangeObserver(() => { - this.activeApplication = props.mainApplicationGroup - .primaryApplication as WebApplication; - this.reloadApplications(); - }); - } - - reloadApplications() { - this.setState({ - descriptors: this.props.mainApplicationGroup.getDescriptors(), - }); - } - - addNewApplication = () => { - this.dismiss(); - this.props.mainApplicationGroup.addNewApplication(); - }; - - selectDescriptor = (descriptor: ApplicationDescriptor) => { - this.dismiss(); - this.props.mainApplicationGroup.loadApplicationForDescriptor(descriptor); - }; - - inputForDescriptor(descriptor: ApplicationDescriptor) { - return document.getElementById(`input-${descriptor.identifier}`); - } - - renameDescriptor = (event: Event, descriptor: ApplicationDescriptor) => { - event.stopPropagation(); - - this.setState({ editingDescriptor: descriptor }); - - setTimeout(() => { - this.inputForDescriptor(descriptor)?.focus(); - }); - }; - - submitRename = () => { - this.props.mainApplicationGroup.renameDescriptor( - this.state.editingDescriptor!, - this.state.editingDescriptor!.label - ); - this.setState({ editingDescriptor: undefined }); - }; - - deinit() { - super.deinit(); - this.removeAppGroupObserver(); - this.removeAppGroupObserver = undefined; - } - - onDescriptorInputChange = ( - descriptor: ApplicationDescriptor, - { currentTarget }: JSX.TargetedEvent - ) => { - descriptor.label = currentTarget.value; - }; - - dismiss = () => { - this.dismissModal(); - }; - - render() { - return ( -
-
-
-
- -
-
- ); - } -} diff --git a/app/assets/javascripts/components/Footer.tsx b/app/assets/javascripts/components/Footer.tsx index 150392624..add25fd55 100644 --- a/app/assets/javascripts/components/Footer.tsx +++ b/app/assets/javascripts/components/Footer.tsx @@ -23,7 +23,6 @@ import { Icon } from './Icon'; import { QuickSettingsMenu } from './QuickSettingsMenu/QuickSettingsMenu'; import { SyncResolutionMenu } from './SyncResolutionMenu'; import { Fragment, render } from 'preact'; -import { AccountSwitcher } from './AccountSwitcher'; /** * Disable before production release. @@ -43,7 +42,6 @@ type State = { dataUpgradeAvailable: boolean; hasPasscode: boolean; descriptors: ApplicationDescriptor[]; - hasAccountSwitcher: boolean; showBetaWarning: boolean; showSyncResolution: boolean; newUpdateAvailable: boolean; @@ -70,7 +68,6 @@ export class Footer extends PureComponent { dataUpgradeAvailable: false, hasPasscode: false, descriptors: props.applicationGroup.getDescriptors(), - hasAccountSwitcher: false, showBetaWarning: false, showSyncResolution: false, newUpdateAvailable: false, @@ -100,7 +97,6 @@ export class Footer extends PureComponent { arbitraryStatusMessage: message, }); }); - this.loadAccountSwitcherState(); this.autorun(() => { const showBetaWarning = this.appState.showBetaWarning; this.setState({ @@ -111,18 +107,6 @@ export class Footer extends PureComponent { }); } - loadAccountSwitcherState() { - const stringValue = localStorage.getItem(ACCOUNT_SWITCHER_FEATURE_KEY); - if (!stringValue && ACCOUNT_SWITCHER_ENABLED) { - /** Enable permanently for this user so they don't lose the feature after its disabled */ - localStorage.setItem(ACCOUNT_SWITCHER_FEATURE_KEY, JSON.stringify(true)); - } - const hasAccountSwitcher = stringValue - ? JSON.parse(stringValue) - : ACCOUNT_SWITCHER_ENABLED; - this.setState({ hasAccountSwitcher }); - } - reloadUpgradeStatus() { this.application.checkForSecurityUpdate().then((available) => { this.setState({ @@ -333,16 +317,6 @@ export class Footer extends PureComponent { } }; - accountSwitcherClickHandler = () => { - render( - , - document.body.appendChild(document.createElement('div')) - ); - }; - accountMenuClickHandler = () => { this.appState.quickSettingsMenu.closeQuickSettingsMenu(); this.appState.accountMenu.toggleShow(); @@ -429,6 +403,7 @@ export class Footer extends PureComponent { onClickOutside={this.clickOutsideAccountMenu} appState={this.appState} application={this.application} + mainApplicationGroup={this.props.applicationGroup} /> )}
@@ -522,24 +497,6 @@ export class Footer extends PureComponent {
Offline
)} - {this.state.hasAccountSwitcher && ( - -
-
-
- -
-
- - )} {this.state.hasPasscode && (
diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index 6257fd188..ecd115fa2 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -16,9 +16,9 @@ import { CheckIcon, ChevronDownIcon, ChevronRightIcon, + ClearCircleFilledIcon, CloseIcon, CloudOffIcon, - ClearCircleFilledIcon, CodeIcon, CopyIcon, DashboardIcon, @@ -83,6 +83,7 @@ import { TuneIcon, UnarchiveIcon, UnpinIcon, + UserAddIcon, UserIcon, UserSwitch, WarningIcon, @@ -99,8 +100,8 @@ export const ICONS = { 'check-circle': CheckCircleIcon, 'chevron-down': ChevronDownIcon, 'chevron-right': ChevronRightIcon, - 'cloud-off': CloudOffIcon, 'clear-circle-filled': ClearCircleFilledIcon, + 'cloud-off': CloudOffIcon, 'eye-off': EyeOffIcon, 'file-doc': FileDocIcon, 'file-image': FileImageIcon, @@ -127,6 +128,7 @@ export const ICONS = { 'rich-text': RichTextIcon, 'trash-filled': TrashFilledIcon, 'trash-sweep': TrashSweepIcon, + 'user-add': UserAddIcon, 'user-switch': UserSwitch, accessibility: AccessibilityIcon, add: AddIcon, diff --git a/app/assets/javascripts/components/Menu/Menu.tsx b/app/assets/javascripts/components/Menu/Menu.tsx index caa56e260..f419f97d5 100644 --- a/app/assets/javascripts/components/Menu/Menu.tsx +++ b/app/assets/javascripts/components/Menu/Menu.tsx @@ -73,8 +73,12 @@ export const Menu: FunctionComponent = ({ child: ComponentChild, index: number, array: ComponentChild[] - ) => { - if (!child) return; + ): ComponentChild => { + if (!child || (Array.isArray(child) && child.length < 1)) return; + + if (Array.isArray(child)) { + return child.map(mapMenuItems); + } const _child = child as VNode; const isFirstMenuItem = diff --git a/app/assets/javascripts/components/Menu/MenuItem.tsx b/app/assets/javascripts/components/Menu/MenuItem.tsx index 5ccfd4f58..ba814fbe4 100644 --- a/app/assets/javascripts/components/Menu/MenuItem.tsx +++ b/app/assets/javascripts/components/Menu/MenuItem.tsx @@ -79,7 +79,7 @@ export const MenuItem: FunctionComponent = forwardRef(
) : null} {children} diff --git a/app/assets/javascripts/utils/calculateSubmenuStyle.tsx b/app/assets/javascripts/utils/calculateSubmenuStyle.tsx index f6791f2a4..874bf32c1 100644 --- a/app/assets/javascripts/utils/calculateSubmenuStyle.tsx +++ b/app/assets/javascripts/utils/calculateSubmenuStyle.tsx @@ -14,7 +14,7 @@ export type SubmenuStyle = { export const calculateSubmenuStyle = ( button: HTMLButtonElement | null, - menu?: HTMLDivElement | null + menu?: HTMLDivElement | HTMLMenuElement | null ): SubmenuStyle | undefined => { const defaultFontSize = window.getComputedStyle( document.documentElement