chore: ui for designating survivor for shared vault & warning when no survivor (#2544)

This commit is contained in:
Aman Harwara
2023-09-29 16:38:38 +05:30
committed by GitHub
parent 7c8816e229
commit 79c5da6c5b
8 changed files with 162 additions and 15 deletions

View File

@@ -4,4 +4,5 @@ export interface SharedVaultUserServerHash {
user_uuid: string user_uuid: string
permission: string permission: string
updated_at_timestamp: number updated_at_timestamp: number
is_designated_survivor: boolean
} }

View File

@@ -49,6 +49,7 @@ describe('SharedVaultService', () => {
eventBus.addEventHandler = jest.fn() eventBus.addEventHandler = jest.fn()
service = new SharedVaultService( service = new SharedVaultService(
sync,
items, items,
session, session,
vaultUsers, vaultUsers,

View File

@@ -34,12 +34,14 @@ import { FindContact } from '../Contacts/UseCase/FindContact'
import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults' import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults'
import { SyncLocalVaultsWithRemoteSharedVaults } from './UseCase/SyncLocalVaultsWithRemoteSharedVaults' import { SyncLocalVaultsWithRemoteSharedVaults } from './UseCase/SyncLocalVaultsWithRemoteSharedVaults'
import { VaultUserServiceInterface } from '../VaultUser/VaultUserServiceInterface' import { VaultUserServiceInterface } from '../VaultUser/VaultUserServiceInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
export class SharedVaultService export class SharedVaultService
extends AbstractService<SharedVaultServiceEvent, SharedVaultServiceEventPayload> extends AbstractService<SharedVaultServiceEvent, SharedVaultServiceEventPayload>
implements SharedVaultServiceInterface, InternalEventHandlerInterface implements SharedVaultServiceInterface, InternalEventHandlerInterface
{ {
constructor( constructor(
private sync: SyncServiceInterface,
private items: ItemManagerInterface, private items: ItemManagerInterface,
private session: SessionsClientInterface, private session: SessionsClientInterface,
private vaultUsers: VaultUserServiceInterface, private vaultUsers: VaultUserServiceInterface,
@@ -123,9 +125,13 @@ export class SharedVaultService
.invalidateVaultUsersCache(event.eventPayload.props.primaryIdentifier.value) .invalidateVaultUsersCache(event.eventPayload.props.primaryIdentifier.value)
.catch(console.error) .catch(console.error)
await this._syncLocalVaultsWithRemoteSharedVaults.execute([vault]) this.sync
.sync()
void this.notifyEvent(SharedVaultServiceEvent.SharedVaultStatusChanged) .then(async () => {
await this._syncLocalVaultsWithRemoteSharedVaults.execute([vault])
void this.notifyEvent(SharedVaultServiceEvent.SharedVaultStatusChanged)
})
.catch(console.error)
} }
break break
} }

View File

@@ -921,6 +921,7 @@ export class Dependencies {
this.factory.set(TYPES.SharedVaultService, () => { this.factory.set(TYPES.SharedVaultService, () => {
return new SharedVaultService( return new SharedVaultService(
this.get<SyncService>(TYPES.SyncService),
this.get<ItemManager>(TYPES.ItemManager), this.get<ItemManager>(TYPES.ItemManager),
this.get<SessionManager>(TYPES.SessionManager), this.get<SessionManager>(TYPES.SessionManager),
this.get<VaultUserService>(TYPES.VaultUserService), this.get<VaultUserService>(TYPES.VaultUserService),

View File

@@ -44,9 +44,11 @@ export class VaultsContext extends AppContext {
await this.awaitPromiseOrDoNothing( await this.awaitPromiseOrDoNothing(
promise, promise,
1, 1,
'Waiting for notifications timed out. Notifications might have been processed in previous sync.' 'Waiting for notifications timed out. Notifications might have been processed in previous sync.',
) )
await this.sync()
if (this.notifications['handleReceivedNotifications'].restore) { if (this.notifications['handleReceivedNotifications'].restore) {
this.notifications['handleReceivedNotifications'].restore() this.notifications['handleReceivedNotifications'].restore()
} }

View File

@@ -0,0 +1,96 @@
import { useApplication } from '@/Components/ApplicationProvider'
import Modal, { ModalAction } from '@/Components/Modal/Modal'
import Spinner from '@/Components/Spinner/Spinner'
import { SharedVaultUserServerHash, VaultListingInterface } from '@standardnotes/snjs'
import { useCallback, useMemo, useState } from 'react'
const DesignateSurvivorModal = ({
vault,
members,
closeModal,
}: {
vault: VaultListingInterface
members: SharedVaultUserServerHash[]
closeModal: () => void
}) => {
const application = useApplication()
const [selectedSurvivor, setSelectedSurvivor] = useState<SharedVaultUserServerHash | null>(null)
const [isDesignating, setIsDesignating] = useState(false)
const designateSelectedSurvivor = useCallback(async () => {
if (!selectedSurvivor) {
return
}
if (!vault.isSharedVaultListing()) {
return
}
try {
setIsDesignating(true)
const result = await application.vaultUsers.designateSurvivor(vault, selectedSurvivor.user_uuid)
if (result.isFailed()) {
throw new Error(result.getError())
}
await application.sync.sync()
closeModal()
} catch (error) {
console.error(error)
} finally {
setIsDesignating(false)
}
}, [application.sync, application.vaultUsers, closeModal, selectedSurvivor, vault])
const modalActions = useMemo(
(): ModalAction[] => [
{
label: isDesignating ? <Spinner className="h-5 w-5 border-info-contrast" /> : 'Designate survivor',
onClick: designateSelectedSurvivor,
type: 'primary',
mobileSlot: 'right',
disabled: !selectedSurvivor || isDesignating,
hidden: members.length === 0,
},
{
label: 'Cancel',
onClick: closeModal,
type: 'cancel',
mobileSlot: 'left',
},
],
[closeModal, designateSelectedSurvivor, isDesignating, members.length, selectedSurvivor],
)
return (
<Modal title="Designate survivor" close={closeModal} actions={modalActions} className="px-4.5 py-4">
<div className="flex flex-col gap-3">
{members.map((member) => {
const isSelected = selectedSurvivor?.uuid === member.uuid
const contact = application.contacts.findContactForServerUser(member)
if (!contact) {
return null
}
const isOwner = application.vaultUsers.isVaultUserOwner(member)
if (isOwner) {
return null
}
return (
<label className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5" key={member.uuid}>
<input
className="h-4 w-4 self-center accent-info"
type="radio"
name="survivor"
checked={isSelected}
onClick={() => setSelectedSurvivor(member)}
/>
<div className="col-start-2 text-sm font-semibold">{contact.name}</div>
<div className="col-start-2 opacity-90">{contact.contactUuid}</div>
</label>
)
})}
</div>
</Modal>
)
}
export default DesignateSurvivorModal

View File

@@ -1,8 +1,10 @@
import { useCallback } from 'react' import { useCallback, useState } from 'react'
import { useApplication } from '@/Components/ApplicationProvider' import { useApplication } from '@/Components/ApplicationProvider'
import { SharedVaultUserServerHash, VaultListingInterface } from '@standardnotes/snjs' import { SharedVaultUserServerHash, VaultListingInterface } from '@standardnotes/snjs'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import Button from '@/Components/Button/Button' import Button from '@/Components/Button/Button'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import DesignateSurvivorModal from './DesignateSurvivorModal'
export const VaultModalMembers = ({ export const VaultModalMembers = ({
members, members,
@@ -27,9 +29,30 @@ export const VaultModalMembers = ({
[application.vaultUsers, vault, onChange], [application.vaultUsers, vault, onChange],
) )
const vaultHasNoDesignatedSurvivor = vault.isSharedVaultListing() && !vault.sharing.designatedSurvivor
const [isDesignateSurvivorModalOpen, setIsDesignateSurvivorModalOpen] = useState(false)
const openDesignateSurvivorModal = () => setIsDesignateSurvivorModalOpen(true)
const closeDesignateSurvivorModal = () => setIsDesignateSurvivorModalOpen(false)
return ( return (
<div> <div>
<div className="mb-3 text-lg">Vault Members</div> <div className="mb-3 text-lg">Vault Members</div>
{vaultHasNoDesignatedSurvivor && members.length > 1 && isCurrentUserAdmin && (
<div className="bg-danger-faded mb-3 grid grid-cols-[auto,1fr] gap-x-[0.65rem] gap-y-0.5 overflow-hidden rounded p-2.5 text-danger">
<Icon type="warning" className="place-self-center" />
<div className="text-base font-semibold">No designated survivor</div>
<div className="col-start-2">
Vaults that have no designated survivor will be deleted when the owner account is deleted. In order to
ensure that no data is lost, please designate a survivor who will be transferred ownership of the vault.
</div>
<Button small className="col-start-2 mt-1.5" onClick={openDesignateSurvivorModal}>
Designate survivor
</Button>
<ModalOverlay isOpen={isDesignateSurvivorModalOpen} close={closeDesignateSurvivorModal}>
<DesignateSurvivorModal vault={vault} members={members} closeModal={closeDesignateSurvivorModal} />
</ModalOverlay>
</div>
)}
<div className="space-y-3.5"> <div className="space-y-3.5">
{members.map((member) => { {members.map((member) => {
const isMemberVaultOwner = application.vaultUsers.isVaultUserOwner(member) const isMemberVaultOwner = application.vaultUsers.isVaultUserOwner(member)
@@ -55,6 +78,12 @@ export const VaultModalMembers = ({
Untrusted Untrusted
</div> </div>
)} )}
{member.is_designated_survivor && (
<div className="flex items-center gap-1 rounded bg-info px-1 py-0.5 text-xs text-success-contrast">
<Icon type="security" size="small" />
Designated survivor
</div>
)}
</div> </div>
<div className="col-start-2 row-start-2">{permission}</div> <div className="col-start-2 row-start-2">{permission}</div>
{isCurrentUserAdmin && !isMemberVaultOwner && ( {isCurrentUserAdmin && !isMemberVaultOwner && (

View File

@@ -116,21 +116,32 @@
} }
@mixin DimmedBackground($color, $opacity) { @mixin DimmedBackground($color, $opacity) {
content: ''; position: relative;
width: 100%; z-index: 1;
height: 100%;
position: absolute; &::after {
left: 0; content: '';
top: 0; width: 100%;
background-color: $color; height: 100%;
opacity: $opacity; position: absolute;
left: 0;
top: 0;
background-color: $color;
opacity: $opacity;
z-index: -1;
pointer-events: none;
}
} }
.bg-warning-faded::after { .bg-warning-faded {
@include DimmedBackground(var(--sn-stylekit-warning-color), 0.08); @include DimmedBackground(var(--sn-stylekit-warning-color), 0.08);
} }
.bg-info-faded::after { .bg-danger-faded {
@include DimmedBackground(var(--sn-stylekit-danger-color), 0.08);
}
.bg-info-faded {
@include DimmedBackground(var(--sn-stylekit-info-color), 0.08); @include DimmedBackground(var(--sn-stylekit-info-color), 0.08);
} }