chore: vault member permissions (#2509)

This commit is contained in:
Aman Harwara
2023-09-18 19:53:34 +05:30
committed by GitHub
parent 2af610c7bf
commit 48e7820100
32 changed files with 331 additions and 94 deletions

View File

@@ -20,6 +20,7 @@ interface Props {
componentViewer: ComponentViewerInterface
requestReload?: (viewer: ComponentViewerInterface, force?: boolean) => void
onLoad?: () => void
readonly?: boolean
}
/**
@@ -30,7 +31,7 @@ const MaxLoadThreshold = 4000
const VisibilityChangeKey = 'visibilitychange'
const MSToWaitAfterIframeLoadToAvoidFlicker = 35
const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer, requestReload }) => {
const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer, requestReload, readonly = false }) => {
const application = useApplication()
const iframeRef = useRef<HTMLIFrameElement | null>(null)
@@ -50,7 +51,7 @@ const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer,
const reloadValidityStatus = useCallback(() => {
setFeatureStatus(componentViewer.getFeatureStatus())
if (!componentViewer.lockReadonly) {
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled)
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled || readonly)
}
setIsComponentValid(componentViewer.shouldRender())
@@ -60,7 +61,7 @@ const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer,
setError(componentViewer.getError())
setDeprecationMessage(uiFeature.deprecationMessage)
}, [componentViewer, uiFeature, featureStatus, isComponentValid, isLoading])
}, [componentViewer, isLoading, isComponentValid, uiFeature.deprecationMessage, featureStatus, readonly])
useEffect(() => {
reloadValidityStatus()

View File

@@ -330,9 +330,12 @@ const ContentTableView = ({ application, items }: Props) => {
setContextMenuItem(file)
},
rowActions: (item) => {
const vault = application.vaults.getItemVault(item)
const isReadonly = vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
return (
<div className="flex items-center gap-2">
<ItemLinksCell item={item} />
{!isReadonly && <ItemLinksCell item={item} />}
<ContextMenuCell items={[item]} />
</div>
)

View File

@@ -69,6 +69,18 @@ const FileMenuOptions: FunctionComponent<Props> = ({
closeMenu()
}, [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) {
return <div className="text-center">No files selected</div>
}
@@ -91,19 +103,25 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</>
)}
{application.featuresController.isEntitledToVaults() && (
<AddToVaultMenuOption iconClassName={iconClass} items={selectedFiles} />
<AddToVaultMenuOption
iconClassName={iconClass}
items={selectedFiles}
disabled={!hasAdminPermissionForAllSharedFiles}
/>
)}
<AddTagOption
navigationController={application.navigationController}
linkingController={application.linkingController}
selectedItems={selectedFiles}
iconClassName={`text-neutral mr-2 ${MenuItemIconSize}`}
disabled={areSomeFilesInReadonlySharedVault}
/>
<MenuSwitchButtonItem
checked={hasProtectedFiles}
onChange={(hasProtectedFiles) => {
void application.filesController.setProtectionForFiles(hasProtectedFiles, selectedFiles)
}}
disabled={areSomeFilesInReadonlySharedVault}
>
<Icon type="lock" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
Password protect
@@ -123,6 +141,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
onClick={() => {
renameToggleCallback?.(true)
}}
disabled={areSomeFilesInReadonlySharedVault}
>
<Icon type="pencil" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
Rename
@@ -133,6 +152,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
closeMenuAndToggleFilesList()
void application.filesController.deleteFilesPermanently(selectedFiles)
}}
disabled={areSomeFilesInReadonlySharedVault}
>
<Icon type="trash" className={`mr-2 text-danger ${MenuItemIconSize}`} />
<span className="text-danger">Delete permanently</span>

View File

@@ -113,6 +113,9 @@ const FilePreviewModal = observer(({ application }: Props) => {
return null
}
const vault = application.vaults.getItemVault(currentFile)
const isReadonly = vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
return (
<Modal
title={currentFile.name}
@@ -181,15 +184,17 @@ const FilePreviewModal = observer(({ application }: Props) => {
)}
</div>
<div className="flex items-center">
<StyledTooltip label="Rename file" className="!z-modal">
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
aria-label="Rename file"
>
<Icon type="pencil-filled" className="text-neutral" />
</button>
</StyledTooltip>
{!isReadonly && (
<StyledTooltip label="Rename file" className="!z-modal">
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
aria-label="Rename file"
>
<Icon type="pencil-filled" className="text-neutral" />
</button>
</StyledTooltip>
)}
<StyledTooltip label="Show linked items" className="!z-modal">
<button
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>
{showLinkedBubblesContainer && (
<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 className="flex min-h-0 flex-grow flex-col-reverse md:flex-row">

View File

@@ -166,7 +166,7 @@ const LinkedItemBubblesContainer = ({
const { vault, lastEditedByContact } = useItemVaultInfo(item)
if (readonly && itemsToDisplay.length === 0) {
if (readonly && itemsToDisplay.length === 0 && !vault) {
return null
}

View File

@@ -41,7 +41,7 @@ const MenuItem = forwardRef(
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
className={classNames(
'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',
'disabled:cursor-not-allowed disabled:opacity-60',
className,

View File

@@ -37,8 +37,9 @@ const MenuSwitchButtonItem = forwardRef(
ref={ref}
className={classNames(
'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',
'disabled:cursor-not-allowed disabled:opacity-60',
className,
)}
onClick={() => {

View File

@@ -10,6 +10,7 @@ import {
SNNote,
NoteType,
PayloadEmitSource,
VaultServiceInterface,
} from '@standardnotes/snjs'
import NoteView from './NoteView'
import { NoteViewController } from './Controller/NoteViewController'
@@ -19,6 +20,7 @@ describe('NoteView', () => {
let application: WebApplication
let notesController: NotesController
let vaults: VaultServiceInterface
const createNoteView = () =>
new NoteView({
@@ -36,9 +38,13 @@ describe('NoteView', () => {
notesController.getSpellcheckStateForNote = jest.fn()
notesController.getEditorWidthForNote = jest.fn()
vaults = {} as jest.Mocked<VaultServiceInterface>
vaults.getItemVault = jest.fn().mockReturnValue(undefined)
application = {
notesController,
noteViewController,
vaults,
} as unknown as jest.Mocked<WebApplication>
application.hasProtectionSources = jest.fn().mockReturnValue(true)

View File

@@ -68,6 +68,7 @@ type State = {
isDesktop?: boolean
editorLineWidth: EditorLineWidth
noteLocked: boolean
readonly: boolean
noteStatus?: NoteStatus
saveError?: boolean
showProtectedWarning: boolean
@@ -116,6 +117,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
const itemVault = this.application.vaults.getItemVault(this.controller.item)
this.state = {
availableStackComponents: [],
editorStateDidLoad: false,
@@ -124,6 +127,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
isDesktop: isDesktopApplication(),
noteStatus: undefined,
noteLocked: this.controller.item.locked,
readonly: itemVault ? this.application.vaultUsers.isCurrentUserReadonlyVaultMember(itemVault) : false,
showProtectedWarning: false,
spellcheck: true,
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 && (
<EditingDisabledBanner
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">
<input
className="input text-lg"
disabled={this.state.noteLocked}
disabled={this.state.noteLocked || this.state.readonly}
id={ElementIds.NoteTitleEditor}
onChange={this.onTitleChange}
onFocus={(event) => {
@@ -911,18 +922,22 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
)}
{renderHeaderOptions && (
<div className="note-view-options-buttons flex items-center gap-3">
<LinkedItemsButton
linkingController={this.application.linkingController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<ChangeEditorButton
noteViewController={this.controller}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<PinNoteButton
notesController={this.application.notesController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
{!this.state.readonly && (
<>
<LinkedItemsButton
linkingController={this.application.linkingController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<ChangeEditorButton
noteViewController={this.controller}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<PinNoteButton
notesController={this.application.notesController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
</>
)}
<NotesOptionsPanel
notesController={this.application.notesController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
@@ -934,7 +949,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
<CollaborationInfoHUD item={this.note} />
</div>
<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>
)}
@@ -961,6 +980,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
componentViewer={this.state.editorComponentViewer}
onLoad={this.onEditorComponentLoad}
requestReload={this.editorComponentViewerRequestsReload}
readonly={this.state.readonly}
/>
</div>
)}
@@ -971,7 +991,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
spellcheck={this.state.spellcheck}
ref={this.setPlainEditorRef}
controller={this.controller}
locked={this.state.noteLocked}
locked={this.state.noteLocked || this.state.readonly}
onFocus={this.onPlainFocus}
onBlur={this.onPlainBlur}
/>
@@ -986,6 +1006,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
filesController={this.application.filesController}
spellcheck={this.state.spellcheck}
controller={this.controller}
readonly={this.state.readonly}
/>
</div>
)}

View File

@@ -16,6 +16,7 @@ type Props = {
linkingController: LinkingController
selectedItems: DecryptedItemInterface[]
iconClassName: string
disabled?: boolean
}
const AddTagOption: FunctionComponent<Props> = ({
@@ -23,6 +24,7 @@ const AddTagOption: FunctionComponent<Props> = ({
linkingController,
selectedItems,
iconClassName,
disabled,
}) => {
const application = useApplication()
const menuContainerRef = useRef<HTMLDivElement>(null)
@@ -57,6 +59,7 @@ const AddTagOption: FunctionComponent<Props> = ({
}
}}
ref={buttonRef}
disabled={disabled}
>
<div className="flex items-center">
<Icon type="hashtag" className={iconClassName} />

View File

@@ -12,9 +12,15 @@ type ChangeEditorOptionProps = {
application: WebApplication
note: SNNote
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 menuContainerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
@@ -38,6 +44,7 @@ const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ applic
setIsOpen(false)
}
}}
disabled={disabled}
ref={buttonRef}
>
<div className="flex items-center">

View File

@@ -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>
)

View File

@@ -30,7 +30,6 @@ import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/Keyboard
import { NoteAttributes } from './NoteAttributes'
import { SpellcheckOptions } from './SpellcheckOptions'
import { NoteSizeWarning } from './NoteSizeWarning'
import { DeletePermanentlyButton } from './DeletePermanentlyButton'
import { useCommandService } from '../CommandProvider'
import { iconClass } from './ClassNames'
import SuperNoteOptions from './SuperNoteOptions'
@@ -209,6 +208,17 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
}
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) {
return null
@@ -220,13 +230,13 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
<>
{notes.length === 1 && (
<>
<MenuItem onClick={openRevisionHistoryModal}>
<MenuItem onClick={openRevisionHistoryModal} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="history" className={iconClass} />
Note history
{historyShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={historyShortcut} />}
</MenuItem>
<HorizontalSeparator classes="my-2" />
<MenuItem onClick={toggleLineWidthModal}>
<MenuItem onClick={toggleLineWidthModal} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="line-width" className={iconClass} />
Editor width
{editorWidthShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={editorWidthShortcut} />}
@@ -238,6 +248,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onChange={(locked) => {
application.notesController.setLockSelectedNotes(locked)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="pencil-off" className={iconClass} />
Prevent editing
@@ -247,6 +258,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onChange={(hidePreviews) => {
application.notesController.setHideSelectedNotePreviews(!hidePreviews)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="rich-text" className={iconClass} />
Show preview
@@ -256,6 +268,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onChange={(protect) => {
application.notesController.setProtectSelectedNotes(protect).catch(console.error)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="lock" className={iconClass} />
Password protect
@@ -263,13 +276,18 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
{notes.length === 1 && (
<>
<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" />
{application.featuresController.isEntitledToVaults() && (
<AddToVaultMenuOption iconClassName={iconClass} items={notes} />
<AddToVaultMenuOption iconClassName={iconClass} items={notes} disabled={!hasAdminPermissionForAllSharedNotes} />
)}
{application.navigationController.tagsCount > 0 && (
@@ -278,12 +296,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
navigationController={application.navigationController}
selectedItems={notes}
linkingController={application.linkingController}
disabled={areSomeNotesInReadonlySharedVault}
/>
)}
<MenuItem
onClick={() => {
application.notesController.setStarSelectedNotes(!starred)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="star" className={iconClass} />
{starred ? 'Unstar' : 'Star'}
@@ -295,6 +315,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onClick={() => {
application.notesController.setPinSelectedNotes(true)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="pin" className={iconClass} />
Pin to top
@@ -306,6 +327,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onClick={() => {
application.notesController.setPinSelectedNotes(false)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="unpin" className={iconClass} />
Unpin
@@ -382,7 +404,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
)}
</>
)}
<MenuItem onClick={duplicateSelectedItems}>
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="copy" className={iconClass} />
Duplicate
</MenuItem>
@@ -392,6 +414,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
await application.notesController.setArchiveSelectedNotes(true).catch(console.error)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="archive" className={iconClassWarning} />
<span className="text-warning">Archive</span>
@@ -403,6 +426,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
await application.notesController.setArchiveSelectedNotes(false).catch(console.error)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="unarchive" className={iconClassWarning} />
<span className="text-warning">Unarchive</span>
@@ -410,18 +434,23 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
)}
{notTrashed &&
(altKeyDown ? (
<DeletePermanentlyButton
<MenuItem
disabled={areSomeNotesInReadonlySharedVault}
onClick={async () => {
await application.notesController.deleteNotesPermanently()
closeMenuAndToggleNotesList()
}}
/>
>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
) : (
<MenuItem
onClick={async () => {
await application.notesController.setTrashSelectedNotes(true)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="trash" className={iconClassDanger} />
<span className="text-danger">Move to trash</span>
@@ -434,21 +463,27 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
await application.notesController.setTrashSelectedNotes(false)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="restore" className={iconClassSuccess} />
<span className="text-success">Restore</span>
</MenuItem>
<DeletePermanentlyButton
<MenuItem
disabled={areSomeNotesInReadonlySharedVault}
onClick={async () => {
await application.notesController.deleteNotesPermanently()
closeMenuAndToggleNotesList()
}}
/>
>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
<MenuItem
onClick={async () => {
await application.notesController.emptyTrash()
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<div className="flex items-start">
<Icon type="trash-sweep" className="mr-2 text-danger" />
@@ -485,16 +520,19 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
editorForNote={editorForNote}
notesController={application.notesController}
note={notes[0]}
disabled={areSomeNotesInReadonlySharedVault}
/>
)}
<HorizontalSeparator classes="my-2" />
<NoteAttributes application={application} note={notes[0]} />
<NoteAttributes className="mb-2" application={application} note={notes[0]} />
<NoteSizeWarning note={notes[0]} />
</>
) : null}
) : (
<div className="h-2" />
)}
<ModalOverlay isOpen={showExportSuperModal} close={closeSuperExportModal}>
<SuperExportModal exportNotes={downloadSelectedItems} close={closeSuperExportModal} />

View File

@@ -9,7 +9,8 @@ export const SpellcheckOptions: FunctionComponent<{
editorForNote: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
notesController: NotesController
note: SNNote
}> = ({ editorForNote, notesController, note }) => {
disabled?: boolean
}> = ({ editorForNote, notesController, note, disabled }) => {
const spellcheckControllable = editorForNote.featureDescription.spellcheckControl
const noteSpellcheck = !spellcheckControllable
? true
@@ -24,7 +25,7 @@ export const SpellcheckOptions: FunctionComponent<{
onChange={() => {
notesController.toggleGlobalSpellcheckForNote(note).catch(console.error)
}}
disabled={!spellcheckControllable}
disabled={disabled || !spellcheckControllable}
>
<Icon type="notes" className={iconClass} />
Spellcheck

View File

@@ -130,7 +130,7 @@ const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog })
wrapper: 'col-start-2',
}}
items={Object.keys(SharedVaultUserPermission.PERMISSIONS).map((key) => ({
label: key === 'Write' ? 'Read / Write' : key,
label: application.vaultUsers.getFormattedMemberPermission(key),
value: key,
}))}
value={selectedContact.permission}

View File

@@ -2,11 +2,10 @@ import { useApplication } from '@/Components/ApplicationProvider'
import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import { InviteRecord, SharedVaultUserPermission } from '@standardnotes/snjs'
import { InviteRecord } from '@standardnotes/snjs'
import { useCallback, useState } from 'react'
import EditContactModal from '../Contacts/EditContactModal'
import { CheckmarkCircle } from '../../../../UIElements/CheckmarkCircle'
import { capitalizeString } from '@/Utils'
type Props = {
inviteRecord: InviteRecord
@@ -36,10 +35,7 @@ const InviteItem = ({ inviteRecord }: Props) => {
const trustedContact = application.contacts.findSenderContactForInvite(inviteRecord.invite)
const permission =
inviteRecord.invite.permission === SharedVaultUserPermission.PERMISSIONS.Write
? 'Read / Write'
: capitalizeString(inviteRecord.invite.permission)
const permission = application.vaultUsers.getFormattedMemberPermission(inviteRecord.invite.permission)
return (
<>

View File

@@ -23,7 +23,8 @@ const VaultItem = ({ vault }: Props) => {
const [isVaultModalOpen, setIsVaultModalOpen] = useState(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 confirm = await application.alerts.confirm(
@@ -122,8 +123,8 @@ const VaultItem = ({ vault }: Props) => {
<div className="mt-2 flex w-full flex-wrap gap-3">
<Button label="Edit" onClick={openEditModal} />
{canShowLockOption && <Button label={isLocked ? 'Unlock' : 'Lock'} onClick={toggleLock} />}
{isCurrentUserAdmin && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
{!isCurrentUserAdmin && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
{isCurrentUserOwner && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
{!isCurrentUserOwner && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
{isCurrentUserAdmin ? (
vault.isSharedVaultListing() ? (
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />

View File

@@ -66,11 +66,11 @@ const EditVaultModalContent: FunctionComponent<{
if (existingVault.isSharedVaultListing()) {
setIsAdmin(
existingVault.isSharedVaultListing() && application.vaultUsers.isCurrentUserSharedVaultOwner(existingVault),
existingVault.isSharedVaultListing() && application.vaultUsers.isCurrentUserSharedVaultAdmin(existingVault),
)
setIsLoadingCollaborationInfo(true)
const users = await application.vaultUsers.getSharedVaultUsers(existingVault)
const users = await application.vaultUsers.getSharedVaultUsersFromServer(existingVault)
if (users) {
setMembers(users)
}

View File

@@ -1,9 +1,8 @@
import { useCallback } from 'react'
import { useApplication } from '@/Components/ApplicationProvider'
import { SharedVaultInviteServerHash, SharedVaultUserPermission } from '@standardnotes/snjs'
import { SharedVaultInviteServerHash } from '@standardnotes/snjs'
import Icon from '@/Components/Icon/Icon'
import Button from '@/Components/Button/Button'
import { capitalizeString } from '@/Utils'
export const VaultModalInvites = ({
invites,
@@ -30,10 +29,7 @@ export const VaultModalInvites = ({
<div className="space-y-3.5">
{invites.map((invite) => {
const contact = application.contacts.findContactForInvite(invite)
const permission =
invite.permission === SharedVaultUserPermission.PERMISSIONS.Write
? 'Read / Write'
: capitalizeString(invite.permission)
const permission = application.vaultUsers.getFormattedMemberPermission(invite.permission)
return (
<div

View File

@@ -1,9 +1,8 @@
import { useCallback } from 'react'
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 Button from '@/Components/Button/Button'
import { capitalizeString } from '@/Utils'
export const VaultModalMembers = ({
members,
@@ -35,10 +34,7 @@ export const VaultModalMembers = ({
{members.map((member) => {
const isMemberVaultOwner = application.vaultUsers.isVaultUserOwner(member)
const contact = application.contacts.findContactForServerUser(member)
const permission =
member.permission === SharedVaultUserPermission.PERMISSIONS.Write
? 'Read / Write'
: capitalizeString(member.permission)
const permission = application.vaultUsers.getFormattedMemberPermission(member.permission)
return (
<div

View File

@@ -25,6 +25,11 @@ const CodeOptionsPlugin = () => {
const [selectedElementKey, setSelectedElementKey] = useState<NodeKey | null>(null)
const updateToolbar = useCallback(() => {
if (!editor.isEditable()) {
setIsCode(false)
return
}
const selection = $getSelection()
if (!$isRangeSelection(selection)) {
return

View File

@@ -58,6 +58,7 @@ type Props = {
linkingController: LinkingController
filesController: FilesController
spellcheck: boolean
readonly?: boolean
}
export const SuperEditor: FunctionComponent<Props> = ({
@@ -66,6 +67,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
filesController,
spellcheck,
controller,
readonly,
}) => {
const note = useRef(controller.item)
const changeEditorFunction = useRef<ChangeEditorFunction>()
@@ -217,7 +219,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<ErrorBoundary>
<LinkingControllerProvider controller={linkingController}>
<FilesControllerProvider controller={filesController}>
<BlocksEditorComposer readonly={note.current.locked} initialValue={note.current.text}>
<BlocksEditorComposer readonly={note.current.locked || readonly} initialValue={note.current.text}>
<BlocksEditor
onChange={handleChange}
className={classNames(
@@ -227,6 +229,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
)}
previewLength={SuperNotePreviewCharLimit}
spellcheck={spellcheck}
readonly={note.current.locked || readonly}
>
<ItemSelectionPlugin currentNote={note.current} />
<FilePlugin currentNote={note.current} />
@@ -242,7 +245,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
<ExportPlugin />
<ReadonlyPlugin note={note.current} />
{readonly === undefined && <ReadonlyPlugin note={note.current} />}
<AutoFocusPlugin isEnabled={controller.isTemplateNote} />
<SuperSearchContextProvider>
<SearchPlugin />

View File

@@ -78,6 +78,7 @@ const VaultMenu = observer(({ items }: { items: DecryptedItemInterface[] }) => {
doesVaultContainItems(vault) ? void removeItemsFromVault() : void addItemsToVault(vault)
}}
className={doesVaultContainItems(vault) ? 'font-bold' : ''}
disabled={vault.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)}
>
<Icon
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 buttonRef = useRef<HTMLButtonElement>(null)
@@ -123,6 +132,7 @@ const AddToVaultMenuOption = ({ iconClassName, items }: { iconClassName: string;
}
}}
ref={buttonRef}
disabled={disabled}
>
<div className="flex items-center">
<Icon type="safe-square" className={iconClassName} />

View File

@@ -37,6 +37,9 @@ export const useVault = (vault: VaultListingInterface) => {
}, [application.vaultDisplayService, application.vaultLocks, canShowLockOption, isLocked, vault])
const isCurrentUserAdmin = !vault.isSharedVaultListing()
? true
: application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
const isCurrentUserOwner = !vault.isSharedVaultListing()
? true
: application.vaultUsers.isCurrentUserSharedVaultOwner(vault)
@@ -54,5 +57,6 @@ export const useVault = (vault: VaultListingInterface) => {
toggleLock,
ensureVaultIsUnlocked,
isCurrentUserAdmin,
isCurrentUserOwner,
}
}