chore: add options menu to vault selection menu items (#2497)

This commit is contained in:
Aman Harwara
2023-09-14 22:02:40 +05:30
committed by GitHub
parent 6759457f39
commit c3996be0fa
7 changed files with 193 additions and 57 deletions

View File

@@ -2,7 +2,7 @@ import { ReactNode } from 'react'
const MenuListItem = ({ children }: { children: ReactNode }) => {
return (
<li className="list-none" role="none">
<li className="flex-grow list-none" role="none">
{children}
</li>
)

View File

@@ -1,6 +1,6 @@
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { ButtonType, VaultListingInterface, VaultLockServiceEvent, isClientDisplayableError } from '@standardnotes/snjs'
import { useCallback, useEffect, useState } from 'react'
import { ButtonType, VaultListingInterface, isClientDisplayableError } from '@standardnotes/snjs'
import { useCallback, useState } from 'react'
import { useApplication } from '@/Components/ApplicationProvider'
import Button from '@/Components/Button/Button'
@@ -8,6 +8,7 @@ import Icon from '@/Components/Icon/Icon'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import ContactInviteModal from '../Invites/ContactInviteModal'
import EditVaultModal from './VaultModal/EditVaultModal'
import { useVault } from '@/Hooks/useVault'
type Props = {
vault: VaultListingInterface
@@ -22,29 +23,7 @@ const VaultItem = ({ vault }: Props) => {
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
const closeVaultModal = () => setIsVaultModalOpen(false)
const isVaultLockable = application.vaultLocks.isVaultLockable(vault)
const [isVaultLocked, setIsVaultLocked] = useState(() => application.vaultLocks.isVaultLocked(vault))
useEffect(() => {
return application.vaultLocks.addEventObserver((event) => {
if (event === VaultLockServiceEvent.VaultLocked || event === VaultLockServiceEvent.VaultUnlocked) {
setIsVaultLocked(application.vaultLocks.isVaultLocked(vault))
}
})
}, [application.vaultLocks, vault])
const toggleLock = useCallback(async () => {
if (!isVaultLockable) {
return
}
if (isVaultLocked) {
application.vaultDisplayService.unlockVault(vault).catch(console.error)
} else {
application.vaultLocks.lockNonPersistentVault(vault).catch(console.error)
}
}, [application.vaultDisplayService, application.vaultLocks, isVaultLockable, isVaultLocked, vault])
const isAdmin = !vault.isSharedVaultListing() ? true : application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
const { isCurrentUserAdmin, isLocked, canShowLockOption, toggleLock, ensureVaultIsUnlocked } = useVault(vault)
const deleteVault = useCallback(async () => {
const confirm = await application.alerts.confirm(
@@ -103,14 +82,6 @@ const VaultItem = ({ vault }: Props) => {
await application.sharedVaults.convertVaultToSharedVault(vault)
}, [application.sharedVaults, vault])
const ensureVaultIsUnlocked = useCallback(async () => {
if (!application.vaultLocks.isVaultLocked(vault)) {
return true
}
const unlocked = await application.vaultDisplayService.unlockVault(vault)
return unlocked
}, [application, vault])
const openEditModal = useCallback(async () => {
if (!(await ensureVaultIsUnlocked())) {
return
@@ -150,10 +121,10 @@ const VaultItem = ({ vault }: Props) => {
)}
<div className="mt-2 flex w-full flex-wrap gap-3">
<Button label="Edit" onClick={openEditModal} />
{isVaultLockable && <Button label={isVaultLocked ? 'Unlock' : 'Lock'} onClick={toggleLock} />}
{isAdmin && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
{!isAdmin && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
{isAdmin ? (
{canShowLockOption && <Button label={isLocked ? 'Unlock' : 'Lock'} onClick={toggleLock} />}
{isCurrentUserAdmin && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
{!isCurrentUserAdmin && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
{isCurrentUserAdmin ? (
vault.isSharedVaultListing() ? (
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />
) : application.hasAccount() ? (

View File

@@ -1,10 +1,11 @@
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import Menu from '../Menu/Menu'
import { useApplication } from '../ApplicationProvider'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
import Icon from '../Icon/Icon'
import { ContentType, VaultListingInterface } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import VaultSelectMenuItemWithOptions from './MenuItemWithVaultOption'
import Icon from '../Icon/Icon'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
const ManyVaultSelectionMenu: FunctionComponent = () => {
const application = useApplication()
@@ -38,19 +39,22 @@ const ManyVaultSelectionMenu: FunctionComponent = () => {
<Menu a11yLabel="Vault selection menu" isOpen>
{!vaults.length && <div className="py-1 text-center">No vaults found</div>}
{vaults.map((vault) => (
<MenuSwitchButtonItem
onChange={() => {
toggleVault(vault)
}}
checked={isVaultVisible(vault)}
key={vault.uuid}
>
<Icon type={vault.iconString} className="mr-2 text-neutral" />
<div className="flex w-full items-center gap-1">
{vault.name}
{application.vaultLocks.isVaultLocked(vault) && <Icon className="ml-1" type="lock" size={'small'} />}
</div>
</MenuSwitchButtonItem>
<VaultSelectMenuItemWithOptions vault={vault}>
<MenuSwitchButtonItem
className="flex-grow !px-0 focus:!bg-transparent"
onChange={() => {
toggleVault(vault)
}}
checked={isVaultVisible(vault)}
key={vault.uuid}
>
<Icon type={vault.iconString} className="mr-2 text-neutral" />
<div className="flex w-full items-center gap-1">
{vault.name}
{application.vaultLocks.isVaultLocked(vault) && <Icon className="ml-1" type="lock" size={'small'} />}
</div>
</MenuSwitchButtonItem>
</VaultSelectMenuItemWithOptions>
))}
</Menu>
)

View File

@@ -0,0 +1,49 @@
import { VaultListingInterface, classNames } from '@standardnotes/snjs'
import { useState, useRef } from 'react'
import Icon from '../Icon/Icon'
import VaultOptionsMenu from './VaultOptionsMenu'
import Popover from '../Popover/Popover'
const VaultSelectMenuItemWithOptions = ({
vault,
children,
}: {
vault: VaultListingInterface
children: React.ReactNode
}) => {
const [isOptionsMenuOpen, setIsOptionsMenuOpen] = useState(false)
const optionsButtonRef = useRef<HTMLButtonElement>(null)
const toggleOptionsMenu = () => {
setIsOptionsMenuOpen((open) => !open)
}
return (
<div className="group flex items-center gap-3 px-3 focus-within:bg-info-backdrop">
{children}
<button
className={classNames(
'flex-shrink-0 rounded-full border border-border p-1 hover:bg-default focus:bg-default group-focus-within:bg-default',
isOptionsMenuOpen && 'bg-default',
)}
onClick={toggleOptionsMenu}
ref={optionsButtonRef}
>
<Icon type="more" size="small" />
</button>
<Popover
title="Vault options"
open={isOptionsMenuOpen}
anchorElement={optionsButtonRef}
side="top"
align="start"
className="py-1"
togglePopover={toggleOptionsMenu}
>
<VaultOptionsMenu vault={vault} />
</Popover>
</div>
)
}
export default VaultSelectMenuItemWithOptions

View File

@@ -5,6 +5,7 @@ import { ContentType, VaultListingInterface } from '@standardnotes/snjs'
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
import { observer } from 'mobx-react-lite'
import Icon from '../Icon/Icon'
import VaultSelectMenuItemWithOptions from './MenuItemWithVaultOption'
const SingleVaultSelectionMenu: FunctionComponent = () => {
const application = useApplication()
@@ -34,12 +35,17 @@ const SingleVaultSelectionMenu: FunctionComponent = () => {
<Menu a11yLabel="Vault selection menu" isOpen>
{!vaults.length && <div className="py-1 text-center">No vaults found</div>}
{vaults.map((vault) => (
<MenuRadioButtonItem key={vault.uuid} checked={isVaultVisible(vault)} onClick={() => selectVault(vault)}>
<div className="flex w-full items-center gap-1">
<VaultSelectMenuItemWithOptions vault={vault}>
<MenuRadioButtonItem
className="!px-0 focus:!bg-transparent md:!py-[0.455rem]"
key={vault.uuid}
checked={isVaultVisible(vault)}
onClick={() => selectVault(vault)}
>
{vault.name}
{application.vaultLocks.isVaultLocked(vault) && <Icon className="ml-1" type="lock" size={'small'} />}
</div>
</MenuRadioButtonItem>
</MenuRadioButtonItem>
</VaultSelectMenuItemWithOptions>
))}
</Menu>
)

View File

@@ -0,0 +1,48 @@
import { VaultListingInterface } from '@standardnotes/snjs'
import Menu from '../Menu/Menu'
import MenuItem from '../Menu/MenuItem'
import Icon from '../Icon/Icon'
import { useState, useCallback } from 'react'
import EditVaultModal from '../Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal'
import { useVault } from '@/Hooks/useVault'
type Props = {
vault: VaultListingInterface
}
const VaultOptionsMenu = ({ vault }: Props) => {
const { canShowLockOption, isLocked, toggleLock, ensureVaultIsUnlocked } = useVault(vault)
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
const openEditModal = useCallback(async () => {
if (!(await ensureVaultIsUnlocked())) {
return
}
setIsVaultModalOpen(true)
}, [ensureVaultIsUnlocked])
return (
<>
<Menu a11yLabel="Vault options menu" isOpen>
<MenuItem onClick={openEditModal}>
<Icon type="pencil-filled" className="mr-2" />
Edit vault
</MenuItem>
{canShowLockOption && (
<MenuItem onClick={toggleLock}>
<Icon type="lock" className="mr-2" />
{isLocked ? 'Unlock' : 'Lock'} vault
</MenuItem>
)}
</Menu>
<EditVaultModal
vault={vault}
isVaultModalOpen={isVaultModalOpen}
closeVaultModal={() => setIsVaultModalOpen(false)}
/>
</>
)
}
export default VaultOptionsMenu

View File

@@ -0,0 +1,58 @@
import { useApplication } from '@/Components/ApplicationProvider'
import {
KeySystemPasswordType,
KeySystemRootKeyStorageMode,
VaultListingInterface,
VaultLockServiceEvent,
} from '@standardnotes/snjs'
import { useState, useEffect, useCallback } from 'react'
export const useVault = (vault: VaultListingInterface) => {
const application = useApplication()
const canShowLockOption =
vault.keyPasswordType === KeySystemPasswordType.UserInputted &&
vault.keyStorageMode === KeySystemRootKeyStorageMode.Ephemeral
const [isLocked, setIsLocked] = useState(() => application.vaultLocks.isVaultLocked(vault))
useEffect(() => {
return application.vaultLocks.addEventObserver((event) => {
if (event === VaultLockServiceEvent.VaultLocked || event === VaultLockServiceEvent.VaultUnlocked) {
setIsLocked(application.vaultLocks.isVaultLocked(vault))
}
})
}, [application.vaultLocks, vault])
const toggleLock = useCallback(async () => {
if (!canShowLockOption) {
return
}
if (isLocked) {
application.vaultDisplayService.unlockVault(vault).catch(console.error)
} else {
application.vaultLocks.lockNonPersistentVault(vault).catch(console.error)
}
}, [application.vaultDisplayService, application.vaultLocks, canShowLockOption, isLocked, vault])
const isCurrentUserAdmin = !vault.isSharedVaultListing()
? true
: application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
const ensureVaultIsUnlocked = useCallback(async () => {
if (!application.vaultLocks.isVaultLocked(vault)) {
return true
}
const unlocked = await application.vaultDisplayService.unlockVault(vault)
return unlocked
}, [application, vault])
return {
canShowLockOption,
isLocked,
toggleLock,
ensureVaultIsUnlocked,
isCurrentUserAdmin,
}
}