chore: vault member permissions (#2509)
This commit is contained in:
@@ -191,7 +191,7 @@ export class VaultInviteService
|
|||||||
public async getInvitableContactsForSharedVault(
|
public async getInvitableContactsForSharedVault(
|
||||||
sharedVault: SharedVaultListingInterface,
|
sharedVault: SharedVaultListingInterface,
|
||||||
): Promise<TrustedContactInterface[]> {
|
): Promise<TrustedContactInterface[]> {
|
||||||
const users = await this.vaultUsers.getSharedVaultUsers(sharedVault)
|
const users = await this.vaultUsers.getSharedVaultUsersFromServer(sharedVault)
|
||||||
if (!users) {
|
if (!users) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { UserServiceInterface } from '../../User/UserServiceInterface'
|
||||||
|
import { Result, SharedVaultUserPermission, SyncUseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
import { SharedVaultListingInterface } from '@standardnotes/models'
|
||||||
|
import { VaultUserCache } from '../VaultUserCache'
|
||||||
|
|
||||||
|
export class IsReadonlyVaultMember implements SyncUseCaseInterface<boolean> {
|
||||||
|
constructor(
|
||||||
|
private users: UserServiceInterface,
|
||||||
|
private cache: VaultUserCache,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
execute(vault: SharedVaultListingInterface): Result<boolean> {
|
||||||
|
if (!vault.sharing) {
|
||||||
|
return Result.ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vault.sharing.ownerUserUuid) {
|
||||||
|
throw new Error(`Shared vault ${vault.sharing.sharedVaultUuid} does not have an owner user uuid`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = this.users.sureUser
|
||||||
|
const vaultUsers = this.cache.get(vault.sharing.sharedVaultUuid)
|
||||||
|
const userPermission = vaultUsers?.find((vaultUser) => vaultUser.user_uuid === user.uuid)?.permission
|
||||||
|
|
||||||
|
return Result.ok(userPermission === SharedVaultUserPermission.PERMISSIONS.Read)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { UserServiceInterface } from '../../User/UserServiceInterface'
|
||||||
|
import { Result, SharedVaultUserPermission, SyncUseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
import { SharedVaultListingInterface } from '@standardnotes/models'
|
||||||
|
import { VaultUserCache } from '../VaultUserCache'
|
||||||
|
|
||||||
|
export class IsVaultAdmin implements SyncUseCaseInterface<boolean> {
|
||||||
|
constructor(
|
||||||
|
private users: UserServiceInterface,
|
||||||
|
private cache: VaultUserCache,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
execute(vault: SharedVaultListingInterface): Result<boolean> {
|
||||||
|
if (!vault.sharing) {
|
||||||
|
return Result.ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vault.sharing.ownerUserUuid) {
|
||||||
|
throw new Error(`Shared vault ${vault.sharing.sharedVaultUuid} does not have an owner user uuid`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = this.users.sureUser
|
||||||
|
const vaultUsers = this.cache.get(vault.sharing.sharedVaultUuid)
|
||||||
|
const userPermission = vaultUsers?.find((vaultUser) => vaultUser.user_uuid === user.uuid)?.permission
|
||||||
|
|
||||||
|
return Result.ok(
|
||||||
|
userPermission === SharedVaultUserPermission.PERMISSIONS.Admin || vault.sharing.ownerUserUuid === user.uuid,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,21 +5,31 @@ import { InternalEventBusInterface } from './../Internal/InternalEventBusInterfa
|
|||||||
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
|
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
|
||||||
import { VaultServiceInterface } from '../Vault/VaultServiceInterface'
|
import { VaultServiceInterface } from '../Vault/VaultServiceInterface'
|
||||||
import { GetVaultUsers } from './UseCase/GetVaultUsers'
|
import { GetVaultUsers } from './UseCase/GetVaultUsers'
|
||||||
import { SharedVaultListingInterface } from '@standardnotes/models'
|
import { SharedVaultListingInterface, VaultListingInterface } from '@standardnotes/models'
|
||||||
import { VaultUserServiceInterface } from './VaultUserServiceInterface'
|
import { VaultUserServiceInterface } from './VaultUserServiceInterface'
|
||||||
import { ClientDisplayableError, SharedVaultUserServerHash, isClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError, SharedVaultUserServerHash, isClientDisplayableError } from '@standardnotes/responses'
|
||||||
import { AbstractService } from './../Service/AbstractService'
|
import { AbstractService } from './../Service/AbstractService'
|
||||||
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
|
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
|
||||||
import { Result } from '@standardnotes/domain-core'
|
import { Result } from '@standardnotes/domain-core'
|
||||||
import { IsVaultOwner } from './UseCase/IsVaultOwner'
|
import { IsVaultOwner } from './UseCase/IsVaultOwner'
|
||||||
|
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||||
|
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||||
|
import { ApplicationEvent } from '../Event/ApplicationEvent'
|
||||||
|
import { IsReadonlyVaultMember } from './UseCase/IsReadonlyVaultMember'
|
||||||
|
import { IsVaultAdmin } from './UseCase/IsVaultAdmin'
|
||||||
|
|
||||||
export class VaultUserService extends AbstractService<VaultUserServiceEvent> implements VaultUserServiceInterface {
|
export class VaultUserService
|
||||||
|
extends AbstractService<VaultUserServiceEvent>
|
||||||
|
implements VaultUserServiceInterface, InternalEventHandlerInterface
|
||||||
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private vaults: VaultServiceInterface,
|
private vaults: VaultServiceInterface,
|
||||||
private vaultLocks: VaultLockServiceInterface,
|
private vaultLocks: VaultLockServiceInterface,
|
||||||
private _getVaultUsers: GetVaultUsers,
|
private _getVaultUsers: GetVaultUsers,
|
||||||
private _removeVaultMember: RemoveVaultMember,
|
private _removeVaultMember: RemoveVaultMember,
|
||||||
private _isVaultOwner: IsVaultOwner,
|
private _isVaultOwner: IsVaultOwner,
|
||||||
|
private _isVaultAdmin: IsVaultAdmin,
|
||||||
|
private _isReadonlyVaultMember: IsReadonlyVaultMember,
|
||||||
private _getVault: GetVault,
|
private _getVault: GetVault,
|
||||||
private _leaveVault: LeaveVault,
|
private _leaveVault: LeaveVault,
|
||||||
eventBus: InternalEventBusInterface,
|
eventBus: InternalEventBusInterface,
|
||||||
@@ -37,7 +47,23 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
|
|||||||
;(this._leaveVault as unknown) = undefined
|
;(this._leaveVault as unknown) = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSharedVaultUsers(
|
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||||
|
if (event.type === ApplicationEvent.CompletedFullSync) {
|
||||||
|
this.vaults.getVaults().forEach((vault) => {
|
||||||
|
if (!vault.isSharedVaultListing()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._getVaultUsers
|
||||||
|
.execute({
|
||||||
|
sharedVaultUuid: vault.sharing.sharedVaultUuid,
|
||||||
|
readFromCache: false,
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSharedVaultUsersFromServer(
|
||||||
sharedVault: SharedVaultListingInterface,
|
sharedVault: SharedVaultListingInterface,
|
||||||
): Promise<SharedVaultUserServerHash[] | undefined> {
|
): Promise<SharedVaultUserServerHash[] | undefined> {
|
||||||
const result = await this._getVaultUsers.execute({
|
const result = await this._getVaultUsers.execute({
|
||||||
@@ -55,6 +81,17 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
|
|||||||
return this._isVaultOwner.execute(sharedVault).getValue()
|
return this._isVaultOwner.execute(sharedVault).getValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean {
|
||||||
|
return this._isVaultAdmin.execute(sharedVault).getValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
public isCurrentUserReadonlyVaultMember(vault: VaultListingInterface): boolean {
|
||||||
|
if (!vault.isSharedVaultListing()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this._isReadonlyVaultMember.execute(vault).getValue()
|
||||||
|
}
|
||||||
|
|
||||||
async removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>> {
|
async removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>> {
|
||||||
if (!this.isCurrentUserSharedVaultOwner(sharedVault)) {
|
if (!this.isCurrentUserSharedVaultOwner(sharedVault)) {
|
||||||
throw new Error('Only vault admins can remove users')
|
throw new Error('Only vault admins can remove users')
|
||||||
@@ -101,4 +138,17 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
|
|||||||
|
|
||||||
void this.notifyEvent(VaultUserServiceEvent.UsersChanged)
|
void this.notifyEvent(VaultUserServiceEvent.UsersChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFormattedMemberPermission(permission: string): string {
|
||||||
|
switch (permission) {
|
||||||
|
case 'admin':
|
||||||
|
return 'Admin'
|
||||||
|
case 'write':
|
||||||
|
return 'Read / Write'
|
||||||
|
case 'read':
|
||||||
|
return 'Read-only'
|
||||||
|
default:
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
|
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
|
||||||
import { SharedVaultListingInterface } from '@standardnotes/models'
|
import { SharedVaultListingInterface, VaultListingInterface } from '@standardnotes/models'
|
||||||
import { ClientDisplayableError, SharedVaultUserServerHash } from '@standardnotes/responses'
|
import { ClientDisplayableError, SharedVaultUserServerHash } from '@standardnotes/responses'
|
||||||
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
|
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
|
||||||
import { Result } from '@standardnotes/domain-core'
|
import { Result } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
export interface VaultUserServiceInterface extends ApplicationServiceInterface<VaultUserServiceEvent, unknown> {
|
export interface VaultUserServiceInterface extends ApplicationServiceInterface<VaultUserServiceEvent, unknown> {
|
||||||
getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise<SharedVaultUserServerHash[] | undefined>
|
getSharedVaultUsersFromServer(
|
||||||
|
sharedVault: SharedVaultListingInterface,
|
||||||
|
): Promise<SharedVaultUserServerHash[] | undefined>
|
||||||
isCurrentUserSharedVaultOwner(sharedVault: SharedVaultListingInterface): boolean
|
isCurrentUserSharedVaultOwner(sharedVault: SharedVaultListingInterface): boolean
|
||||||
|
isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean
|
||||||
|
isCurrentUserReadonlyVaultMember(vault: VaultListingInterface): boolean
|
||||||
removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>>
|
removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>>
|
||||||
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
|
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
|
||||||
isVaultUserOwner(user: SharedVaultUserServerHash): boolean
|
isVaultUserOwner(user: SharedVaultUserServerHash): boolean
|
||||||
|
getFormattedMemberPermission(permission: string): string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ import { EncryptionOperators } from '@standardnotes/encryption'
|
|||||||
import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
|
import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
|
||||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
import { AuthorizeVaultDeletion } from '@standardnotes/services/src/Domain/Vault/UseCase/AuthorizeVaultDeletion'
|
import { AuthorizeVaultDeletion } from '@standardnotes/services/src/Domain/Vault/UseCase/AuthorizeVaultDeletion'
|
||||||
|
import { IsVaultAdmin } from '@standardnotes/services/src/Domain/VaultUser/UseCase/IsVaultAdmin'
|
||||||
|
import { IsReadonlyVaultMember } from '@standardnotes/services/src/Domain/VaultUser/UseCase/IsReadonlyVaultMember'
|
||||||
|
|
||||||
export class Dependencies {
|
export class Dependencies {
|
||||||
private factory = new Map<symbol, () => unknown>()
|
private factory = new Map<symbol, () => unknown>()
|
||||||
@@ -335,6 +337,17 @@ export class Dependencies {
|
|||||||
return new IsVaultOwner(this.get<UserService>(TYPES.UserService))
|
return new IsVaultOwner(this.get<UserService>(TYPES.UserService))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.factory.set(TYPES.IsVaultAdmin, () => {
|
||||||
|
return new IsVaultAdmin(this.get<UserService>(TYPES.UserService), this.get<VaultUserCache>(TYPES.VaultUserCache))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.factory.set(TYPES.IsReadonlyVaultMember, () => {
|
||||||
|
return new IsReadonlyVaultMember(
|
||||||
|
this.get<UserService>(TYPES.UserService),
|
||||||
|
this.get<VaultUserCache>(TYPES.VaultUserCache),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
this.factory.set(TYPES.DecryptBackupFile, () => {
|
this.factory.set(TYPES.DecryptBackupFile, () => {
|
||||||
return new DecryptBackupFile(
|
return new DecryptBackupFile(
|
||||||
this.get<EncryptionService>(TYPES.EncryptionService),
|
this.get<EncryptionService>(TYPES.EncryptionService),
|
||||||
@@ -844,6 +857,8 @@ export class Dependencies {
|
|||||||
this.get<GetVaultUsers>(TYPES.GetVaultUsers),
|
this.get<GetVaultUsers>(TYPES.GetVaultUsers),
|
||||||
this.get<RemoveVaultMember>(TYPES.RemoveVaultMember),
|
this.get<RemoveVaultMember>(TYPES.RemoveVaultMember),
|
||||||
this.get<IsVaultOwner>(TYPES.IsVaultOwner),
|
this.get<IsVaultOwner>(TYPES.IsVaultOwner),
|
||||||
|
this.get<IsVaultAdmin>(TYPES.IsVaultAdmin),
|
||||||
|
this.get<IsReadonlyVaultMember>(TYPES.IsReadonlyVaultMember),
|
||||||
this.get<GetVault>(TYPES.GetVault),
|
this.get<GetVault>(TYPES.GetVault),
|
||||||
this.get<LeaveVault>(TYPES.LeaveVault),
|
this.get<LeaveVault>(TYPES.LeaveVault),
|
||||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function RegisterApplicationServicesEvents(container: Dependencies, event
|
|||||||
events.addEventHandler(container.get(TYPES.VaultInviteService), ApplicationEvent.Launched)
|
events.addEventHandler(container.get(TYPES.VaultInviteService), ApplicationEvent.Launched)
|
||||||
events.addEventHandler(container.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites)
|
events.addEventHandler(container.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites)
|
||||||
events.addEventHandler(container.get(TYPES.VaultInviteService), WebSocketsServiceEvent.UserInvitedToSharedVault)
|
events.addEventHandler(container.get(TYPES.VaultInviteService), WebSocketsServiceEvent.UserInvitedToSharedVault)
|
||||||
|
events.addEventHandler(container.get(TYPES.VaultUserService), ApplicationEvent.CompletedFullSync)
|
||||||
|
|
||||||
if (container.get(TYPES.FilesBackupService)) {
|
if (container.get(TYPES.FilesBackupService)) {
|
||||||
events.addEventHandler(container.get(TYPES.FilesBackupService), ApplicationEvent.ApplicationStageChanged)
|
events.addEventHandler(container.get(TYPES.FilesBackupService), ApplicationEvent.ApplicationStageChanged)
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ export const TYPES = {
|
|||||||
EncryptTypeAPayloadWithKeyLookup: Symbol.for('EncryptTypeAPayloadWithKeyLookup'),
|
EncryptTypeAPayloadWithKeyLookup: Symbol.for('EncryptTypeAPayloadWithKeyLookup'),
|
||||||
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
|
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
|
||||||
IsVaultOwner: Symbol.for('IsVaultOwner'),
|
IsVaultOwner: Symbol.for('IsVaultOwner'),
|
||||||
|
IsVaultAdmin: Symbol.for('IsVaultAdmin'),
|
||||||
|
IsReadonlyVaultMember: Symbol.for('IsReadonlyVaultMember'),
|
||||||
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
|
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
|
||||||
ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'),
|
ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'),
|
||||||
DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'),
|
DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface Props {
|
|||||||
componentViewer: ComponentViewerInterface
|
componentViewer: ComponentViewerInterface
|
||||||
requestReload?: (viewer: ComponentViewerInterface, force?: boolean) => void
|
requestReload?: (viewer: ComponentViewerInterface, force?: boolean) => void
|
||||||
onLoad?: () => void
|
onLoad?: () => void
|
||||||
|
readonly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +31,7 @@ const MaxLoadThreshold = 4000
|
|||||||
const VisibilityChangeKey = 'visibilitychange'
|
const VisibilityChangeKey = 'visibilitychange'
|
||||||
const MSToWaitAfterIframeLoadToAvoidFlicker = 35
|
const MSToWaitAfterIframeLoadToAvoidFlicker = 35
|
||||||
|
|
||||||
const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer, requestReload }) => {
|
const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer, requestReload, readonly = false }) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
|
|
||||||
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
||||||
@@ -50,7 +51,7 @@ const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer,
|
|||||||
const reloadValidityStatus = useCallback(() => {
|
const reloadValidityStatus = useCallback(() => {
|
||||||
setFeatureStatus(componentViewer.getFeatureStatus())
|
setFeatureStatus(componentViewer.getFeatureStatus())
|
||||||
if (!componentViewer.lockReadonly) {
|
if (!componentViewer.lockReadonly) {
|
||||||
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled)
|
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled || readonly)
|
||||||
}
|
}
|
||||||
setIsComponentValid(componentViewer.shouldRender())
|
setIsComponentValid(componentViewer.shouldRender())
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer,
|
|||||||
|
|
||||||
setError(componentViewer.getError())
|
setError(componentViewer.getError())
|
||||||
setDeprecationMessage(uiFeature.deprecationMessage)
|
setDeprecationMessage(uiFeature.deprecationMessage)
|
||||||
}, [componentViewer, uiFeature, featureStatus, isComponentValid, isLoading])
|
}, [componentViewer, isLoading, isComponentValid, uiFeature.deprecationMessage, featureStatus, readonly])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadValidityStatus()
|
reloadValidityStatus()
|
||||||
|
|||||||
@@ -330,9 +330,12 @@ const ContentTableView = ({ application, items }: Props) => {
|
|||||||
setContextMenuItem(file)
|
setContextMenuItem(file)
|
||||||
},
|
},
|
||||||
rowActions: (item) => {
|
rowActions: (item) => {
|
||||||
|
const vault = application.vaults.getItemVault(item)
|
||||||
|
const isReadonly = vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ItemLinksCell item={item} />
|
{!isReadonly && <ItemLinksCell item={item} />}
|
||||||
<ContextMenuCell items={[item]} />
|
<ContextMenuCell items={[item]} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,6 +69,18 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
closeMenu()
|
closeMenu()
|
||||||
}, [closeMenu, toggleAppPane])
|
}, [closeMenu, toggleAppPane])
|
||||||
|
|
||||||
|
const areSomeFilesInReadonlySharedVault = selectedFiles.some((file) => {
|
||||||
|
const vault = application.vaults.getItemVault(file)
|
||||||
|
return vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
|
||||||
|
})
|
||||||
|
const hasAdminPermissionForAllSharedFiles = selectedFiles.every((file) => {
|
||||||
|
const vault = application.vaults.getItemVault(file)
|
||||||
|
if (!vault?.isSharedVaultListing()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
|
||||||
|
})
|
||||||
|
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
return <div className="text-center">No files selected</div>
|
return <div className="text-center">No files selected</div>
|
||||||
}
|
}
|
||||||
@@ -91,19 +103,25 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{application.featuresController.isEntitledToVaults() && (
|
{application.featuresController.isEntitledToVaults() && (
|
||||||
<AddToVaultMenuOption iconClassName={iconClass} items={selectedFiles} />
|
<AddToVaultMenuOption
|
||||||
|
iconClassName={iconClass}
|
||||||
|
items={selectedFiles}
|
||||||
|
disabled={!hasAdminPermissionForAllSharedFiles}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<AddTagOption
|
<AddTagOption
|
||||||
navigationController={application.navigationController}
|
navigationController={application.navigationController}
|
||||||
linkingController={application.linkingController}
|
linkingController={application.linkingController}
|
||||||
selectedItems={selectedFiles}
|
selectedItems={selectedFiles}
|
||||||
iconClassName={`text-neutral mr-2 ${MenuItemIconSize}`}
|
iconClassName={`text-neutral mr-2 ${MenuItemIconSize}`}
|
||||||
|
disabled={areSomeFilesInReadonlySharedVault}
|
||||||
/>
|
/>
|
||||||
<MenuSwitchButtonItem
|
<MenuSwitchButtonItem
|
||||||
checked={hasProtectedFiles}
|
checked={hasProtectedFiles}
|
||||||
onChange={(hasProtectedFiles) => {
|
onChange={(hasProtectedFiles) => {
|
||||||
void application.filesController.setProtectionForFiles(hasProtectedFiles, selectedFiles)
|
void application.filesController.setProtectionForFiles(hasProtectedFiles, selectedFiles)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeFilesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="lock" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
|
<Icon type="lock" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
|
||||||
Password protect
|
Password protect
|
||||||
@@ -123,6 +141,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
renameToggleCallback?.(true)
|
renameToggleCallback?.(true)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeFilesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="pencil" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
|
<Icon type="pencil" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
|
||||||
Rename
|
Rename
|
||||||
@@ -133,6 +152,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
closeMenuAndToggleFilesList()
|
closeMenuAndToggleFilesList()
|
||||||
void application.filesController.deleteFilesPermanently(selectedFiles)
|
void application.filesController.deleteFilesPermanently(selectedFiles)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeFilesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="trash" className={`mr-2 text-danger ${MenuItemIconSize}`} />
|
<Icon type="trash" className={`mr-2 text-danger ${MenuItemIconSize}`} />
|
||||||
<span className="text-danger">Delete permanently</span>
|
<span className="text-danger">Delete permanently</span>
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ const FilePreviewModal = observer(({ application }: Props) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vault = application.vaults.getItemVault(currentFile)
|
||||||
|
const isReadonly = vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={currentFile.name}
|
title={currentFile.name}
|
||||||
@@ -181,15 +184,17 @@ const FilePreviewModal = observer(({ application }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<StyledTooltip label="Rename file" className="!z-modal">
|
{!isReadonly && (
|
||||||
<button
|
<StyledTooltip label="Rename file" className="!z-modal">
|
||||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
<button
|
||||||
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
|
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||||
aria-label="Rename file"
|
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
|
||||||
>
|
aria-label="Rename file"
|
||||||
<Icon type="pencil-filled" className="text-neutral" />
|
>
|
||||||
</button>
|
<Icon type="pencil-filled" className="text-neutral" />
|
||||||
</StyledTooltip>
|
</button>
|
||||||
|
</StyledTooltip>
|
||||||
|
)}
|
||||||
<StyledTooltip label="Show linked items" className="!z-modal">
|
<StyledTooltip label="Show linked items" className="!z-modal">
|
||||||
<button
|
<button
|
||||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||||
@@ -249,7 +254,11 @@ const FilePreviewModal = observer(({ application }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
{showLinkedBubblesContainer && (
|
{showLinkedBubblesContainer && (
|
||||||
<div className="-mt-1 min-h-0 flex-shrink-0 border-b border-border px-3.5 py-1.5">
|
<div className="-mt-1 min-h-0 flex-shrink-0 border-b border-border px-3.5 py-1.5">
|
||||||
<LinkedItemBubblesContainer linkingController={application.linkingController} item={currentFile} />
|
<LinkedItemBubblesContainer
|
||||||
|
linkingController={application.linkingController}
|
||||||
|
item={currentFile}
|
||||||
|
readonly={isReadonly}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex min-h-0 flex-grow flex-col-reverse md:flex-row">
|
<div className="flex min-h-0 flex-grow flex-col-reverse md:flex-row">
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ const LinkedItemBubblesContainer = ({
|
|||||||
|
|
||||||
const { vault, lastEditedByContact } = useItemVaultInfo(item)
|
const { vault, lastEditedByContact } = useItemVaultInfo(item)
|
||||||
|
|
||||||
if (readonly && itemsToDisplay.length === 0) {
|
if (readonly && itemsToDisplay.length === 0 && !vault) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const MenuItem = forwardRef(
|
|||||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex w-full cursor-pointer select-none border-0 bg-transparent px-3 py-2 text-left md:py-1.5',
|
'flex w-full cursor-pointer select-none border-0 bg-transparent px-3 py-2 text-left md:py-1.5',
|
||||||
'text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground',
|
'text-mobile-menu-item text-text enabled:hover:bg-contrast enabled:hover:text-foreground',
|
||||||
'focus:bg-info-backdrop focus:shadow-none md:text-tablet-menu-item lg:text-menu-item',
|
'focus:bg-info-backdrop focus:shadow-none md:text-tablet-menu-item lg:text-menu-item',
|
||||||
'disabled:cursor-not-allowed disabled:opacity-60',
|
'disabled:cursor-not-allowed disabled:opacity-60',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ const MenuSwitchButtonItem = forwardRef(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5',
|
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5',
|
||||||
'text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none',
|
'text-left text-text focus:bg-info-backdrop focus:shadow-none enabled:hover:bg-contrast enabled:hover:text-foreground',
|
||||||
'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item',
|
'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item',
|
||||||
|
'disabled:cursor-not-allowed disabled:opacity-60',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
SNNote,
|
SNNote,
|
||||||
NoteType,
|
NoteType,
|
||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
|
VaultServiceInterface,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import NoteView from './NoteView'
|
import NoteView from './NoteView'
|
||||||
import { NoteViewController } from './Controller/NoteViewController'
|
import { NoteViewController } from './Controller/NoteViewController'
|
||||||
@@ -19,6 +20,7 @@ describe('NoteView', () => {
|
|||||||
let application: WebApplication
|
let application: WebApplication
|
||||||
|
|
||||||
let notesController: NotesController
|
let notesController: NotesController
|
||||||
|
let vaults: VaultServiceInterface
|
||||||
|
|
||||||
const createNoteView = () =>
|
const createNoteView = () =>
|
||||||
new NoteView({
|
new NoteView({
|
||||||
@@ -36,9 +38,13 @@ describe('NoteView', () => {
|
|||||||
notesController.getSpellcheckStateForNote = jest.fn()
|
notesController.getSpellcheckStateForNote = jest.fn()
|
||||||
notesController.getEditorWidthForNote = jest.fn()
|
notesController.getEditorWidthForNote = jest.fn()
|
||||||
|
|
||||||
|
vaults = {} as jest.Mocked<VaultServiceInterface>
|
||||||
|
vaults.getItemVault = jest.fn().mockReturnValue(undefined)
|
||||||
|
|
||||||
application = {
|
application = {
|
||||||
notesController,
|
notesController,
|
||||||
noteViewController,
|
noteViewController,
|
||||||
|
vaults,
|
||||||
} as unknown as jest.Mocked<WebApplication>
|
} as unknown as jest.Mocked<WebApplication>
|
||||||
|
|
||||||
application.hasProtectionSources = jest.fn().mockReturnValue(true)
|
application.hasProtectionSources = jest.fn().mockReturnValue(true)
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type State = {
|
|||||||
isDesktop?: boolean
|
isDesktop?: boolean
|
||||||
editorLineWidth: EditorLineWidth
|
editorLineWidth: EditorLineWidth
|
||||||
noteLocked: boolean
|
noteLocked: boolean
|
||||||
|
readonly: boolean
|
||||||
noteStatus?: NoteStatus
|
noteStatus?: NoteStatus
|
||||||
saveError?: boolean
|
saveError?: boolean
|
||||||
showProtectedWarning: boolean
|
showProtectedWarning: boolean
|
||||||
@@ -116,6 +117,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
||||||
|
|
||||||
|
const itemVault = this.application.vaults.getItemVault(this.controller.item)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
availableStackComponents: [],
|
availableStackComponents: [],
|
||||||
editorStateDidLoad: false,
|
editorStateDidLoad: false,
|
||||||
@@ -124,6 +127,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
isDesktop: isDesktopApplication(),
|
isDesktop: isDesktopApplication(),
|
||||||
noteStatus: undefined,
|
noteStatus: undefined,
|
||||||
noteLocked: this.controller.item.locked,
|
noteLocked: this.controller.item.locked,
|
||||||
|
readonly: itemVault ? this.application.vaultUsers.isCurrentUserReadonlyVaultMember(itemVault) : false,
|
||||||
showProtectedWarning: false,
|
showProtectedWarning: false,
|
||||||
spellcheck: true,
|
spellcheck: true,
|
||||||
stackComponentViewers: [],
|
stackComponentViewers: [],
|
||||||
@@ -855,6 +859,13 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{this.state.readonly && (
|
||||||
|
<div className="bg-warning-faded flex items-center px-3.5 py-2 text-sm text-accessory-tint-3">
|
||||||
|
<Icon type="pencil-off" className="mr-3" />
|
||||||
|
You don't have permission to edit this note
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{this.state.noteLocked && (
|
{this.state.noteLocked && (
|
||||||
<EditingDisabledBanner
|
<EditingDisabledBanner
|
||||||
onClick={() => this.application.notesController.setLockSelectedNotes(!this.state.noteLocked)}
|
onClick={() => this.application.notesController.setLockSelectedNotes(!this.state.noteLocked)}
|
||||||
@@ -878,7 +889,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
<div className="title flex-grow overflow-auto">
|
<div className="title flex-grow overflow-auto">
|
||||||
<input
|
<input
|
||||||
className="input text-lg"
|
className="input text-lg"
|
||||||
disabled={this.state.noteLocked}
|
disabled={this.state.noteLocked || this.state.readonly}
|
||||||
id={ElementIds.NoteTitleEditor}
|
id={ElementIds.NoteTitleEditor}
|
||||||
onChange={this.onTitleChange}
|
onChange={this.onTitleChange}
|
||||||
onFocus={(event) => {
|
onFocus={(event) => {
|
||||||
@@ -911,18 +922,22 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
)}
|
)}
|
||||||
{renderHeaderOptions && (
|
{renderHeaderOptions && (
|
||||||
<div className="note-view-options-buttons flex items-center gap-3">
|
<div className="note-view-options-buttons flex items-center gap-3">
|
||||||
<LinkedItemsButton
|
{!this.state.readonly && (
|
||||||
linkingController={this.application.linkingController}
|
<>
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
<LinkedItemsButton
|
||||||
/>
|
linkingController={this.application.linkingController}
|
||||||
<ChangeEditorButton
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
noteViewController={this.controller}
|
/>
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
<ChangeEditorButton
|
||||||
/>
|
noteViewController={this.controller}
|
||||||
<PinNoteButton
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
notesController={this.application.notesController}
|
/>
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
<PinNoteButton
|
||||||
/>
|
notesController={this.application.notesController}
|
||||||
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<NotesOptionsPanel
|
<NotesOptionsPanel
|
||||||
notesController={this.application.notesController}
|
notesController={this.application.notesController}
|
||||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||||
@@ -934,7 +949,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
<CollaborationInfoHUD item={this.note} />
|
<CollaborationInfoHUD item={this.note} />
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<LinkedItemBubblesContainer item={this.note} linkingController={this.application.linkingController} />
|
<LinkedItemBubblesContainer
|
||||||
|
item={this.note}
|
||||||
|
linkingController={this.application.linkingController}
|
||||||
|
readonly={this.state.readonly}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -961,6 +980,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
componentViewer={this.state.editorComponentViewer}
|
componentViewer={this.state.editorComponentViewer}
|
||||||
onLoad={this.onEditorComponentLoad}
|
onLoad={this.onEditorComponentLoad}
|
||||||
requestReload={this.editorComponentViewerRequestsReload}
|
requestReload={this.editorComponentViewerRequestsReload}
|
||||||
|
readonly={this.state.readonly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -971,7 +991,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
spellcheck={this.state.spellcheck}
|
spellcheck={this.state.spellcheck}
|
||||||
ref={this.setPlainEditorRef}
|
ref={this.setPlainEditorRef}
|
||||||
controller={this.controller}
|
controller={this.controller}
|
||||||
locked={this.state.noteLocked}
|
locked={this.state.noteLocked || this.state.readonly}
|
||||||
onFocus={this.onPlainFocus}
|
onFocus={this.onPlainFocus}
|
||||||
onBlur={this.onPlainBlur}
|
onBlur={this.onPlainBlur}
|
||||||
/>
|
/>
|
||||||
@@ -986,6 +1006,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
filesController={this.application.filesController}
|
filesController={this.application.filesController}
|
||||||
spellcheck={this.state.spellcheck}
|
spellcheck={this.state.spellcheck}
|
||||||
controller={this.controller}
|
controller={this.controller}
|
||||||
|
readonly={this.state.readonly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Props = {
|
|||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
selectedItems: DecryptedItemInterface[]
|
selectedItems: DecryptedItemInterface[]
|
||||||
iconClassName: string
|
iconClassName: string
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddTagOption: FunctionComponent<Props> = ({
|
const AddTagOption: FunctionComponent<Props> = ({
|
||||||
@@ -23,6 +24,7 @@ const AddTagOption: FunctionComponent<Props> = ({
|
|||||||
linkingController,
|
linkingController,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
iconClassName,
|
iconClassName,
|
||||||
|
disabled,
|
||||||
}) => {
|
}) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -57,6 +59,7 @@ const AddTagOption: FunctionComponent<Props> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Icon type="hashtag" className={iconClassName} />
|
<Icon type="hashtag" className={iconClassName} />
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ type ChangeEditorOptionProps = {
|
|||||||
application: WebApplication
|
application: WebApplication
|
||||||
note: SNNote
|
note: SNNote
|
||||||
iconClassName: string
|
iconClassName: string
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note, iconClassName }) => {
|
const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
|
||||||
|
application,
|
||||||
|
note,
|
||||||
|
iconClassName,
|
||||||
|
disabled,
|
||||||
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
@@ -38,6 +44,7 @@ const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ applic
|
|||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
|
||||||
import MenuItem from '../Menu/MenuItem'
|
|
||||||
|
|
||||||
type DeletePermanentlyButtonProps = {
|
|
||||||
onClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => (
|
|
||||||
<MenuItem onClick={onClick}>
|
|
||||||
<Icon type="close" className="mr-2 text-danger" />
|
|
||||||
<span className="text-danger">Delete permanently</span>
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
@@ -30,7 +30,6 @@ import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/Keyboard
|
|||||||
import { NoteAttributes } from './NoteAttributes'
|
import { NoteAttributes } from './NoteAttributes'
|
||||||
import { SpellcheckOptions } from './SpellcheckOptions'
|
import { SpellcheckOptions } from './SpellcheckOptions'
|
||||||
import { NoteSizeWarning } from './NoteSizeWarning'
|
import { NoteSizeWarning } from './NoteSizeWarning'
|
||||||
import { DeletePermanentlyButton } from './DeletePermanentlyButton'
|
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useCommandService } from '../CommandProvider'
|
||||||
import { iconClass } from './ClassNames'
|
import { iconClass } from './ClassNames'
|
||||||
import SuperNoteOptions from './SuperNoteOptions'
|
import SuperNoteOptions from './SuperNoteOptions'
|
||||||
@@ -209,6 +208,17 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const areSomeNotesInSharedVault = notes.some((note) => application.vaults.getItemVault(note)?.isSharedVaultListing())
|
const areSomeNotesInSharedVault = notes.some((note) => application.vaults.getItemVault(note)?.isSharedVaultListing())
|
||||||
|
const areSomeNotesInReadonlySharedVault = notes.some((note) => {
|
||||||
|
const vault = application.vaults.getItemVault(note)
|
||||||
|
return vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
|
||||||
|
})
|
||||||
|
const hasAdminPermissionForAllSharedNotes = notes.every((note) => {
|
||||||
|
const vault = application.vaults.getItemVault(note)
|
||||||
|
if (!vault?.isSharedVaultListing()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
|
||||||
|
})
|
||||||
|
|
||||||
if (notes.length === 0) {
|
if (notes.length === 0) {
|
||||||
return null
|
return null
|
||||||
@@ -220,13 +230,13 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<>
|
<>
|
||||||
{notes.length === 1 && (
|
{notes.length === 1 && (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={openRevisionHistoryModal}>
|
<MenuItem onClick={openRevisionHistoryModal} disabled={areSomeNotesInReadonlySharedVault}>
|
||||||
<Icon type="history" className={iconClass} />
|
<Icon type="history" className={iconClass} />
|
||||||
Note history
|
Note history
|
||||||
{historyShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={historyShortcut} />}
|
{historyShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={historyShortcut} />}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
<MenuItem onClick={toggleLineWidthModal}>
|
<MenuItem onClick={toggleLineWidthModal} disabled={areSomeNotesInReadonlySharedVault}>
|
||||||
<Icon type="line-width" className={iconClass} />
|
<Icon type="line-width" className={iconClass} />
|
||||||
Editor width
|
Editor width
|
||||||
{editorWidthShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={editorWidthShortcut} />}
|
{editorWidthShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={editorWidthShortcut} />}
|
||||||
@@ -238,6 +248,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
onChange={(locked) => {
|
onChange={(locked) => {
|
||||||
application.notesController.setLockSelectedNotes(locked)
|
application.notesController.setLockSelectedNotes(locked)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="pencil-off" className={iconClass} />
|
<Icon type="pencil-off" className={iconClass} />
|
||||||
Prevent editing
|
Prevent editing
|
||||||
@@ -247,6 +258,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
onChange={(hidePreviews) => {
|
onChange={(hidePreviews) => {
|
||||||
application.notesController.setHideSelectedNotePreviews(!hidePreviews)
|
application.notesController.setHideSelectedNotePreviews(!hidePreviews)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="rich-text" className={iconClass} />
|
<Icon type="rich-text" className={iconClass} />
|
||||||
Show preview
|
Show preview
|
||||||
@@ -256,6 +268,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
onChange={(protect) => {
|
onChange={(protect) => {
|
||||||
application.notesController.setProtectSelectedNotes(protect).catch(console.error)
|
application.notesController.setProtectSelectedNotes(protect).catch(console.error)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="lock" className={iconClass} />
|
<Icon type="lock" className={iconClass} />
|
||||||
Password protect
|
Password protect
|
||||||
@@ -263,13 +276,18 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
{notes.length === 1 && (
|
{notes.length === 1 && (
|
||||||
<>
|
<>
|
||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
<ChangeEditorOption iconClassName={iconClass} application={application} note={notes[0]} />
|
<ChangeEditorOption
|
||||||
|
iconClassName={iconClass}
|
||||||
|
application={application}
|
||||||
|
note={notes[0]}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
|
|
||||||
{application.featuresController.isEntitledToVaults() && (
|
{application.featuresController.isEntitledToVaults() && (
|
||||||
<AddToVaultMenuOption iconClassName={iconClass} items={notes} />
|
<AddToVaultMenuOption iconClassName={iconClass} items={notes} disabled={!hasAdminPermissionForAllSharedNotes} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{application.navigationController.tagsCount > 0 && (
|
{application.navigationController.tagsCount > 0 && (
|
||||||
@@ -278,12 +296,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
navigationController={application.navigationController}
|
navigationController={application.navigationController}
|
||||||
selectedItems={notes}
|
selectedItems={notes}
|
||||||
linkingController={application.linkingController}
|
linkingController={application.linkingController}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
application.notesController.setStarSelectedNotes(!starred)
|
application.notesController.setStarSelectedNotes(!starred)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="star" className={iconClass} />
|
<Icon type="star" className={iconClass} />
|
||||||
{starred ? 'Unstar' : 'Star'}
|
{starred ? 'Unstar' : 'Star'}
|
||||||
@@ -295,6 +315,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
application.notesController.setPinSelectedNotes(true)
|
application.notesController.setPinSelectedNotes(true)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="pin" className={iconClass} />
|
<Icon type="pin" className={iconClass} />
|
||||||
Pin to top
|
Pin to top
|
||||||
@@ -306,6 +327,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
application.notesController.setPinSelectedNotes(false)
|
application.notesController.setPinSelectedNotes(false)
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="unpin" className={iconClass} />
|
<Icon type="unpin" className={iconClass} />
|
||||||
Unpin
|
Unpin
|
||||||
@@ -382,7 +404,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={duplicateSelectedItems}>
|
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
|
||||||
<Icon type="copy" className={iconClass} />
|
<Icon type="copy" className={iconClass} />
|
||||||
Duplicate
|
Duplicate
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -392,6 +414,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
await application.notesController.setArchiveSelectedNotes(true).catch(console.error)
|
await application.notesController.setArchiveSelectedNotes(true).catch(console.error)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="archive" className={iconClassWarning} />
|
<Icon type="archive" className={iconClassWarning} />
|
||||||
<span className="text-warning">Archive</span>
|
<span className="text-warning">Archive</span>
|
||||||
@@ -403,6 +426,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
await application.notesController.setArchiveSelectedNotes(false).catch(console.error)
|
await application.notesController.setArchiveSelectedNotes(false).catch(console.error)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="unarchive" className={iconClassWarning} />
|
<Icon type="unarchive" className={iconClassWarning} />
|
||||||
<span className="text-warning">Unarchive</span>
|
<span className="text-warning">Unarchive</span>
|
||||||
@@ -410,18 +434,23 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
)}
|
)}
|
||||||
{notTrashed &&
|
{notTrashed &&
|
||||||
(altKeyDown ? (
|
(altKeyDown ? (
|
||||||
<DeletePermanentlyButton
|
<MenuItem
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.deleteNotesPermanently()
|
await application.notesController.deleteNotesPermanently()
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Icon type="close" className="mr-2 text-danger" />
|
||||||
|
<span className="text-danger">Delete permanently</span>
|
||||||
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.setTrashSelectedNotes(true)
|
await application.notesController.setTrashSelectedNotes(true)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="trash" className={iconClassDanger} />
|
<Icon type="trash" className={iconClassDanger} />
|
||||||
<span className="text-danger">Move to trash</span>
|
<span className="text-danger">Move to trash</span>
|
||||||
@@ -434,21 +463,27 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
await application.notesController.setTrashSelectedNotes(false)
|
await application.notesController.setTrashSelectedNotes(false)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<Icon type="restore" className={iconClassSuccess} />
|
<Icon type="restore" className={iconClassSuccess} />
|
||||||
<span className="text-success">Restore</span>
|
<span className="text-success">Restore</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<DeletePermanentlyButton
|
<MenuItem
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.deleteNotesPermanently()
|
await application.notesController.deleteNotesPermanently()
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Icon type="close" className="mr-2 text-danger" />
|
||||||
|
<span className="text-danger">Delete permanently</span>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.emptyTrash()
|
await application.notesController.emptyTrash()
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<Icon type="trash-sweep" className="mr-2 text-danger" />
|
<Icon type="trash-sweep" className="mr-2 text-danger" />
|
||||||
@@ -485,16 +520,19 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
editorForNote={editorForNote}
|
editorForNote={editorForNote}
|
||||||
notesController={application.notesController}
|
notesController={application.notesController}
|
||||||
note={notes[0]}
|
note={notes[0]}
|
||||||
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
|
|
||||||
<NoteAttributes application={application} note={notes[0]} />
|
<NoteAttributes className="mb-2" application={application} note={notes[0]} />
|
||||||
|
|
||||||
<NoteSizeWarning note={notes[0]} />
|
<NoteSizeWarning note={notes[0]} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : (
|
||||||
|
<div className="h-2" />
|
||||||
|
)}
|
||||||
|
|
||||||
<ModalOverlay isOpen={showExportSuperModal} close={closeSuperExportModal}>
|
<ModalOverlay isOpen={showExportSuperModal} close={closeSuperExportModal}>
|
||||||
<SuperExportModal exportNotes={downloadSelectedItems} close={closeSuperExportModal} />
|
<SuperExportModal exportNotes={downloadSelectedItems} close={closeSuperExportModal} />
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export const SpellcheckOptions: FunctionComponent<{
|
|||||||
editorForNote: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
editorForNote: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
||||||
notesController: NotesController
|
notesController: NotesController
|
||||||
note: SNNote
|
note: SNNote
|
||||||
}> = ({ editorForNote, notesController, note }) => {
|
disabled?: boolean
|
||||||
|
}> = ({ editorForNote, notesController, note, disabled }) => {
|
||||||
const spellcheckControllable = editorForNote.featureDescription.spellcheckControl
|
const spellcheckControllable = editorForNote.featureDescription.spellcheckControl
|
||||||
const noteSpellcheck = !spellcheckControllable
|
const noteSpellcheck = !spellcheckControllable
|
||||||
? true
|
? true
|
||||||
@@ -24,7 +25,7 @@ export const SpellcheckOptions: FunctionComponent<{
|
|||||||
onChange={() => {
|
onChange={() => {
|
||||||
notesController.toggleGlobalSpellcheckForNote(note).catch(console.error)
|
notesController.toggleGlobalSpellcheckForNote(note).catch(console.error)
|
||||||
}}
|
}}
|
||||||
disabled={!spellcheckControllable}
|
disabled={disabled || !spellcheckControllable}
|
||||||
>
|
>
|
||||||
<Icon type="notes" className={iconClass} />
|
<Icon type="notes" className={iconClass} />
|
||||||
Spellcheck
|
Spellcheck
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog })
|
|||||||
wrapper: 'col-start-2',
|
wrapper: 'col-start-2',
|
||||||
}}
|
}}
|
||||||
items={Object.keys(SharedVaultUserPermission.PERMISSIONS).map((key) => ({
|
items={Object.keys(SharedVaultUserPermission.PERMISSIONS).map((key) => ({
|
||||||
label: key === 'Write' ? 'Read / Write' : key,
|
label: application.vaultUsers.getFormattedMemberPermission(key),
|
||||||
value: key,
|
value: key,
|
||||||
}))}
|
}))}
|
||||||
value={selectedContact.permission}
|
value={selectedContact.permission}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { useApplication } from '@/Components/ApplicationProvider'
|
|||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||||
import { InviteRecord, SharedVaultUserPermission } from '@standardnotes/snjs'
|
import { InviteRecord } from '@standardnotes/snjs'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import EditContactModal from '../Contacts/EditContactModal'
|
import EditContactModal from '../Contacts/EditContactModal'
|
||||||
import { CheckmarkCircle } from '../../../../UIElements/CheckmarkCircle'
|
import { CheckmarkCircle } from '../../../../UIElements/CheckmarkCircle'
|
||||||
import { capitalizeString } from '@/Utils'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
inviteRecord: InviteRecord
|
inviteRecord: InviteRecord
|
||||||
@@ -36,10 +35,7 @@ const InviteItem = ({ inviteRecord }: Props) => {
|
|||||||
|
|
||||||
const trustedContact = application.contacts.findSenderContactForInvite(inviteRecord.invite)
|
const trustedContact = application.contacts.findSenderContactForInvite(inviteRecord.invite)
|
||||||
|
|
||||||
const permission =
|
const permission = application.vaultUsers.getFormattedMemberPermission(inviteRecord.invite.permission)
|
||||||
inviteRecord.invite.permission === SharedVaultUserPermission.PERMISSIONS.Write
|
|
||||||
? 'Read / Write'
|
|
||||||
: capitalizeString(inviteRecord.invite.permission)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ const VaultItem = ({ vault }: Props) => {
|
|||||||
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
|
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
|
||||||
const closeVaultModal = () => setIsVaultModalOpen(false)
|
const closeVaultModal = () => setIsVaultModalOpen(false)
|
||||||
|
|
||||||
const { isCurrentUserAdmin, isLocked, canShowLockOption, toggleLock, ensureVaultIsUnlocked } = useVault(vault)
|
const { isCurrentUserAdmin, isCurrentUserOwner, isLocked, canShowLockOption, toggleLock, ensureVaultIsUnlocked } =
|
||||||
|
useVault(vault)
|
||||||
|
|
||||||
const deleteVault = useCallback(async () => {
|
const deleteVault = useCallback(async () => {
|
||||||
const confirm = await application.alerts.confirm(
|
const confirm = await application.alerts.confirm(
|
||||||
@@ -122,8 +123,8 @@ 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} />
|
||||||
{canShowLockOption && <Button label={isLocked ? 'Unlock' : 'Lock'} onClick={toggleLock} />}
|
{canShowLockOption && <Button label={isLocked ? 'Unlock' : 'Lock'} onClick={toggleLock} />}
|
||||||
{isCurrentUserAdmin && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
|
{isCurrentUserOwner && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
|
||||||
{!isCurrentUserAdmin && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
|
{!isCurrentUserOwner && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
|
||||||
{isCurrentUserAdmin ? (
|
{isCurrentUserAdmin ? (
|
||||||
vault.isSharedVaultListing() ? (
|
vault.isSharedVaultListing() ? (
|
||||||
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />
|
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ const EditVaultModalContent: FunctionComponent<{
|
|||||||
|
|
||||||
if (existingVault.isSharedVaultListing()) {
|
if (existingVault.isSharedVaultListing()) {
|
||||||
setIsAdmin(
|
setIsAdmin(
|
||||||
existingVault.isSharedVaultListing() && application.vaultUsers.isCurrentUserSharedVaultOwner(existingVault),
|
existingVault.isSharedVaultListing() && application.vaultUsers.isCurrentUserSharedVaultAdmin(existingVault),
|
||||||
)
|
)
|
||||||
|
|
||||||
setIsLoadingCollaborationInfo(true)
|
setIsLoadingCollaborationInfo(true)
|
||||||
const users = await application.vaultUsers.getSharedVaultUsers(existingVault)
|
const users = await application.vaultUsers.getSharedVaultUsersFromServer(existingVault)
|
||||||
if (users) {
|
if (users) {
|
||||||
setMembers(users)
|
setMembers(users)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import { SharedVaultInviteServerHash, SharedVaultUserPermission } from '@standardnotes/snjs'
|
import { SharedVaultInviteServerHash } 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 { capitalizeString } from '@/Utils'
|
|
||||||
|
|
||||||
export const VaultModalInvites = ({
|
export const VaultModalInvites = ({
|
||||||
invites,
|
invites,
|
||||||
@@ -30,10 +29,7 @@ export const VaultModalInvites = ({
|
|||||||
<div className="space-y-3.5">
|
<div className="space-y-3.5">
|
||||||
{invites.map((invite) => {
|
{invites.map((invite) => {
|
||||||
const contact = application.contacts.findContactForInvite(invite)
|
const contact = application.contacts.findContactForInvite(invite)
|
||||||
const permission =
|
const permission = application.vaultUsers.getFormattedMemberPermission(invite.permission)
|
||||||
invite.permission === SharedVaultUserPermission.PERMISSIONS.Write
|
|
||||||
? 'Read / Write'
|
|
||||||
: capitalizeString(invite.permission)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import { SharedVaultUserPermission, 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 { capitalizeString } from '@/Utils'
|
|
||||||
|
|
||||||
export const VaultModalMembers = ({
|
export const VaultModalMembers = ({
|
||||||
members,
|
members,
|
||||||
@@ -35,10 +34,7 @@ export const VaultModalMembers = ({
|
|||||||
{members.map((member) => {
|
{members.map((member) => {
|
||||||
const isMemberVaultOwner = application.vaultUsers.isVaultUserOwner(member)
|
const isMemberVaultOwner = application.vaultUsers.isVaultUserOwner(member)
|
||||||
const contact = application.contacts.findContactForServerUser(member)
|
const contact = application.contacts.findContactForServerUser(member)
|
||||||
const permission =
|
const permission = application.vaultUsers.getFormattedMemberPermission(member.permission)
|
||||||
member.permission === SharedVaultUserPermission.PERMISSIONS.Write
|
|
||||||
? 'Read / Write'
|
|
||||||
: capitalizeString(member.permission)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ const CodeOptionsPlugin = () => {
|
|||||||
const [selectedElementKey, setSelectedElementKey] = useState<NodeKey | null>(null)
|
const [selectedElementKey, setSelectedElementKey] = useState<NodeKey | null>(null)
|
||||||
|
|
||||||
const updateToolbar = useCallback(() => {
|
const updateToolbar = useCallback(() => {
|
||||||
|
if (!editor.isEditable()) {
|
||||||
|
setIsCode(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const selection = $getSelection()
|
const selection = $getSelection()
|
||||||
if (!$isRangeSelection(selection)) {
|
if (!$isRangeSelection(selection)) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ type Props = {
|
|||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
filesController: FilesController
|
filesController: FilesController
|
||||||
spellcheck: boolean
|
spellcheck: boolean
|
||||||
|
readonly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SuperEditor: FunctionComponent<Props> = ({
|
export const SuperEditor: FunctionComponent<Props> = ({
|
||||||
@@ -66,6 +67,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
filesController,
|
filesController,
|
||||||
spellcheck,
|
spellcheck,
|
||||||
controller,
|
controller,
|
||||||
|
readonly,
|
||||||
}) => {
|
}) => {
|
||||||
const note = useRef(controller.item)
|
const note = useRef(controller.item)
|
||||||
const changeEditorFunction = useRef<ChangeEditorFunction>()
|
const changeEditorFunction = useRef<ChangeEditorFunction>()
|
||||||
@@ -217,7 +219,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<LinkingControllerProvider controller={linkingController}>
|
<LinkingControllerProvider controller={linkingController}>
|
||||||
<FilesControllerProvider controller={filesController}>
|
<FilesControllerProvider controller={filesController}>
|
||||||
<BlocksEditorComposer readonly={note.current.locked} initialValue={note.current.text}>
|
<BlocksEditorComposer readonly={note.current.locked || readonly} initialValue={note.current.text}>
|
||||||
<BlocksEditor
|
<BlocksEditor
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -227,6 +229,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
)}
|
)}
|
||||||
previewLength={SuperNotePreviewCharLimit}
|
previewLength={SuperNotePreviewCharLimit}
|
||||||
spellcheck={spellcheck}
|
spellcheck={spellcheck}
|
||||||
|
readonly={note.current.locked || readonly}
|
||||||
>
|
>
|
||||||
<ItemSelectionPlugin currentNote={note.current} />
|
<ItemSelectionPlugin currentNote={note.current} />
|
||||||
<FilePlugin currentNote={note.current} />
|
<FilePlugin currentNote={note.current} />
|
||||||
@@ -242,7 +245,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
|
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
|
||||||
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
|
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
|
||||||
<ExportPlugin />
|
<ExportPlugin />
|
||||||
<ReadonlyPlugin note={note.current} />
|
{readonly === undefined && <ReadonlyPlugin note={note.current} />}
|
||||||
<AutoFocusPlugin isEnabled={controller.isTemplateNote} />
|
<AutoFocusPlugin isEnabled={controller.isTemplateNote} />
|
||||||
<SuperSearchContextProvider>
|
<SuperSearchContextProvider>
|
||||||
<SearchPlugin />
|
<SearchPlugin />
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const VaultMenu = observer(({ items }: { items: DecryptedItemInterface[] }) => {
|
|||||||
doesVaultContainItems(vault) ? void removeItemsFromVault() : void addItemsToVault(vault)
|
doesVaultContainItems(vault) ? void removeItemsFromVault() : void addItemsToVault(vault)
|
||||||
}}
|
}}
|
||||||
className={doesVaultContainItems(vault) ? 'font-bold' : ''}
|
className={doesVaultContainItems(vault) ? 'font-bold' : ''}
|
||||||
|
disabled={vault.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
type={vault.iconString}
|
type={vault.iconString}
|
||||||
@@ -98,7 +99,15 @@ const VaultMenu = observer(({ items }: { items: DecryptedItemInterface[] }) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const AddToVaultMenuOption = ({ iconClassName, items }: { iconClassName: string; items: DecryptedItemInterface[] }) => {
|
const AddToVaultMenuOption = ({
|
||||||
|
iconClassName,
|
||||||
|
items,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
iconClassName: string
|
||||||
|
items: DecryptedItemInterface[]
|
||||||
|
disabled?: boolean
|
||||||
|
}) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
@@ -123,6 +132,7 @@ const AddToVaultMenuOption = ({ iconClassName, items }: { iconClassName: string;
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Icon type="safe-square" className={iconClassName} />
|
<Icon type="safe-square" className={iconClassName} />
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export const useVault = (vault: VaultListingInterface) => {
|
|||||||
}, [application.vaultDisplayService, application.vaultLocks, canShowLockOption, isLocked, vault])
|
}, [application.vaultDisplayService, application.vaultLocks, canShowLockOption, isLocked, vault])
|
||||||
|
|
||||||
const isCurrentUserAdmin = !vault.isSharedVaultListing()
|
const isCurrentUserAdmin = !vault.isSharedVaultListing()
|
||||||
|
? true
|
||||||
|
: application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
|
||||||
|
const isCurrentUserOwner = !vault.isSharedVaultListing()
|
||||||
? true
|
? true
|
||||||
: application.vaultUsers.isCurrentUserSharedVaultOwner(vault)
|
: application.vaultUsers.isCurrentUserSharedVaultOwner(vault)
|
||||||
|
|
||||||
@@ -54,5 +57,6 @@ export const useVault = (vault: VaultListingInterface) => {
|
|||||||
toggleLock,
|
toggleLock,
|
||||||
ensureVaultIsUnlocked,
|
ensureVaultIsUnlocked,
|
||||||
isCurrentUserAdmin,
|
isCurrentUserAdmin,
|
||||||
|
isCurrentUserOwner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user