feat: add account switcher menu (#941)
This commit is contained in:
@@ -10,10 +10,13 @@ import { AccountMenuPane } from '.';
|
|||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { Menu } from '../Menu/Menu';
|
import { Menu } from '../Menu/Menu';
|
||||||
import { MenuItem, MenuItemSeparator, MenuItemType } from '../Menu/MenuItem';
|
import { MenuItem, MenuItemSeparator, MenuItemType } from '../Menu/MenuItem';
|
||||||
|
import { WorkspaceSwitcherOption } from './WorkspaceSwitcher/WorkspaceSwitcherOption';
|
||||||
|
import { ApplicationGroup } from '@/ui_models/application_group';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
|
mainApplicationGroup: ApplicationGroup;
|
||||||
setMenuPane: (pane: AccountMenuPane) => void;
|
setMenuPane: (pane: AccountMenuPane) => void;
|
||||||
closeMenu: () => void;
|
closeMenu: () => void;
|
||||||
};
|
};
|
||||||
@@ -21,7 +24,7 @@ type Props = {
|
|||||||
const iconClassName = 'color-neutral mr-2';
|
const iconClassName = 'color-neutral mr-2';
|
||||||
|
|
||||||
export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||||
({ application, appState, setMenuPane, closeMenu }) => {
|
({ application, appState, setMenuPane, closeMenu, mainApplicationGroup }) => {
|
||||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||||
const [lastSyncDate, setLastSyncDate] = useState(
|
const [lastSyncDate, setLastSyncDate] = useState(
|
||||||
formatLastSyncDate(application.sync.getLastSyncDate() as Date)
|
formatLastSyncDate(application.sync.getLastSyncDate() as Date)
|
||||||
@@ -56,7 +59,7 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between px-3 mt-1 mb-3">
|
<div className="flex items-center justify-between px-3 mt-1 mb-1">
|
||||||
<div className="sn-account-menu-headline">Account</div>
|
<div className="sn-account-menu-headline">Account</div>
|
||||||
<div className="flex cursor-pointer" onClick={closeMenu}>
|
<div className="flex cursor-pointer" onClick={closeMenu}>
|
||||||
<Icon type="close" className="color-neutral" />
|
<Icon type="close" className="color-neutral" />
|
||||||
@@ -69,7 +72,7 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
|||||||
<div className="my-0.5 font-bold wrap">{user.email}</div>
|
<div className="my-0.5 font-bold wrap">{user.email}</div>
|
||||||
<span className="color-neutral">{application.getHost()}</span>
|
<span className="color-neutral">{application.getHost()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start justify-between px-3 mb-2">
|
<div className="flex items-start justify-between px-3 mb-3">
|
||||||
{isSyncingInProgress ? (
|
{isSyncingInProgress ? (
|
||||||
<div className="flex items-center color-info font-semibold">
|
<div className="flex items-center color-info font-semibold">
|
||||||
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
|
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
|
||||||
@@ -106,12 +109,17 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="h-1px my-2 bg-border"></div>
|
|
||||||
<Menu
|
<Menu
|
||||||
isOpen={appState.accountMenu.show}
|
isOpen={appState.accountMenu.show}
|
||||||
a11yLabel="General account menu"
|
a11yLabel="General account menu"
|
||||||
closeMenu={closeMenu}
|
closeMenu={closeMenu}
|
||||||
>
|
>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
<WorkspaceSwitcherOption
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
<MenuItemSeparator />
|
||||||
{user ? (
|
{user ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
type={MenuItemType.IconButton}
|
type={MenuItemType.IconButton}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Icon } from '@/components/Icon';
|
||||||
|
import { MenuItem, MenuItemType } from '@/components/Menu/MenuItem';
|
||||||
|
import { KeyboardKey } from '@/services/ioService';
|
||||||
|
import { ApplicationDescriptor } from '@standardnotes/snjs/dist/@types';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
descriptor: ApplicationDescriptor;
|
||||||
|
onClick: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
renameDescriptor: (label: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||||
|
descriptor,
|
||||||
|
onClick,
|
||||||
|
onDelete,
|
||||||
|
renameDescriptor,
|
||||||
|
}) => {
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRenaming) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isRenaming]);
|
||||||
|
|
||||||
|
const handleInputKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === KeyboardKey.Enter) {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = (event: FocusEvent) => {
|
||||||
|
const name = (event.target as HTMLInputElement).value;
|
||||||
|
renameDescriptor(name);
|
||||||
|
setIsRenaming(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
type={MenuItemType.RadioButton}
|
||||||
|
className="sn-dropdown-item py-2 focus:bg-info-backdrop focus:shadow-none"
|
||||||
|
onClick={onClick}
|
||||||
|
checked={descriptor.primary}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
{isRenaming ? (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={descriptor.label}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>{descriptor.label}</div>
|
||||||
|
)}
|
||||||
|
{descriptor.primary && (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="w-5 h-5 p-0 mr-3 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setIsRenaming((isRenaming) => !isRenaming);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="pencil" className="sn-icon--mid color-neutral" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-5 h-5 p-0 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<Icon type="trash" className="sn-icon--mid color-danger" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,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<Props> = observer(
|
||||||
|
({ mainApplicationGroup, appState, isOpen }) => {
|
||||||
|
const [applicationDescriptors, setApplicationDescriptors] = useState<
|
||||||
|
ApplicationDescriptor[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const removeAppGroupObserver =
|
||||||
|
mainApplicationGroup.addApplicationChangeObserver(() => {
|
||||||
|
const applicationDescriptors = mainApplicationGroup.getDescriptors();
|
||||||
|
setApplicationDescriptors(applicationDescriptors);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeAppGroupObserver();
|
||||||
|
};
|
||||||
|
}, [mainApplicationGroup]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
a11yLabel="Workspace switcher menu"
|
||||||
|
className="px-0 focus:shadow-none"
|
||||||
|
isOpen={isOpen}
|
||||||
|
>
|
||||||
|
{applicationDescriptors.map((descriptor) => (
|
||||||
|
<WorkspaceMenuItem
|
||||||
|
descriptor={descriptor}
|
||||||
|
onDelete={() => {
|
||||||
|
appState.accountMenu.setSigningOut(true);
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
mainApplicationGroup.loadApplicationForDescriptor(descriptor);
|
||||||
|
}}
|
||||||
|
renameDescriptor={(label: string) =>
|
||||||
|
mainApplicationGroup.renameDescriptor(descriptor, label)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<MenuItemSeparator />
|
||||||
|
<MenuItem
|
||||||
|
type={MenuItemType.IconButton}
|
||||||
|
onClick={() => {
|
||||||
|
mainApplicationGroup.addNewApplication();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="user-add" className="color-neutral mr-2" />
|
||||||
|
Add another workspace
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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<Props> = observer(
|
||||||
|
({ mainApplicationGroup, appState }) => {
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>();
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
if (!isOpen) {
|
||||||
|
const menuPosition = calculateSubmenuStyle(buttonRef.current);
|
||||||
|
if (menuPosition) {
|
||||||
|
setMenuStyle(menuPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const newMenuPosition = calculateSubmenuStyle(
|
||||||
|
buttonRef.current,
|
||||||
|
menuRef.current
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newMenuPosition) {
|
||||||
|
setMenuStyle(newMenuPosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
|
||||||
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
|
role="menuitem"
|
||||||
|
onClick={toggleMenu}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Icon type="user-switch" className="color-neutral mr-2" />
|
||||||
|
Switch workspace
|
||||||
|
</div>
|
||||||
|
<Icon type="chevron-right" className="color-neutral" />
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto"
|
||||||
|
style={menuStyle}
|
||||||
|
>
|
||||||
|
<WorkspaceSwitcherMenu
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
|
appState={appState}
|
||||||
|
isOpen={isOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -9,6 +9,7 @@ import { SignInPane } from './SignIn';
|
|||||||
import { CreateAccount } from './CreateAccount';
|
import { CreateAccount } from './CreateAccount';
|
||||||
import { ConfirmPassword } from './ConfirmPassword';
|
import { ConfirmPassword } from './ConfirmPassword';
|
||||||
import { JSXInternal } from 'preact/src/jsx';
|
import { JSXInternal } from 'preact/src/jsx';
|
||||||
|
import { ApplicationGroup } from '@/ui_models/application_group';
|
||||||
|
|
||||||
export enum AccountMenuPane {
|
export enum AccountMenuPane {
|
||||||
GeneralMenu,
|
GeneralMenu,
|
||||||
@@ -21,18 +22,27 @@ type Props = {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
onClickOutside: () => void;
|
onClickOutside: () => void;
|
||||||
|
mainApplicationGroup: ApplicationGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaneSelectorProps = {
|
type PaneSelectorProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
|
mainApplicationGroup: ApplicationGroup;
|
||||||
menuPane: AccountMenuPane;
|
menuPane: AccountMenuPane;
|
||||||
setMenuPane: (pane: AccountMenuPane) => void;
|
setMenuPane: (pane: AccountMenuPane) => void;
|
||||||
closeMenu: () => void;
|
closeMenu: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
||||||
({ application, appState, menuPane, setMenuPane, closeMenu }) => {
|
({
|
||||||
|
application,
|
||||||
|
appState,
|
||||||
|
menuPane,
|
||||||
|
setMenuPane,
|
||||||
|
closeMenu,
|
||||||
|
mainApplicationGroup,
|
||||||
|
}) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
@@ -42,6 +52,7 @@ const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
|||||||
<GeneralAccountMenu
|
<GeneralAccountMenu
|
||||||
appState={appState}
|
appState={appState}
|
||||||
application={application}
|
application={application}
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
setMenuPane={setMenuPane}
|
setMenuPane={setMenuPane}
|
||||||
closeMenu={closeMenu}
|
closeMenu={closeMenu}
|
||||||
/>
|
/>
|
||||||
@@ -81,7 +92,7 @@ const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const AccountMenu: FunctionComponent<Props> = observer(
|
export const AccountMenu: FunctionComponent<Props> = observer(
|
||||||
({ application, appState, onClickOutside }) => {
|
({ application, appState, onClickOutside, mainApplicationGroup }) => {
|
||||||
const {
|
const {
|
||||||
currentPane,
|
currentPane,
|
||||||
setCurrentPane,
|
setCurrentPane,
|
||||||
@@ -123,6 +134,7 @@ export const AccountMenu: FunctionComponent<Props> = observer(
|
|||||||
<MenuPaneSelector
|
<MenuPaneSelector
|
||||||
appState={appState}
|
appState={appState}
|
||||||
application={application}
|
application={application}
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
menuPane={currentPane}
|
menuPane={currentPane}
|
||||||
setMenuPane={setCurrentPane}
|
setMenuPane={setCurrentPane}
|
||||||
closeMenu={closeAccountMenu}
|
closeMenu={closeAccountMenu}
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import { ApplicationGroup } from '@/ui_models/application_group';
|
|
||||||
import { WebApplication } from '@/ui_models/application';
|
|
||||||
import { ApplicationDescriptor } from '@standardnotes/snjs';
|
|
||||||
import { PureComponent } from '@/components/Abstract/PureComponent';
|
|
||||||
import { JSX } from 'preact';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
application: WebApplication;
|
|
||||||
mainApplicationGroup: ApplicationGroup;
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
descriptors: ApplicationDescriptor[];
|
|
||||||
editingDescriptor?: ApplicationDescriptor;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AccountSwitcher extends PureComponent<Props, State> {
|
|
||||||
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<HTMLInputElement, Event>
|
|
||||||
) => {
|
|
||||||
descriptor.label = currentTarget.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
dismiss = () => {
|
|
||||||
this.dismissModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="sk-modal">
|
|
||||||
<div onClick={this.dismiss} className="sk-modal-background" />
|
|
||||||
<div id="account-switcher" className="sk-modal-content">
|
|
||||||
<div className="sn-component">
|
|
||||||
<div id="menu-panel" className="sk-menu-panel">
|
|
||||||
<div className="sk-menu-panel-header">
|
|
||||||
<div className="sk-menu-panel-column">
|
|
||||||
<div className="sk-menu-panel-header-title">
|
|
||||||
Account Switcher
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="sk-menu-panel-column">
|
|
||||||
<a onClick={this.addNewApplication} className="sk-label info">
|
|
||||||
Add Account
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.state.descriptors.map((descriptor) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={descriptor.identifier}
|
|
||||||
onClick={() => this.selectDescriptor(descriptor)}
|
|
||||||
className="sk-menu-panel-row"
|
|
||||||
>
|
|
||||||
<div className="sk-menu-panel-column stretch">
|
|
||||||
<div className="left">
|
|
||||||
{descriptor.identifier ==
|
|
||||||
this.activeApplication.identifier && (
|
|
||||||
<div className="sk-menu-panel-column">
|
|
||||||
<div className="sk-circle small success" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="sk-menu-panel-column stretch">
|
|
||||||
<input
|
|
||||||
value={descriptor.label}
|
|
||||||
disabled={
|
|
||||||
descriptor !== this.state.editingDescriptor
|
|
||||||
}
|
|
||||||
onChange={(event) =>
|
|
||||||
this.onDescriptorInputChange(descriptor, event)
|
|
||||||
}
|
|
||||||
onKeyUp={(event) =>
|
|
||||||
event.keyCode == 13 && this.submitRename()
|
|
||||||
}
|
|
||||||
id={`input-${descriptor.identifier}`}
|
|
||||||
spellcheck={false}
|
|
||||||
className="sk-label clickable"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{descriptor.identifier ==
|
|
||||||
this.activeApplication.identifier && (
|
|
||||||
<div className="sk-sublabel">
|
|
||||||
Current Application
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{descriptor.identifier ==
|
|
||||||
this.activeApplication.identifier && (
|
|
||||||
<div className="sk-menu-panel-column">
|
|
||||||
<button
|
|
||||||
onClick={(event) =>
|
|
||||||
this.renameDescriptor(event, descriptor)
|
|
||||||
}
|
|
||||||
className="sn-button success"
|
|
||||||
>
|
|
||||||
Rename
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ import { Icon } from './Icon';
|
|||||||
import { QuickSettingsMenu } from './QuickSettingsMenu/QuickSettingsMenu';
|
import { QuickSettingsMenu } from './QuickSettingsMenu/QuickSettingsMenu';
|
||||||
import { SyncResolutionMenu } from './SyncResolutionMenu';
|
import { SyncResolutionMenu } from './SyncResolutionMenu';
|
||||||
import { Fragment, render } from 'preact';
|
import { Fragment, render } from 'preact';
|
||||||
import { AccountSwitcher } from './AccountSwitcher';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable before production release.
|
* Disable before production release.
|
||||||
@@ -43,7 +42,6 @@ type State = {
|
|||||||
dataUpgradeAvailable: boolean;
|
dataUpgradeAvailable: boolean;
|
||||||
hasPasscode: boolean;
|
hasPasscode: boolean;
|
||||||
descriptors: ApplicationDescriptor[];
|
descriptors: ApplicationDescriptor[];
|
||||||
hasAccountSwitcher: boolean;
|
|
||||||
showBetaWarning: boolean;
|
showBetaWarning: boolean;
|
||||||
showSyncResolution: boolean;
|
showSyncResolution: boolean;
|
||||||
newUpdateAvailable: boolean;
|
newUpdateAvailable: boolean;
|
||||||
@@ -70,7 +68,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
dataUpgradeAvailable: false,
|
dataUpgradeAvailable: false,
|
||||||
hasPasscode: false,
|
hasPasscode: false,
|
||||||
descriptors: props.applicationGroup.getDescriptors(),
|
descriptors: props.applicationGroup.getDescriptors(),
|
||||||
hasAccountSwitcher: false,
|
|
||||||
showBetaWarning: false,
|
showBetaWarning: false,
|
||||||
showSyncResolution: false,
|
showSyncResolution: false,
|
||||||
newUpdateAvailable: false,
|
newUpdateAvailable: false,
|
||||||
@@ -100,7 +97,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
arbitraryStatusMessage: message,
|
arbitraryStatusMessage: message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.loadAccountSwitcherState();
|
|
||||||
this.autorun(() => {
|
this.autorun(() => {
|
||||||
const showBetaWarning = this.appState.showBetaWarning;
|
const showBetaWarning = this.appState.showBetaWarning;
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -111,18 +107,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
reloadUpgradeStatus() {
|
||||||
this.application.checkForSecurityUpdate().then((available) => {
|
this.application.checkForSecurityUpdate().then((available) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -333,16 +317,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
accountSwitcherClickHandler = () => {
|
|
||||||
render(
|
|
||||||
<AccountSwitcher
|
|
||||||
application={this.application}
|
|
||||||
mainApplicationGroup={this.props.applicationGroup}
|
|
||||||
/>,
|
|
||||||
document.body.appendChild(document.createElement('div'))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
accountMenuClickHandler = () => {
|
accountMenuClickHandler = () => {
|
||||||
this.appState.quickSettingsMenu.closeQuickSettingsMenu();
|
this.appState.quickSettingsMenu.closeQuickSettingsMenu();
|
||||||
this.appState.accountMenu.toggleShow();
|
this.appState.accountMenu.toggleShow();
|
||||||
@@ -429,6 +403,7 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
onClickOutside={this.clickOutsideAccountMenu}
|
onClickOutside={this.clickOutsideAccountMenu}
|
||||||
appState={this.appState}
|
appState={this.appState}
|
||||||
application={this.application}
|
application={this.application}
|
||||||
|
mainApplicationGroup={this.props.applicationGroup}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -522,24 +497,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
<div className="sk-label">Offline</div>
|
<div className="sk-label">Offline</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{this.state.hasAccountSwitcher && (
|
|
||||||
<Fragment>
|
|
||||||
<div className="sk-app-bar-item border" />
|
|
||||||
<div
|
|
||||||
onClick={this.accountSwitcherClickHandler}
|
|
||||||
className="sk-app-bar-item"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
(this.state.hasPasscode ? 'alone' : '') +
|
|
||||||
' flex items-center'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon type="user-switch" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
{this.state.hasPasscode && (
|
{this.state.hasPasscode && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="sk-app-bar-item border" />
|
<div className="sk-app-bar-item border" />
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import {
|
|||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
ClearCircleFilledIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
CloudOffIcon,
|
CloudOffIcon,
|
||||||
ClearCircleFilledIcon,
|
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DashboardIcon,
|
DashboardIcon,
|
||||||
@@ -83,6 +83,7 @@ import {
|
|||||||
TuneIcon,
|
TuneIcon,
|
||||||
UnarchiveIcon,
|
UnarchiveIcon,
|
||||||
UnpinIcon,
|
UnpinIcon,
|
||||||
|
UserAddIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
UserSwitch,
|
UserSwitch,
|
||||||
WarningIcon,
|
WarningIcon,
|
||||||
@@ -99,8 +100,8 @@ export const ICONS = {
|
|||||||
'check-circle': CheckCircleIcon,
|
'check-circle': CheckCircleIcon,
|
||||||
'chevron-down': ChevronDownIcon,
|
'chevron-down': ChevronDownIcon,
|
||||||
'chevron-right': ChevronRightIcon,
|
'chevron-right': ChevronRightIcon,
|
||||||
'cloud-off': CloudOffIcon,
|
|
||||||
'clear-circle-filled': ClearCircleFilledIcon,
|
'clear-circle-filled': ClearCircleFilledIcon,
|
||||||
|
'cloud-off': CloudOffIcon,
|
||||||
'eye-off': EyeOffIcon,
|
'eye-off': EyeOffIcon,
|
||||||
'file-doc': FileDocIcon,
|
'file-doc': FileDocIcon,
|
||||||
'file-image': FileImageIcon,
|
'file-image': FileImageIcon,
|
||||||
@@ -127,6 +128,7 @@ export const ICONS = {
|
|||||||
'rich-text': RichTextIcon,
|
'rich-text': RichTextIcon,
|
||||||
'trash-filled': TrashFilledIcon,
|
'trash-filled': TrashFilledIcon,
|
||||||
'trash-sweep': TrashSweepIcon,
|
'trash-sweep': TrashSweepIcon,
|
||||||
|
'user-add': UserAddIcon,
|
||||||
'user-switch': UserSwitch,
|
'user-switch': UserSwitch,
|
||||||
accessibility: AccessibilityIcon,
|
accessibility: AccessibilityIcon,
|
||||||
add: AddIcon,
|
add: AddIcon,
|
||||||
|
|||||||
@@ -73,8 +73,12 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
|||||||
child: ComponentChild,
|
child: ComponentChild,
|
||||||
index: number,
|
index: number,
|
||||||
array: ComponentChild[]
|
array: ComponentChild[]
|
||||||
) => {
|
): ComponentChild => {
|
||||||
if (!child) return;
|
if (!child || (Array.isArray(child) && child.length < 1)) return;
|
||||||
|
|
||||||
|
if (Array.isArray(child)) {
|
||||||
|
return child.map(mapMenuItems);
|
||||||
|
}
|
||||||
|
|
||||||
const _child = child as VNode<unknown>;
|
const _child = child as VNode<unknown>;
|
||||||
const isFirstMenuItem =
|
const isFirstMenuItem =
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const MenuItem: FunctionComponent<MenuItemProps> = forwardRef(
|
|||||||
<div
|
<div
|
||||||
className={`pseudo-radio-btn ${
|
className={`pseudo-radio-btn ${
|
||||||
checked ? 'pseudo-radio-btn--checked' : ''
|
checked ? 'pseudo-radio-btn--checked' : ''
|
||||||
} mr-2`}
|
} mr-2 flex-shrink-0`}
|
||||||
></div>
|
></div>
|
||||||
) : null}
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type SubmenuStyle = {
|
|||||||
|
|
||||||
export const calculateSubmenuStyle = (
|
export const calculateSubmenuStyle = (
|
||||||
button: HTMLButtonElement | null,
|
button: HTMLButtonElement | null,
|
||||||
menu?: HTMLDivElement | null
|
menu?: HTMLDivElement | HTMLMenuElement | null
|
||||||
): SubmenuStyle | undefined => {
|
): SubmenuStyle | undefined => {
|
||||||
const defaultFontSize = window.getComputedStyle(
|
const defaultFontSize = window.getComputedStyle(
|
||||||
document.documentElement
|
document.documentElement
|
||||||
|
|||||||
Reference in New Issue
Block a user