{"record":{"id":"0852a47501d01e14","repo":"laurent22/joplin","slug":"cannot-change-the-note-lock-of-a-read-only-note","errorCode":null,"errorMessage":"Cannot change the note lock of a read-only note","messagePattern":"Cannot change the note lock of a read-only note","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/lib/services/noteLock/setNoteLockState.ts","lineNumber":19,"sourceCode":"import { ModelType } from '../../BaseModel';\nimport BaseItem from '../../models/BaseItem';\nimport ItemChange from '../../models/ItemChange';\nimport Note from '../../models/Note';\nimport Setting from '../../models/Setting';\nimport { itemIsReadOnlySync, ItemSlice } from '../../models/utils/readOnly';\nimport { NoteEntity } from '../database/types';\nimport eventManager, { EventName } from '../../eventManager';\nimport isNoteLockEnabled from './isNoteLockEnabled';\nimport NoteLockSession from './NoteLockSession';\n\n// The UI hides the enable/disable actions for these cases, but the commands can also be\n// invoked directly (keyboard, plugins), so the transitions fail closed here too.\nconst checkCanChangeLockState = (note: NoteEntity, noteId: string) => {\n\tif (!isNoteLockEnabled()) throw new Error('Note lock is not enabled');\n\tif (!note) throw new Error(`No such note: ${noteId}`);\n\tif (note.deleted_time) throw new Error('Cannot change the note lock of a deleted note');\n\tif (note.is_conflict) throw new Error('Cannot change the note lock of a conflict note');\n\tif (itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note as ItemSlice, Setting.value('sync.userId'), BaseItem.syncShareCache)) throw new Error('Cannot change the note lock of a read-only note');\n\tif (!NoteLockSession.instance().isUnlocked()) throw new Error('Cannot change the note lock while the session is locked');\n};\n\nconst validationFields = ['id', 'is_locked', 'deleted_time', 'is_conflict', 'share_id'];\n\n// These only validate and emit: the note screen listens for the event and persists the\n// change with a scheduled gated save.\nexport const enableNoteLock = async (noteId: string) => {\n\tconst note = await Note.load(noteId, { fields: validationFields });\n\tcheckCanChangeLockState(note, noteId);\n\tif (note.is_locked) throw new Error(`Note is already locked: ${noteId}`);\n\teventManager.emit(EventName.NoteLockNoteStateChange, { noteId, isLocked: true });\n};\n\nexport const disableNoteLock = async (noteId: string) => {\n\tconst note = await Note.load(noteId, { fields: validationFields });\n\tcheckCanChangeLockState(note, noteId);\n\tif (!note.is_locked) throw new Error(`Note is not locked: ${noteId}`);","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/laurent22/joplin/blob/981a03c5c9e88130bccff4db47c411d35ec7ae2c/packages/lib/services/noteLock/setNoteLockState.ts#L1-L37","documentation":"Notes that are read-only (e.g. shared/E2EE items the current user cannot edit, enforced via itemIsReadOnlySync and the sync share cache) cannot have their lock state changed. This is a fail-closed guard for direct command invocations.","triggerScenarios":"Calling enableNoteLock/disableNoteLock on a note for which itemIsReadOnlySync returns true — typically a note owned by someone else in a shared notebook or otherwise non-editable by the current sync user.","commonSituations":"Users attempting to lock notes in read-only shared notebooks; plugins enumerating shared notes without checking edit permissions; sync user mismatch after switching accounts.","solutions":["Only attempt lock changes on notes you own or that are writable (check permissions in the sharing UI).","Filter read-only notes out before batch operations using the same itemIsReadOnlySync predicate.","Catch the error and inform the user the note is read-only.","Request edit access to the shared note."],"exampleFix":"// before\nawait joplin.commands.execute('enableNoteLock', noteId);\n// after\nconst note = await Note.load(noteId);\nif (note && !itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note, Setting.value('sync.userId'), BaseItem.syncShareCache)) await joplin.commands.execute('enableNoteLock', noteId);","handlingStrategy":"validation","validationCode":"const writable = !itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note, Setting.value('sync.userId'), BaseItem.syncShareCache);\nif (writable) await joplin.commands.execute('enableNoteLock', noteId);","typeGuard":"const isEditableNote = (n: NoteEntity): boolean => !itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, n as ItemSlice, Setting.value('sync.userId'), BaseItem.syncShareCache);","tryCatchPattern":"try { await enableNoteLock(id); } catch (e) { if (e.message.includes('read-only')) showToast('You do not have edit access'); else throw e; }","preventionTips":["Check share/notebook permissions before modifying shared notes","Use itemIsReadOnlySync to pre-filter candidate notes","Confirm the correct sync userId is configured"],"tags":["note-lock","read-only","permissions"],"backgroundTag":"permission-denied","analyzedSha":"981a03c5c9e88130bccff4db47c411d35ec7ae2c","analyzedAt":"2026-09-17T14:49:40.960Z","contentChangedAt":"2026-09-17T14:49:40.960Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}