chore: add options menu to vault selection menu items (#2497)
This commit is contained in:
@@ -2,7 +2,7 @@ import { ReactNode } from 'react'
|
|||||||
|
|
||||||
const MenuListItem = ({ children }: { children: ReactNode }) => {
|
const MenuListItem = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<li className="list-none" role="none">
|
<li className="flex-grow list-none" role="none">
|
||||||
{children}
|
{children}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||||
import { ButtonType, VaultListingInterface, VaultLockServiceEvent, isClientDisplayableError } from '@standardnotes/snjs'
|
import { ButtonType, VaultListingInterface, isClientDisplayableError } from '@standardnotes/snjs'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
@@ -8,6 +8,7 @@ import Icon from '@/Components/Icon/Icon'
|
|||||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||||
import ContactInviteModal from '../Invites/ContactInviteModal'
|
import ContactInviteModal from '../Invites/ContactInviteModal'
|
||||||
import EditVaultModal from './VaultModal/EditVaultModal'
|
import EditVaultModal from './VaultModal/EditVaultModal'
|
||||||
|
import { useVault } from '@/Hooks/useVault'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
vault: VaultListingInterface
|
vault: VaultListingInterface
|
||||||
@@ -22,29 +23,7 @@ const VaultItem = ({ vault }: Props) => {
|
|||||||
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
|
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
|
||||||
const closeVaultModal = () => setIsVaultModalOpen(false)
|
const closeVaultModal = () => setIsVaultModalOpen(false)
|
||||||
|
|
||||||
const isVaultLockable = application.vaultLocks.isVaultLockable(vault)
|
const { isCurrentUserAdmin, isLocked, canShowLockOption, toggleLock, ensureVaultIsUnlocked } = useVault(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 deleteVault = useCallback(async () => {
|
const deleteVault = useCallback(async () => {
|
||||||
const confirm = await application.alerts.confirm(
|
const confirm = await application.alerts.confirm(
|
||||||
@@ -103,14 +82,6 @@ const VaultItem = ({ vault }: Props) => {
|
|||||||
await application.sharedVaults.convertVaultToSharedVault(vault)
|
await application.sharedVaults.convertVaultToSharedVault(vault)
|
||||||
}, [application.sharedVaults, 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 () => {
|
const openEditModal = useCallback(async () => {
|
||||||
if (!(await ensureVaultIsUnlocked())) {
|
if (!(await ensureVaultIsUnlocked())) {
|
||||||
return
|
return
|
||||||
@@ -150,10 +121,10 @@ const VaultItem = ({ vault }: Props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="mt-2 flex w-full flex-wrap gap-3">
|
<div className="mt-2 flex w-full flex-wrap gap-3">
|
||||||
<Button label="Edit" onClick={openEditModal} />
|
<Button label="Edit" onClick={openEditModal} />
|
||||||
{isVaultLockable && <Button label={isVaultLocked ? 'Unlock' : 'Lock'} onClick={toggleLock} />}
|
{canShowLockOption && <Button label={isLocked ? 'Unlock' : 'Lock'} onClick={toggleLock} />}
|
||||||
{isAdmin && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
|
{isCurrentUserAdmin && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
|
||||||
{!isAdmin && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
|
{!isCurrentUserAdmin && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
|
||||||
{isAdmin ? (
|
{isCurrentUserAdmin ? (
|
||||||
vault.isSharedVaultListing() ? (
|
vault.isSharedVaultListing() ? (
|
||||||
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />
|
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />
|
||||||
) : application.hasAccount() ? (
|
) : application.hasAccount() ? (
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||||
import Menu from '../Menu/Menu'
|
import Menu from '../Menu/Menu'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
|
||||||
import Icon from '../Icon/Icon'
|
|
||||||
import { ContentType, VaultListingInterface } from '@standardnotes/snjs'
|
import { ContentType, VaultListingInterface } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import VaultSelectMenuItemWithOptions from './MenuItemWithVaultOption'
|
||||||
|
import Icon from '../Icon/Icon'
|
||||||
|
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||||
|
|
||||||
const ManyVaultSelectionMenu: FunctionComponent = () => {
|
const ManyVaultSelectionMenu: FunctionComponent = () => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
@@ -38,19 +39,22 @@ const ManyVaultSelectionMenu: FunctionComponent = () => {
|
|||||||
<Menu a11yLabel="Vault selection menu" isOpen>
|
<Menu a11yLabel="Vault selection menu" isOpen>
|
||||||
{!vaults.length && <div className="py-1 text-center">No vaults found</div>}
|
{!vaults.length && <div className="py-1 text-center">No vaults found</div>}
|
||||||
{vaults.map((vault) => (
|
{vaults.map((vault) => (
|
||||||
<MenuSwitchButtonItem
|
<VaultSelectMenuItemWithOptions vault={vault}>
|
||||||
onChange={() => {
|
<MenuSwitchButtonItem
|
||||||
toggleVault(vault)
|
className="flex-grow !px-0 focus:!bg-transparent"
|
||||||
}}
|
onChange={() => {
|
||||||
checked={isVaultVisible(vault)}
|
toggleVault(vault)
|
||||||
key={vault.uuid}
|
}}
|
||||||
>
|
checked={isVaultVisible(vault)}
|
||||||
<Icon type={vault.iconString} className="mr-2 text-neutral" />
|
key={vault.uuid}
|
||||||
<div className="flex w-full items-center gap-1">
|
>
|
||||||
{vault.name}
|
<Icon type={vault.iconString} className="mr-2 text-neutral" />
|
||||||
{application.vaultLocks.isVaultLocked(vault) && <Icon className="ml-1" type="lock" size={'small'} />}
|
<div className="flex w-full items-center gap-1">
|
||||||
</div>
|
{vault.name}
|
||||||
</MenuSwitchButtonItem>
|
{application.vaultLocks.isVaultLocked(vault) && <Icon className="ml-1" type="lock" size={'small'} />}
|
||||||
|
</div>
|
||||||
|
</MenuSwitchButtonItem>
|
||||||
|
</VaultSelectMenuItemWithOptions>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -5,6 +5,7 @@ import { ContentType, VaultListingInterface } from '@standardnotes/snjs'
|
|||||||
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
|
import VaultSelectMenuItemWithOptions from './MenuItemWithVaultOption'
|
||||||
|
|
||||||
const SingleVaultSelectionMenu: FunctionComponent = () => {
|
const SingleVaultSelectionMenu: FunctionComponent = () => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
@@ -34,12 +35,17 @@ const SingleVaultSelectionMenu: FunctionComponent = () => {
|
|||||||
<Menu a11yLabel="Vault selection menu" isOpen>
|
<Menu a11yLabel="Vault selection menu" isOpen>
|
||||||
{!vaults.length && <div className="py-1 text-center">No vaults found</div>}
|
{!vaults.length && <div className="py-1 text-center">No vaults found</div>}
|
||||||
{vaults.map((vault) => (
|
{vaults.map((vault) => (
|
||||||
<MenuRadioButtonItem key={vault.uuid} checked={isVaultVisible(vault)} onClick={() => selectVault(vault)}>
|
<VaultSelectMenuItemWithOptions vault={vault}>
|
||||||
<div className="flex w-full items-center gap-1">
|
<MenuRadioButtonItem
|
||||||
|
className="!px-0 focus:!bg-transparent md:!py-[0.455rem]"
|
||||||
|
key={vault.uuid}
|
||||||
|
checked={isVaultVisible(vault)}
|
||||||
|
onClick={() => selectVault(vault)}
|
||||||
|
>
|
||||||
{vault.name}
|
{vault.name}
|
||||||
{application.vaultLocks.isVaultLocked(vault) && <Icon className="ml-1" type="lock" size={'small'} />}
|
{application.vaultLocks.isVaultLocked(vault) && <Icon className="ml-1" type="lock" size={'small'} />}
|
||||||
</div>
|
</MenuRadioButtonItem>
|
||||||
</MenuRadioButtonItem>
|
</VaultSelectMenuItemWithOptions>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
58
packages/web/src/javascripts/Hooks/useVault.ts
Normal file
58
packages/web/src/javascripts/Hooks/useVault.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user