diff --git a/.eslintignore b/.eslintignore index d32e28f338..e166bef5b8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -142,6 +142,7 @@ packages/app-desktop/gui/DialogButtonRow.js packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js packages/app-desktop/gui/DialogTitle.js packages/app-desktop/gui/DropboxLoginScreen.js +packages/app-desktop/gui/Dropdown/Dropdown.js packages/app-desktop/gui/EditFolderDialog/Dialog.js packages/app-desktop/gui/EditFolderDialog/IconSelector.js packages/app-desktop/gui/EmojiBox.js @@ -162,6 +163,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js packages/app-desktop/gui/MainScreen/MainScreen.js packages/app-desktop/gui/MainScreen/commands/addProfile.js packages/app-desktop/gui/MainScreen/commands/commandPalette.js +packages/app-desktop/gui/MainScreen/commands/deleteFolder.js +packages/app-desktop/gui/MainScreen/commands/deleteNote.js +packages/app-desktop/gui/MainScreen/commands/duplicateNote.js packages/app-desktop/gui/MainScreen/commands/editAlarm.js packages/app-desktop/gui/MainScreen/commands/exportPdf.js packages/app-desktop/gui/MainScreen/commands/gotoAnything.js @@ -196,6 +200,7 @@ packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js packages/app-desktop/gui/MainScreen/commands/toggleEditors.js packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js packages/app-desktop/gui/MainScreen/commands/toggleNoteList.js +packages/app-desktop/gui/MainScreen/commands/toggleNoteType.js packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderField.js packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderReverse.js packages/app-desktop/gui/MainScreen/commands/togglePerFolderSortOrder.js @@ -502,12 +507,14 @@ packages/lib/commands/openMasterPasswordDialog.js packages/lib/commands/synchronize.js packages/lib/components/EncryptionConfigScreen/utils.js packages/lib/components/shared/note-screen-shared.js +packages/lib/components/shared/reduxSharedMiddleware.js packages/lib/database-driver-better-sqlite.js packages/lib/database.js packages/lib/debug/DebugService.js packages/lib/dom.js packages/lib/dummy.test.js packages/lib/errorUtils.js +packages/lib/errors.js packages/lib/eventManager.js packages/lib/file-api-driver-joplinServer.js packages/lib/file-api-driver-memory.js @@ -562,6 +569,7 @@ packages/lib/models/settings/FileHandler.js packages/lib/models/utils/itemCanBeEncrypted.js packages/lib/models/utils/paginatedFeed.js packages/lib/models/utils/paginationToSql.js +packages/lib/models/utils/readOnly.js packages/lib/models/utils/types.js packages/lib/models/utils/userData.js packages/lib/models/utils/userData.test.js @@ -765,7 +773,10 @@ packages/lib/services/synchronizer/syncInfoUtils.test.js packages/lib/services/synchronizer/synchronizer_LockHandler.test.js packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.js packages/lib/services/synchronizer/tools.js +packages/lib/services/synchronizer/utils/handleConflictAction.js packages/lib/services/synchronizer/utils/handleSyncStartupOperation.js +packages/lib/services/synchronizer/utils/resourceRemotePath.js +packages/lib/services/synchronizer/utils/syncDeleteStep.js packages/lib/services/synchronizer/utils/types.js packages/lib/shim.js packages/lib/testing/syncTargetUtils.js diff --git a/.gitignore b/.gitignore index 4ef50cb791..79528ffc64 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,7 @@ packages/app-desktop/gui/DialogButtonRow.js packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js packages/app-desktop/gui/DialogTitle.js packages/app-desktop/gui/DropboxLoginScreen.js +packages/app-desktop/gui/Dropdown/Dropdown.js packages/app-desktop/gui/EditFolderDialog/Dialog.js packages/app-desktop/gui/EditFolderDialog/IconSelector.js packages/app-desktop/gui/EmojiBox.js @@ -147,6 +148,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js packages/app-desktop/gui/MainScreen/MainScreen.js packages/app-desktop/gui/MainScreen/commands/addProfile.js packages/app-desktop/gui/MainScreen/commands/commandPalette.js +packages/app-desktop/gui/MainScreen/commands/deleteFolder.js +packages/app-desktop/gui/MainScreen/commands/deleteNote.js +packages/app-desktop/gui/MainScreen/commands/duplicateNote.js packages/app-desktop/gui/MainScreen/commands/editAlarm.js packages/app-desktop/gui/MainScreen/commands/exportPdf.js packages/app-desktop/gui/MainScreen/commands/gotoAnything.js @@ -181,6 +185,7 @@ packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js packages/app-desktop/gui/MainScreen/commands/toggleEditors.js packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js packages/app-desktop/gui/MainScreen/commands/toggleNoteList.js +packages/app-desktop/gui/MainScreen/commands/toggleNoteType.js packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderField.js packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderReverse.js packages/app-desktop/gui/MainScreen/commands/togglePerFolderSortOrder.js @@ -487,12 +492,14 @@ packages/lib/commands/openMasterPasswordDialog.js packages/lib/commands/synchronize.js packages/lib/components/EncryptionConfigScreen/utils.js packages/lib/components/shared/note-screen-shared.js +packages/lib/components/shared/reduxSharedMiddleware.js packages/lib/database-driver-better-sqlite.js packages/lib/database.js packages/lib/debug/DebugService.js packages/lib/dom.js packages/lib/dummy.test.js packages/lib/errorUtils.js +packages/lib/errors.js packages/lib/eventManager.js packages/lib/file-api-driver-joplinServer.js packages/lib/file-api-driver-memory.js @@ -547,6 +554,7 @@ packages/lib/models/settings/FileHandler.js packages/lib/models/utils/itemCanBeEncrypted.js packages/lib/models/utils/paginatedFeed.js packages/lib/models/utils/paginationToSql.js +packages/lib/models/utils/readOnly.js packages/lib/models/utils/types.js packages/lib/models/utils/userData.js packages/lib/models/utils/userData.test.js @@ -750,7 +758,10 @@ packages/lib/services/synchronizer/syncInfoUtils.test.js packages/lib/services/synchronizer/synchronizer_LockHandler.test.js packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.js packages/lib/services/synchronizer/tools.js +packages/lib/services/synchronizer/utils/handleConflictAction.js packages/lib/services/synchronizer/utils/handleSyncStartupOperation.js +packages/lib/services/synchronizer/utils/resourceRemotePath.js +packages/lib/services/synchronizer/utils/syncDeleteStep.js packages/lib/services/synchronizer/utils/types.js packages/lib/shim.js packages/lib/testing/syncTargetUtils.js diff --git a/README.md b/README.md index c9f686179e..9052844ccf 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ A community maintained list of these distributions can be found here: [Unofficia - [Server: File URL Format](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_file_url_format.md) - [Server: Delta Sync](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_delta_sync.md) - [Server: Sharing](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_sharing.md) + - [Read-only items](https://github.com/laurent22/joplin/blob/dev/readme/spec/read_only.md) - Google Summer of Code 2022 diff --git a/packages/app-desktop/commands/startExternalEditing.ts b/packages/app-desktop/commands/startExternalEditing.ts index f1443f27e9..c1ed130155 100644 --- a/packages/app-desktop/commands/startExternalEditing.ts +++ b/packages/app-desktop/commands/startExternalEditing.ts @@ -23,6 +23,6 @@ export const runtime = (): CommandRuntime => { bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message)); } }, - enabledCondition: 'oneNoteSelected', + enabledCondition: 'oneNoteSelected && !noteIsReadOnly', }; }; diff --git a/packages/app-desktop/commands/stopExternalEditing.ts b/packages/app-desktop/commands/stopExternalEditing.ts index 7999ac7ffd..0988ea5e58 100644 --- a/packages/app-desktop/commands/stopExternalEditing.ts +++ b/packages/app-desktop/commands/stopExternalEditing.ts @@ -15,6 +15,6 @@ export const runtime = (): CommandRuntime => { noteId = noteId || stateUtils.selectedNoteId(context.state); void ExternalEditWatcher.instance().stopWatching(noteId); }, - enabledCondition: 'oneNoteSelected', + enabledCondition: 'oneNoteSelected && !noteIsReadOnly', }; }; diff --git a/packages/app-desktop/commands/toggleExternalEditing.ts b/packages/app-desktop/commands/toggleExternalEditing.ts index 5a40993e9b..506cd072f0 100644 --- a/packages/app-desktop/commands/toggleExternalEditing.ts +++ b/packages/app-desktop/commands/toggleExternalEditing.ts @@ -2,6 +2,7 @@ import CommandService, { CommandRuntime, CommandDeclaration } from '@joplin/lib/ import { _ } from '@joplin/lib/locale'; import { stateUtils } from '@joplin/lib/reducer'; import { DesktopCommandContext } from '../services/commands/types'; +import { enabledCondition } from '../gui/NoteEditor/editorCommandDeclarations'; export const declaration: CommandDeclaration = { name: 'toggleExternalEditing', @@ -22,7 +23,7 @@ export const runtime = (): CommandRuntime => { void CommandService.instance().execute('startExternalEditing', noteId); } }, - enabledCondition: 'oneNoteSelected', + enabledCondition: enabledCondition(declaration.name), mapStateToTitle: (state: any) => { const noteId = stateUtils.selectedNoteId(state); return state.watchedNoteFiles.includes(noteId) ? _('Stop') : ''; diff --git a/packages/app-desktop/gui/Dropdown/Dropdown.tsx b/packages/app-desktop/gui/Dropdown/Dropdown.tsx new file mode 100644 index 0000000000..645b259dc5 --- /dev/null +++ b/packages/app-desktop/gui/Dropdown/Dropdown.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { useMemo, useCallback } from 'react'; + +export type DropdownOptions = Record; +export enum DropdownVariant { + Default = 1, + NoBorder, +} + +export interface ChangeEvent { + value: string; +} + +export type ChangeEventHandler = (event: ChangeEvent)=> void; + +interface Props { + options: DropdownOptions; + variant?: DropdownVariant; + className?: string; + onChange?: ChangeEventHandler; + value?: string; + disabled?: boolean; +} + +export const Dropdown = (props: Props) => { + const renderOptions = () => { + const optionComps = []; + for (const [value, label] of Object.entries(props.options)) { + optionComps.push(); + } + return optionComps; + }; + + const onChange = useCallback((event: any) => { + props.onChange({ value: event.target.value }); + }, [props.onChange]); + + const classNames = useMemo(() => { + const variant = props.variant || DropdownVariant.Default; + const output = [ + 'dropdown-control', + `-variant${variant}`, + ]; + if (props.className) output.push(props.className); + return output.join(' '); + }, [props.variant, props.className]); + + return ( + + ); +}; diff --git a/packages/app-desktop/gui/Dropdown/style.scss b/packages/app-desktop/gui/Dropdown/style.scss new file mode 100644 index 0000000000..997502bd2b --- /dev/null +++ b/packages/app-desktop/gui/Dropdown/style.scss @@ -0,0 +1,14 @@ +.dropdown-control { + border: 1px solid var(--joplin-border-color4); + border-radius: 3px; + font-size: var(--joplin-font-size)px; + color: var(--joplin-color); + padding: 0 8px; + box-sizing: border-box; + background-color: var(--joplin-background-color4); + min-height: 26px; + + &.-variant2 { + border: none; + } +} \ No newline at end of file diff --git a/packages/app-desktop/gui/EditFolderDialog/Dialog.tsx b/packages/app-desktop/gui/EditFolderDialog/Dialog.tsx index 5baf449e98..06342fb8fa 100644 --- a/packages/app-desktop/gui/EditFolderDialog/Dialog.tsx +++ b/packages/app-desktop/gui/EditFolderDialog/Dialog.tsx @@ -63,6 +63,8 @@ export default function(props: Props) { const folder: FolderEntity = { title: folderTitle, icon: Folder.serializeIcon(folderIcon), + is_shared: 0, + share_id: '', }; if (!isNew) folder.id = props.folderId; diff --git a/packages/app-desktop/gui/MainScreen/commands/deleteFolder.ts b/packages/app-desktop/gui/MainScreen/commands/deleteFolder.ts new file mode 100644 index 0000000000..be49ddb66d --- /dev/null +++ b/packages/app-desktop/gui/MainScreen/commands/deleteFolder.ts @@ -0,0 +1,30 @@ +import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; +import { _ } from '@joplin/lib/locale'; +import bridge from '../../../services/bridge'; +import Folder from '@joplin/lib/models/Folder'; +const { substrWithEllipsis } = require('@joplin/lib/string-utils'); + +export const declaration: CommandDeclaration = { + name: 'deleteFolder', + label: () => _('Delete notebook'), +}; + +export const runtime = (): CommandRuntime => { + return { + execute: async (context: CommandContext, folderId: string = null) => { + if (folderId === null) folderId = context.state.selectedFolderId; + + const folder = await Folder.load(folderId); + if (!folder) throw new Error(`No such folder: ${folderId}`); + + const ok = bridge().showConfirmMessageBox(_('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32)), { + buttons: [_('Delete'), _('Cancel')], + defaultId: 1, + }); + if (!ok) return; + + await Folder.delete(folderId); + }, + enabledCondition: '!folderIsReadOnly', + }; +}; diff --git a/packages/app-desktop/gui/MainScreen/commands/deleteNote.ts b/packages/app-desktop/gui/MainScreen/commands/deleteNote.ts new file mode 100644 index 0000000000..45aa49aaff --- /dev/null +++ b/packages/app-desktop/gui/MainScreen/commands/deleteNote.ts @@ -0,0 +1,32 @@ +import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; +import { _ } from '@joplin/lib/locale'; +import Note from '@joplin/lib/models/Note'; +import bridge from '../../../services/bridge'; + +export const declaration: CommandDeclaration = { + name: 'deleteNote', + label: () => _('Delete note'), + iconName: 'fa-times', +}; + +export const runtime = (): CommandRuntime => { + return { + execute: async (context: CommandContext, noteIds: string[] = null) => { + if (noteIds === null) noteIds = context.state.selectedNoteIds; + + if (!noteIds.length) return; + + const msg = await Note.deleteMessage(noteIds); + if (!msg) return; + + const ok = bridge().showConfirmMessageBox(msg, { + buttons: [_('Delete'), _('Cancel')], + defaultId: 1, + }); + + if (!ok) return; + await Note.batchDelete(noteIds); + }, + enabledCondition: '!noteIsReadOnly', + }; +}; diff --git a/packages/app-desktop/gui/MainScreen/commands/duplicateNote.ts b/packages/app-desktop/gui/MainScreen/commands/duplicateNote.ts new file mode 100644 index 0000000000..e2ab87240d --- /dev/null +++ b/packages/app-desktop/gui/MainScreen/commands/duplicateNote.ts @@ -0,0 +1,24 @@ +import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; +import { _ } from '@joplin/lib/locale'; +import Note from '@joplin/lib/models/Note'; + +export const declaration: CommandDeclaration = { + name: 'duplicateNote', + label: () => _('Duplicate'), +}; + +export const runtime = (): CommandRuntime => { + return { + execute: async (context: CommandContext, noteIds: string[] = null) => { + if (noteIds === null) noteIds = context.state.selectedNoteIds; + + for (let i = 0; i < noteIds.length; i++) { + const note = await Note.load(noteIds[i]); + await Note.duplicate(noteIds[i], { + uniqueTitle: _('%s - Copy', note.title), + }); + } + }, + enabledCondition: '!noteIsReadOnly', + }; +}; diff --git a/packages/app-desktop/gui/MainScreen/commands/index.ts b/packages/app-desktop/gui/MainScreen/commands/index.ts index d1b4f38089..d496cef636 100644 --- a/packages/app-desktop/gui/MainScreen/commands/index.ts +++ b/packages/app-desktop/gui/MainScreen/commands/index.ts @@ -1,6 +1,9 @@ // AUTO-GENERATED using `gulp buildCommandIndex` import * as addProfile from './addProfile'; import * as commandPalette from './commandPalette'; +import * as deleteFolder from './deleteFolder'; +import * as deleteNote from './deleteNote'; +import * as duplicateNote from './duplicateNote'; import * as editAlarm from './editAlarm'; import * as exportPdf from './exportPdf'; import * as gotoAnything from './gotoAnything'; @@ -34,6 +37,7 @@ import * as showSpellCheckerMenu from './showSpellCheckerMenu'; import * as toggleEditors from './toggleEditors'; import * as toggleLayoutMoveMode from './toggleLayoutMoveMode'; import * as toggleNoteList from './toggleNoteList'; +import * as toggleNoteType from './toggleNoteType'; import * as toggleNotesSortOrderField from './toggleNotesSortOrderField'; import * as toggleNotesSortOrderReverse from './toggleNotesSortOrderReverse'; import * as togglePerFolderSortOrder from './togglePerFolderSortOrder'; @@ -43,6 +47,9 @@ import * as toggleVisiblePanes from './toggleVisiblePanes'; const index:any[] = [ addProfile, commandPalette, + deleteFolder, + deleteNote, + duplicateNote, editAlarm, exportPdf, gotoAnything, @@ -76,6 +83,7 @@ const index:any[] = [ toggleEditors, toggleLayoutMoveMode, toggleNoteList, + toggleNoteType, toggleNotesSortOrderField, toggleNotesSortOrderReverse, togglePerFolderSortOrder, diff --git a/packages/app-desktop/gui/MainScreen/commands/moveToFolder.ts b/packages/app-desktop/gui/MainScreen/commands/moveToFolder.ts index fb3de5b6da..a2b384fde5 100644 --- a/packages/app-desktop/gui/MainScreen/commands/moveToFolder.ts +++ b/packages/app-desktop/gui/MainScreen/commands/moveToFolder.ts @@ -44,6 +44,6 @@ export const runtime = (comp: any): CommandRuntime => { }, }); }, - enabledCondition: 'someNotesSelected', + enabledCondition: 'someNotesSelected && !noteIsReadOnly', }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/newFolder.ts b/packages/app-desktop/gui/MainScreen/commands/newFolder.ts index c1b5873d88..68a16fdffc 100644 --- a/packages/app-desktop/gui/MainScreen/commands/newFolder.ts +++ b/packages/app-desktop/gui/MainScreen/commands/newFolder.ts @@ -18,5 +18,6 @@ export const runtime = (): CommandRuntime => { void CommandService.instance().execute('openFolderDialog', options); }, + enabledCondition: '!folderIsReadOnly', }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/newNote.ts b/packages/app-desktop/gui/MainScreen/commands/newNote.ts index 479a0ff138..8338741d29 100644 --- a/packages/app-desktop/gui/MainScreen/commands/newNote.ts +++ b/packages/app-desktop/gui/MainScreen/commands/newNote.ts @@ -30,6 +30,6 @@ export const runtime = (): CommandRuntime => { id: newNote.id, }); }, - enabledCondition: 'oneFolderSelected && !inConflictFolder', + enabledCondition: 'oneFolderSelected && !inConflictFolder && !folderIsReadOnly', }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/newSubFolder.ts b/packages/app-desktop/gui/MainScreen/commands/newSubFolder.ts index b4b11e4920..3d4e214e26 100644 --- a/packages/app-desktop/gui/MainScreen/commands/newSubFolder.ts +++ b/packages/app-desktop/gui/MainScreen/commands/newSubFolder.ts @@ -13,5 +13,6 @@ export const runtime = (): CommandRuntime => { parentId = parentId || context.state.selectedFolderId; return CommandService.instance().execute('newFolder', parentId); }, + enabledCondition: '!folderIsReadOnly', }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/newTodo.ts b/packages/app-desktop/gui/MainScreen/commands/newTodo.ts index 2054306958..66a54c54d4 100644 --- a/packages/app-desktop/gui/MainScreen/commands/newTodo.ts +++ b/packages/app-desktop/gui/MainScreen/commands/newTodo.ts @@ -12,6 +12,6 @@ export const runtime = (): CommandRuntime => { execute: async (_context: CommandContext, body = '') => { return CommandService.instance().execute('newNote', body, true); }, - enabledCondition: 'oneFolderSelected && !inConflictFolder', + enabledCondition: 'oneFolderSelected && !inConflictFolder && !folderIsReadOnly', }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/openFolderDialog.ts b/packages/app-desktop/gui/MainScreen/commands/openFolderDialog.ts index dc405b065f..3d8fa820e7 100644 --- a/packages/app-desktop/gui/MainScreen/commands/openFolderDialog.ts +++ b/packages/app-desktop/gui/MainScreen/commands/openFolderDialog.ts @@ -33,5 +33,6 @@ export const runtime = (): CommandRuntime => { }, }); }, + enabledCondition: '!folderIsReadOnly', }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/toggleNoteType.ts b/packages/app-desktop/gui/MainScreen/commands/toggleNoteType.ts new file mode 100644 index 0000000000..83df6ef4fa --- /dev/null +++ b/packages/app-desktop/gui/MainScreen/commands/toggleNoteType.ts @@ -0,0 +1,30 @@ +import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; +import { _ } from '@joplin/lib/locale'; +import Note from '@joplin/lib/models/Note'; +import eventManager from '@joplin/lib/eventManager'; + +export const declaration: CommandDeclaration = { + name: 'toggleNoteType', + label: () => _('Switch between note and to-do type'), +}; + +export const runtime = (): CommandRuntime => { + return { + execute: async (context: CommandContext, noteIds: string[] = null) => { + if (noteIds === null) noteIds = context.state.selectedNoteIds; + + for (let i = 0; i < noteIds.length; i++) { + const note = await Note.load(noteIds[i]); + const newNote = await Note.save(Note.toggleIsTodo(note), { userSideValidation: true }); + const eventNote = { + id: newNote.id, + is_todo: newNote.is_todo, + todo_due: newNote.todo_due, + todo_completed: newNote.todo_completed, + }; + eventManager.emit('noteTypeToggle', { noteId: note.id, note: eventNote }); + } + }, + enabledCondition: '!noteIsReadOnly', + }; +}; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx index ec71561b8b..f9c4b2d669 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx @@ -893,7 +893,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { mode={props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML ? 'xml' : 'joplin-markdown'} codeMirrorTheme={styles.editor.codeMirrorTheme} style={styles.editor} - readOnly={props.visiblePanes.indexOf('editor') < 0} + readOnly={props.disabled || props.visiblePanes.indexOf('editor') < 0} autoMatchBraces={matchBracesOptions} keyMap={props.keyboardMode} plugins={props.plugins} @@ -927,7 +927,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
- + {props.noteToolbar}
diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx index 57f6ff723e..08a0885d64 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.tsx @@ -167,6 +167,7 @@ function Editor(props: EditorProps, ref: any) { const safeOptions: Record = { value: props.value, + readOnly: props.readOnly, }; const unsafeOptions: Record = { diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx index 8f5286607a..991b921683 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx @@ -11,6 +11,7 @@ const { buildStyle } = require('@joplin/lib/theme'); interface ToolbarProps { themeId: number; toolbarButtonInfos: ToolbarButtonInfo[]; + disabled?: boolean; } function styles_(props: ToolbarProps) { @@ -28,7 +29,7 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance()); function Toolbar(props: ToolbarProps) { const styles = styles_(props); - return ; + return ; } const mapStateToProps = (state: AppState) => { diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index fd88be83df..ed43855121 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -31,7 +31,7 @@ import usePrevious from '../hooks/usePrevious'; import Setting from '@joplin/lib/models/Setting'; import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext'; import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher'; - +import { itemIsReadOnly } from '@joplin/lib/models/utils/readOnly'; const { themeStyle } = require('@joplin/lib/theme'); const { substrWithEllipsis } = require('@joplin/lib/string-utils'); import NoteSearchBar from '../NoteSearchBar'; @@ -40,6 +40,12 @@ import Note from '@joplin/lib/models/Note'; import Folder from '@joplin/lib/models/Folder'; const bridge = require('@electron/remote').require('./bridge').default; import NoteRevisionViewer from '../NoteRevisionViewer'; +import { readFromSettings } from '@joplin/lib/services/share/reducer'; +import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; +import { ModelType } from '@joplin/lib/BaseModel'; +import BaseItem from '@joplin/lib/models/BaseItem'; +import { ErrorCode } from '@joplin/lib/errors'; +import ItemChange from '@joplin/lib/models/ItemChange'; const commands = [ require('./commands/showRevisions'), @@ -51,6 +57,7 @@ function NoteEditor(props: NoteEditorProps) { const [showRevisions, setShowRevisions] = useState(false); const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false); const [scrollWhenReady, setScrollWhenReady] = useState(null); + const [isReadOnly, setIsReadOnly] = useState(false); const editorRef = useRef(); const titleInputRef = useRef(); @@ -279,6 +286,23 @@ function NoteEditor(props: NoteEditorProps) { // } // }, [props.dispatch]); + useAsyncEffect(async event => { + if (!formNote.id) return; + + try { + const result = await itemIsReadOnly(BaseItem, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, formNote.id, props.syncUserId, props.shareCache); + if (event.cancelled) return; + setIsReadOnly(result); + } catch (error) { + if (error.code === ErrorCode.NotFound) { + // Can happen if the note has been deleted but a render is + // triggered anyway. It can be ignored. + } else { + throw error; + } + } + }, [formNote.id, props.syncUserId, props.shareCache]); + const onBodyWillChange = useCallback((event: any) => { handleProvisionalFlag(); @@ -406,7 +430,7 @@ function NoteEditor(props: NoteEditorProps) { htmlToMarkdown: htmlToMarkdown, markupToHtml: markupToHtml, allAssets: allAssets, - disabled: false, + disabled: isReadOnly, themeId: props.themeId, dispatch: props.dispatch, noteToolbar: null, @@ -570,6 +594,7 @@ function NoteEditor(props: NoteEditorProps) { noteTitle={formNote.title} noteUserUpdatedTime={formNote.user_updated_time} onTitleChange={onTitleChange} + disabled={isReadOnly} /> {renderSearchInfo()}
@@ -629,7 +654,9 @@ const mapStateToProps = (state: AppState) => { ], whenClauseContext)[0], contentMaxWidth: state.settings['style.editor.contentMaxWidth'], isSafeMode: state.settings.isSafeMode, - useCustomPdfViewer: false, // state.settings.useCustomPdfViewer, + useCustomPdfViewer: false, + syncUserId: state.settings['sync.userId'], + shareCache: readFromSettings(state), }; }; diff --git a/packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx b/packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx index 5b4d655d38..8ee510d2a6 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx @@ -38,6 +38,7 @@ interface Props { isProvisional: boolean; titleInputRef: any; onTitleChange(event: ChangeEvent): void; + disabled: boolean; } function styles_(props: Props) { @@ -98,6 +99,7 @@ export default function NoteTitleBar(props: Props) { return ; } @@ -109,6 +111,7 @@ export default function NoteTitleBar(props: Props) { ref={props.titleInputRef} placeholder={props.isProvisional ? _('Creating new %s...', props.noteIsTodo ? _('to-do') : _('note')) : ''} style={styles.titleInput} + readOnly={props.disabled} onChange={props.onTitleChange} onKeyDown={onTitleKeydown} value={props.noteTitle} diff --git a/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts b/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts index 2bc383524f..34b38512b6 100644 --- a/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts +++ b/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts @@ -1,5 +1,11 @@ import { CommandDeclaration } from '@joplin/lib/services/CommandService'; import { _ } from '@joplin/lib/locale'; +import { joplinCommandToTinyMceCommands } from './NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands'; + +export const enabledCondition = (commandName: string) => { + const markdownEditorOnly = !Object.keys(joplinCommandToTinyMceCommands).includes(commandName); + return `(!modalDialogVisible || gotoAnythingVisible) ${markdownEditorOnly ? '&& markdownEditorPaneVisible' : ''} && oneNoteSelected && noteIsMarkdown && !noteIsReadOnly`; +}; const declarations: CommandDeclaration[] = [ { diff --git a/packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts b/packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts index 029f2b499a..0ef3a6b636 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts @@ -7,10 +7,13 @@ const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; import Resource from '@joplin/lib/models/Resource'; import BaseItem from '@joplin/lib/models/BaseItem'; -import BaseModel from '@joplin/lib/BaseModel'; +import BaseModel, { ModelType } from '@joplin/lib/BaseModel'; import { processPastedHtml } from './resourceHandling'; import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types'; import { TinyMceEditorEvents } from '../NoteBody/TinyMCE/utils/types'; +import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly'; +import Setting from '@joplin/lib/models/Setting'; +import ItemChange from '@joplin/lib/models/ItemChange'; const fs = require('fs-extra'); const { writeFile } = require('fs-extra'); const { clipboard } = require('electron'); @@ -50,7 +53,11 @@ export async function openItemById(itemId: string, dispatch: Function, hash = '' } try { - await ResourceEditWatcher.instance().openAndWatch(resource.id); + if (itemIsReadOnlySync(ModelType.Resource, ItemChange.SOURCE_UNSPECIFIED, resource as ItemSlice, Setting.value('sync.userId'), BaseItem.syncShareCache)) { + await ResourceEditWatcher.instance().openAsReadOnly(resource.id); + } else { + await ResourceEditWatcher.instance().openAndWatch(resource.id); + } } catch (error) { console.error(error); bridge().showErrorMessageBox(error.message); diff --git a/packages/app-desktop/gui/NoteEditor/utils/types.ts b/packages/app-desktop/gui/NoteEditor/utils/types.ts index 4daaae8c04..8d0c938d7a 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/types.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/types.ts @@ -2,6 +2,7 @@ import AsyncActionQueue from '@joplin/lib/AsyncActionQueue'; import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils'; import { PluginStates } from '@joplin/lib/services/plugins/reducer'; +import { State as ShareState } from '@joplin/lib/services/share/reducer'; import { MarkupLanguage } from '@joplin/renderer'; import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml'; import { MarkupToHtmlOptions } from './useMarkupToHtml'; @@ -45,6 +46,8 @@ export interface NoteEditorProps { contentMaxWidth: number; isSafeMode: boolean; useCustomPdfViewer: boolean; + shareCache: ShareState; + syncUserId: string; } export interface NoteBodyEditorProps { diff --git a/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts b/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts index 52f877d8b8..980032fe87 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts @@ -1,10 +1,9 @@ import { useEffect } from 'react'; import { FormNote, ScrollOptionTypes } from './types'; -import editorCommandDeclarations from '../editorCommandDeclarations'; +import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations'; import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService'; import time from '@joplin/lib/time'; import { reg } from '@joplin/lib/registry'; -import { joplinCommandToTinyMceCommands } from '../NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands'; const commandsWithDependencies = [ require('../commands/showLocalSearch'), @@ -30,8 +29,6 @@ interface HookDependencies { // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any, setFormNote: Function): CommandRuntime { - const markdownEditorOnly = !Object.keys(joplinCommandToTinyMceCommands).includes(declaration.name); - return { execute: async (_context: CommandContext, ...args: any[]) => { if (!editorRef.current) { @@ -73,7 +70,7 @@ function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any, s // currently selected text. // // https://github.com/laurent22/joplin/issues/5707 - enabledCondition: `(!modalDialogVisible || gotoAnythingVisible) ${markdownEditorOnly ? '&& markdownEditorPaneVisible' : ''} && oneNoteSelected && noteIsMarkdown`, + enabledCondition: enabledCondition(declaration.name), }; } diff --git a/packages/app-desktop/gui/NoteList/NoteList.tsx b/packages/app-desktop/gui/NoteList/NoteList.tsx index 6eaa878a21..5c3c1fa179 100644 --- a/packages/app-desktop/gui/NoteList/NoteList.tsx +++ b/packages/app-desktop/gui/NoteList/NoteList.tsx @@ -5,7 +5,7 @@ import eventManager from '@joplin/lib/eventManager'; import NoteListUtils from '../utils/NoteListUtils'; import { _ } from '@joplin/lib/locale'; import time from '@joplin/lib/time'; -import BaseModel from '@joplin/lib/BaseModel'; +import BaseModel, { ModelType } from '@joplin/lib/BaseModel'; import bridge from '../../services/bridge'; import Setting from '@joplin/lib/models/Setting'; import NoteListItem from '../NoteListItem'; @@ -19,6 +19,9 @@ import Note from '@joplin/lib/models/Note'; import Folder from '@joplin/lib/models/Folder'; import { Props } from './types'; import usePrevious from '../hooks/usePrevious'; +import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly'; +import { FolderEntity } from '@joplin/lib/services/database/types'; +import ItemChange from '@joplin/lib/models/ItemChange'; const commands = [ require('./commands/focusElementNoteList'), @@ -186,7 +189,7 @@ const NoteListComponent = (props: Props) => { setDragOverTargetNoteIndex(null); const targetNoteIndex = dragTargetNoteIndex_(event); - const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids')); + const noteIds: string[] = JSON.parse(dt.getData('text/x-jop-note-ids')); void Note.insertNotesAt(props.selectedFolderId, noteIds, targetNoteIndex, props.uncompletedTodosOnTop, props.showCompletedTodos); }; @@ -223,7 +226,9 @@ const NoteListComponent = (props: Props) => { } }; - const noteItem_dragStart = (event: any) => { + const noteItem_dragStart = useCallback((event: any) => { + if (props.parentFolderIsReadOnly) return false; + let noteIds = []; // Here there is two cases: @@ -236,13 +241,14 @@ const NoteListComponent = (props: Props) => { if (clickedNoteId) noteIds.push(clickedNoteId); } - if (!noteIds.length) return; + if (!noteIds.length) return false; event.dataTransfer.setDragImage(new Image(), 1, 1); event.dataTransfer.clearData(); event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds)); event.dataTransfer.effectAllowed = 'move'; - }; + return true; + }, [props.parentFolderIsReadOnly, props.selectedNoteIds]); const renderItem = useCallback((item: any, index: number) => { const highlightedWords = () => { @@ -278,6 +284,7 @@ const NoteListComponent = (props: Props) => { onNoteDragOver={noteItem_noteDragOver} onTitleClick={noteItem_titleClick} onContextMenu={itemContextMenu} + draggable={!props.parentFolderIsReadOnly} />; // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [style, props.themeId, width, itemHeight, dragOverTargetNoteIndex, props.provisionalNoteIds, props.selectedNoteIds, props.watchedNoteFiles, @@ -286,6 +293,7 @@ const NoteListComponent = (props: Props) => { props.searches, props.selectedSearchId, props.highlightedWords, + props.parentFolderIsReadOnly, ]); const previousSelectedNoteIds = usePrevious(props.selectedNoteIds, []); @@ -393,7 +401,8 @@ const NoteListComponent = (props: Props) => { if (noteIds.length && (keyCode === 46 || (keyCode === 8 && event.metaKey))) { // DELETE / CMD+Backspace event.preventDefault(); - await NoteListUtils.confirmDeleteNotes(noteIds); + void CommandService.instance().execute('deleteNote', noteIds); + // await NoteListUtils.confirmDeleteNotes(noteIds); } if (noteIds.length && keyCode === 32) { @@ -541,6 +550,9 @@ const NoteListComponent = (props: Props) => { }; const mapStateToProps = (state: AppState) => { + const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? BaseModel.byId(state.folders, state.selectedFolderId) : null; + const userId = state.settings['sync.userId']; + return { notes: state.notes, folders: state.folders, @@ -560,6 +572,7 @@ const mapStateToProps = (state: AppState) => { plugins: state.pluginService.plugins, customCss: state.customCss, focusedField: state.focusedField, + parentFolderIsReadOnly: state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync(ModelType.Folder, ItemChange.SOURCE_UNSPECIFIED, selectedFolder as ItemSlice, userId, state.shareService) : false, }; }; diff --git a/packages/app-desktop/gui/NoteList/types.ts b/packages/app-desktop/gui/NoteList/types.ts index f4544e8c80..2f9758b589 100644 --- a/packages/app-desktop/gui/NoteList/types.ts +++ b/packages/app-desktop/gui/NoteList/types.ts @@ -25,4 +25,5 @@ export interface Props { provisionalNoteIds: string[]; visible: boolean; focusedField: string; + parentFolderIsReadOnly: boolean; } diff --git a/packages/app-desktop/gui/NoteListControls/NoteListControls.tsx b/packages/app-desktop/gui/NoteListControls/NoteListControls.tsx index c9b3dc6961..dc5af87ee4 100644 --- a/packages/app-desktop/gui/NoteListControls/NoteListControls.tsx +++ b/packages/app-desktop/gui/NoteListControls/NoteListControls.tsx @@ -9,7 +9,8 @@ import Note from '@joplin/lib/models/Note'; import { notesSortOrderNextField } from '../../services/sortOrder/notesSortOrderUtils'; import { _ } from '@joplin/lib/locale'; const { connect } = require('react-redux'); -const styled = require('styled-components').default; +import styled from 'styled-components'; +import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext'; enum BaseBreakpoint { Sm = 75, @@ -27,6 +28,8 @@ interface Props { height: number; width: number; onContentHeightChange: (sameRow: boolean)=> void; + newNoteButtonEnabled: boolean; + newTodoButtonEnabled: boolean; } interface Breakpoints { @@ -255,6 +258,7 @@ function NoteListControls(props: Props) { level={ButtonLevel.Primary} size={ButtonSize.Small} onClick={onNewNoteButtonClick} + disabled={!props.newNoteButtonEnabled} /> ); @@ -300,9 +305,13 @@ function NoteListControls(props: Props) { } const mapStateToProps = (state: AppState) => { + const whenClauseContext = stateToWhenClauseContext(state); + return { // TODO: showNewNoteButtons and the logic associated is not needed anymore. showNewNoteButtons: true, + newNoteButtonEnabled: CommandService.instance().isEnabled('newNote', whenClauseContext), + newTodoButtonEnabled: CommandService.instance().isEnabled('newTodo', whenClauseContext), sortOrderButtonsVisible: state.settings['notes.sortOrder.buttonsVisible'], sortOrderField: state.settings['notes.sortOrder.field'], sortOrderReverse: state.settings['notes.sortOrder.reverse'], diff --git a/packages/app-desktop/gui/NoteListItem.tsx b/packages/app-desktop/gui/NoteListItem.tsx index a9033b1588..b320e54228 100644 --- a/packages/app-desktop/gui/NoteListItem.tsx +++ b/packages/app-desktop/gui/NoteListItem.tsx @@ -58,6 +58,7 @@ interface NoteListItemProps { onNoteDragOver: any; onTitleClick: any; onContextMenu(event: React.MouseEvent): void; + draggable: boolean; } function NoteListItem(props: NoteListItemProps, ref: any) { @@ -185,7 +186,7 @@ function NoteListItem(props: NoteListItemProps, ref: any) { ref={anchorRef} onContextMenu={props.onContextMenu} href="#" - draggable={true} + draggable={props.draggable} style={listItemTitleStyle} onClick={onTitleClick} onDragStart={props.onDragStart} diff --git a/packages/app-desktop/gui/NoteToolbar/NoteToolbar.tsx b/packages/app-desktop/gui/NoteToolbar/NoteToolbar.tsx index 333344516c..8d5be99d71 100644 --- a/packages/app-desktop/gui/NoteToolbar/NoteToolbar.tsx +++ b/packages/app-desktop/gui/NoteToolbar/NoteToolbar.tsx @@ -11,6 +11,7 @@ interface NoteToolbarProps { themeId: number; style: any; toolbarButtonInfos: ToolbarButtonInfo[]; + disabled: boolean; } function styles_(props: NoteToolbarProps) { @@ -27,7 +28,7 @@ function styles_(props: NoteToolbarProps) { function NoteToolbar(props: NoteToolbarProps) { const styles = styles_(props); - return ; + return ; } const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance()); diff --git a/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx b/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx index d051463c86..1020d1041b 100644 --- a/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx +++ b/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx @@ -2,7 +2,7 @@ import Dialog from '../Dialog'; import DialogButtonRow, { ClickEvent, ButtonSpec } from '../DialogButtonRow'; import DialogTitle from '../DialogTitle'; import { _ } from '@joplin/lib/locale'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { FolderEntity } from '@joplin/lib/services/database/types'; import Folder from '@joplin/lib/models/Folder'; import ShareService, { ApiShare } from '@joplin/lib/services/share/ShareService'; @@ -12,11 +12,12 @@ import StyledInput from '../style/StyledInput'; import Button, { ButtonSize } from '../Button/Button'; import Logger from '@joplin/lib/Logger'; import StyledMessage from '../style/StyledMessage'; -import { ShareUserStatus, StateShare, StateShareUser } from '@joplin/lib/services/share/reducer'; +import { SharePermissions, ShareUserStatus, StateShare, StateShareUser } from '@joplin/lib/services/share/reducer'; import { State } from '@joplin/lib/reducer'; import { connect } from 'react-redux'; import { reg } from '@joplin/lib/registry'; import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect'; +import { ChangeEvent, Dropdown, DropdownOptions, DropdownVariant } from '../Dropdown/Dropdown'; const logger = Logger.create('ShareFolderDialog'); @@ -108,13 +109,20 @@ enum ShareState { } function ShareFolderDialog(props: Props) { + const permissionOptions: DropdownOptions = { + 'can_read': _('Can view'), + 'can_read_and_write': _('Can edit'), + }; + const [folder, setFolder] = useState(null); const [recipientEmail, setRecipientEmail] = useState(''); + const [recipientPermissions, setRecipientPermissions] = useState('can_read'); const [latestError, setLatestError] = useState(null); const [share, setShare] = useState(null); const [shareUsers, setShareUsers] = useState([]); const [shareState, setShareState] = useState(ShareState.Idle); const [customButtons, setCustomButtons] = useState([]); + const [recipientsBeingUpdated, setRecipientsBeingUpdated] = useState>({}); async function synchronize(event: AsyncEffectEvent = null) { setShareState(ShareState.Synchronizing); @@ -166,7 +174,14 @@ function ShareFolderDialog(props: Props) { void ShareService.instance().refreshShares(); }, [props.folderId]); - async function shareRecipient_click() { + const permissionsFromString = (p: string): SharePermissions => { + return { + can_read: 1, + can_write: p === 'can_read_and_write' ? 1 : 0, + }; + }; + + const shareRecipient_click = useCallback(async () => { setShareState(ShareState.Creating); setLatestError(null); @@ -192,7 +207,7 @@ function ShareFolderDialog(props: Props) { } try { - await ShareService.instance().addShareRecipient(share.id, share.master_key_id, recipientEmail); + await ShareService.instance().addShareRecipient(share.id, share.master_key_id, recipientEmail, permissionsFromString(recipientPermissions)); } catch (error) { // Handle the error but continue the process because we need to at // least refresh the shares since one has been created above. @@ -212,7 +227,7 @@ function ShareFolderDialog(props: Props) { } finally { defer(null); } - } + }, [recipientPermissions, props.folderId, recipientEmail]); function recipientEmail_change(event: any) { setRecipientEmail(event.target.value); @@ -239,19 +254,41 @@ function ShareFolderDialog(props: Props) { ); } + const recipientPermissions_change = useCallback((event: ChangeEvent) => { + setRecipientPermissions(event.value); + }, []); + function renderAddRecipient() { const disabled = shareState !== ShareState.Idle; + return ( {_('Add recipient:')} + ); } + const recipient_permissionChange = useCallback(async (shareUserId: string, value: string) => { + try { + setRecipientsBeingUpdated(prev => { + return { ...prev, [shareUserId]: true }; + }); + await ShareService.instance().setPermissions(share.id, shareUserId, permissionsFromString(value)); + } catch (error) { + alert(`Could not set permissions: ${error.message}`); + logger.error(error); + } finally { + setRecipientsBeingUpdated(prev => { + return { ...prev, [shareUserId]: false }; + }); + } + }, [share]); + function renderRecipient(index: number, shareUser: StateShareUser) { const statusToIcon = { [ShareUserStatus.Waiting]: 'fas fa-question', @@ -265,11 +302,15 @@ function ShareFolderDialog(props: Props) { [ShareUserStatus.Accepted]: _('Recipient has accepted the invitation'), }; + const permission = shareUser.can_write ? 'can_read_and_write' : 'can_read'; + const enabled = !recipientsBeingUpdated[shareUser.id]; + return ( {shareUser.user.email} + recipient_permissionChange(shareUser.id, event.value)}/> -