From 9e5a02094ca93ca9b09dfffdf7d72a976630aa81 Mon Sep 17 00:00:00 2001 From: mm21 <8033134+mm21@users.noreply.github.com> Date: Wed, 31 May 2023 17:11:42 +0000 Subject: [PATCH 1/8] Add ETAPI tests to repro inherited attribute issue and workaround --- .../get-inherited-attribute-workaround.http | 67 +++++++++++++++++++ test-etapi/get-inherited-attribute.http | 44 ++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 test-etapi/get-inherited-attribute-workaround.http create mode 100644 test-etapi/get-inherited-attribute.http diff --git a/test-etapi/get-inherited-attribute-workaround.http b/test-etapi/get-inherited-attribute-workaround.http new file mode 100644 index 000000000..527f11e69 --- /dev/null +++ b/test-etapi/get-inherited-attribute-workaround.http @@ -0,0 +1,67 @@ +POST {{triliumHost}}/etapi/attributes +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "root", + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true +} + +> {% client.global.set("createdInheritedAttributeId", response.body.attributeId); %} + +### + +POST {{triliumHost}}/etapi/create-note +Authorization: {{authToken}} +Content-Type: application/json + +{ + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": "Hi there!" +} + +> {% +client.global.set("createdNoteId", response.body.note.noteId); +client.global.set("createdBranchId", response.body.branch.branchId); +%} + +### + +POST {{triliumHost}}/etapi/attributes +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "{{createdNoteId}}", + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": false +} + +> {% client.global.set("createdOwnedAttributeId", response.body.attributeId); %} + +### + +DELETE {{triliumHost}}/etapi/attributes/{{createdOwnedAttributeId}} +Authorization: {{authToken}} + +> {% client.assert(response.status === 204); %} + +### + +GET {{triliumHost}}/etapi/notes/{{createdNoteId}} +Authorization: {{authToken}} + +> {% +client.assert(response.status === 200); +client.assert(response.body.noteId == client.global.get("createdNoteId")); +client.assert(response.body.attributes.length == 1); +client.assert(response.body.attributes[0].attributeId == + client.global.get("createdInheritedAttributeId")); +%} diff --git a/test-etapi/get-inherited-attribute.http b/test-etapi/get-inherited-attribute.http new file mode 100644 index 000000000..d614f419e --- /dev/null +++ b/test-etapi/get-inherited-attribute.http @@ -0,0 +1,44 @@ +POST {{triliumHost}}/etapi/attributes +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "root", + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true +} + +> {% client.global.set("createdAttributeId", response.body.attributeId); %} + +### + +POST {{triliumHost}}/etapi/create-note +Authorization: {{authToken}} +Content-Type: application/json + +{ + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": "Hi there!" +} + +> {% +client.global.set("createdNoteId", response.body.note.noteId); +client.global.set("createdBranchId", response.body.branch.branchId); +%} + +### + +GET {{triliumHost}}/etapi/notes/{{createdNoteId}} +Authorization: {{authToken}} + +> {% +client.assert(response.status === 200); +client.assert(response.body.noteId == client.global.get("createdNoteId")); +client.assert(response.body.attributes.length == 1); +client.assert(response.body.attributes[0].attributeId == + client.global.get("createdAttributeId")); +%} From b5cfc28912404b026825f76eb96b3dce04f6c905 Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 31 May 2023 23:21:06 +0200 Subject: [PATCH 2/8] rename inheritableAttributeCache to __inheritableAttributeCache for consistency --- src/becca/entities/bnote.js | 14 ++-- src/etapi/mappers.js | 5 +- src/share/shaca/entities/snote.js | 12 ++-- .../get-inherited-attribute-workaround.http | 67 ------------------- 4 files changed, 17 insertions(+), 81 deletions(-) delete mode 100644 test-etapi/get-inherited-attribute-workaround.http diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 0be9a3ab4..ec24fe08e 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -108,7 +108,7 @@ class BNote extends AbstractBeccaEntity { this.__attributeCache = null; /** @type {BAttribute[]|null} * @private */ - this.inheritableAttributeCache = null; + this.__inheritableAttributeCache = null; /** @type {BAttribute[]} * @private */ @@ -454,11 +454,11 @@ class BNote extends AbstractBeccaEntity { } } - this.inheritableAttributeCache = []; + this.__inheritableAttributeCache = []; for (const attr of this.__attributeCache) { if (attr.isInheritable) { - this.inheritableAttributeCache.push(attr); + this.__inheritableAttributeCache.push(attr); } } } @@ -475,11 +475,11 @@ class BNote extends AbstractBeccaEntity { return []; } - if (!this.inheritableAttributeCache) { - this.__getAttributes(path); // will refresh also this.inheritableAttributeCache + if (!this.__inheritableAttributeCache) { + this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache } - return this.inheritableAttributeCache; + return this.__inheritableAttributeCache; } __validateTypeName(type, name) { @@ -845,7 +845,7 @@ class BNote extends AbstractBeccaEntity { this.flatTextCache = null; this.__attributeCache = null; - this.inheritableAttributeCache = null; + this.__inheritableAttributeCache = null; this.ancestorCache = null; } diff --git a/src/etapi/mappers.js b/src/etapi/mappers.js index ad959f36a..86fea9c3d 100644 --- a/src/etapi/mappers.js +++ b/src/etapi/mappers.js @@ -1,3 +1,4 @@ +/** @param {BNote} note */ function mapNoteToPojo(note) { return { noteId: note.noteId, @@ -17,6 +18,7 @@ function mapNoteToPojo(note) { }; } +/** @param {BBranch} branch */ function mapBranchToPojo(branch) { return { branchId: branch.branchId, @@ -29,6 +31,7 @@ function mapBranchToPojo(branch) { }; } +/** @param {BAttribute} attr */ function mapAttributeToPojo(attr) { return { attributeId: attr.attributeId, @@ -46,4 +49,4 @@ module.exports = { mapNoteToPojo, mapBranchToPojo, mapAttributeToPojo -}; \ No newline at end of file +}; diff --git a/src/share/shaca/entities/snote.js b/src/share/shaca/entities/snote.js index e083ca97b..9c8dbbfcc 100644 --- a/src/share/shaca/entities/snote.js +++ b/src/share/shaca/entities/snote.js @@ -40,7 +40,7 @@ class SNote extends AbstractShacaEntity { /** @param {SAttribute[]|null} */ this.__attributeCache = null; /** @param {SAttribute[]|null} */ - this.inheritableAttributeCache = null; + this.__inheritableAttributeCache = null; /** @param {SAttribute[]} */ this.targetRelations = []; @@ -190,11 +190,11 @@ class SNote extends AbstractShacaEntity { } } - this.inheritableAttributeCache = []; + this.__inheritableAttributeCache = []; for (const attr of this.__attributeCache) { if (attr.isInheritable) { - this.inheritableAttributeCache.push(attr); + this.__inheritableAttributeCache.push(attr); } } } @@ -208,11 +208,11 @@ class SNote extends AbstractShacaEntity { return []; } - if (!this.inheritableAttributeCache) { - this.__getAttributes(path); // will refresh also this.inheritableAttributeCache + if (!this.__inheritableAttributeCache) { + this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache } - return this.inheritableAttributeCache; + return this.__inheritableAttributeCache; } /** @returns {boolean} */ diff --git a/test-etapi/get-inherited-attribute-workaround.http b/test-etapi/get-inherited-attribute-workaround.http deleted file mode 100644 index 527f11e69..000000000 --- a/test-etapi/get-inherited-attribute-workaround.http +++ /dev/null @@ -1,67 +0,0 @@ -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "root", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdInheritedAttributeId", response.body.attributeId); %} - -### - -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% -client.global.set("createdNoteId", response.body.note.noteId); -client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": false -} - -> {% client.global.set("createdOwnedAttributeId", response.body.attributeId); %} - -### - -DELETE {{triliumHost}}/etapi/attributes/{{createdOwnedAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.noteId == client.global.get("createdNoteId")); -client.assert(response.body.attributes.length == 1); -client.assert(response.body.attributes[0].attributeId == - client.global.get("createdInheritedAttributeId")); -%} From ae42e0efc77e468afd6b1b0d84619574f68fb5e6 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 1 Jun 2023 00:07:57 +0200 Subject: [PATCH 3/8] refactoring --- src/becca/becca_loader.js | 33 +++++++----- src/becca/entities/bnote.js | 56 +++++++-------------- src/public/app/widgets/title_bar_buttons.js | 10 ++-- src/services/notes.js | 34 ++++--------- 4 files changed, 56 insertions(+), 77 deletions(-) diff --git a/src/becca/becca_loader.js b/src/becca/becca_loader.js index f7659e964..82dd09e42 100644 --- a/src/becca/becca_loader.js +++ b/src/becca/becca_loader.js @@ -69,15 +69,22 @@ function reload() { require('../services/ws').reloadFrontend(); } -function postProcessEntityUpdate(entityName, entity) { +/** + * This gets run on entity being created or updated. + * + * @param entityName + * @param entityRow - can be a becca entity (change comes from this trilium instance) or just a row (from sync). + * Should be therefore treated as a row. + */ +function postProcessEntityUpdate(entityName, entityRow) { if (entityName === 'notes') { - noteUpdated(entity); + noteUpdated(entityRow); } else if (entityName === 'branches') { - branchUpdated(entity); + branchUpdated(entityRow); } else if (entityName === 'attributes') { - attributeUpdated(entity); + attributeUpdated(entityRow); } else if (entityName === 'note_reordering') { - noteReorderingUpdated(entity); + noteReorderingUpdated(entityRow); } } @@ -163,8 +170,8 @@ function branchDeleted(branchId) { delete becca.branches[branch.branchId]; } -function noteUpdated(entity) { - const note = becca.notes[entity.noteId]; +function noteUpdated(entityRow) { + const note = becca.notes[entityRow.noteId]; if (note) { // type / mime could have been changed, and they are present in flatTextCache @@ -172,15 +179,15 @@ function noteUpdated(entity) { } } -function branchUpdated(branch) { - const childNote = becca.notes[branch.noteId]; +function branchUpdated(branchRow) { + const childNote = becca.notes[branchRow.noteId]; if (childNote) { childNote.flatTextCache = null; childNote.sortParents(); } - const parentNote = becca.notes[branch.parentNoteId]; + const parentNote = becca.notes[branchRow.parentNoteId]; if (parentNote) { parentNote.sortChildren(); @@ -222,8 +229,10 @@ function attributeDeleted(attributeId) { } } -function attributeUpdated(attribute) { - const note = becca.notes[attribute.noteId]; +/** @param {BAttribute} attributeRow */ +function attributeUpdated(attributeRow) { + const attribute = becca.attributes[attributeRow.attributeId]; + const note = becca.notes[attributeRow.noteId]; if (note) { if (attribute.isAffectingSubtree || note.isInherited()) { diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index ec24fe08e..657de1e66 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -84,7 +84,7 @@ class BNote extends AbstractBeccaEntity { this.decrypt(); /** @type {string|null} */ - this.flatTextCache = null; + this.__flatTextCache = null; return this; } @@ -118,7 +118,7 @@ class BNote extends AbstractBeccaEntity { /** @type {BNote[]|null} * @private */ - this.ancestorCache = null; + this.__ancestorCache = null; // following attributes are filled during searching from database @@ -813,40 +813,40 @@ class BNote extends AbstractBeccaEntity { * @returns {string} - returns flattened textual representation of note, prefixes and attributes */ getFlatText() { - if (!this.flatTextCache) { - this.flatTextCache = `${this.noteId} ${this.type} ${this.mime} `; + if (!this.__flatTextCache) { + this.__flatTextCache = `${this.noteId} ${this.type} ${this.mime} `; for (const branch of this.parentBranches) { if (branch.prefix) { - this.flatTextCache += `${branch.prefix} `; + this.__flatTextCache += `${branch.prefix} `; } } - this.flatTextCache += `${this.title} `; + this.__flatTextCache += `${this.title} `; for (const attr of this.getAttributes()) { // it's best to use space as separator since spaces are filtered from the search string by the tokenization into words - this.flatTextCache += `${attr.type === 'label' ? '#' : '~'}${attr.name}`; + this.__flatTextCache += `${attr.type === 'label' ? '#' : '~'}${attr.name}`; if (attr.value) { - this.flatTextCache += `=${attr.value}`; + this.__flatTextCache += `=${attr.value}`; } - this.flatTextCache += ' '; + this.__flatTextCache += ' '; } - this.flatTextCache = utils.normalize(this.flatTextCache); + this.__flatTextCache = utils.normalize(this.__flatTextCache); } - return this.flatTextCache; + return this.__flatTextCache; } invalidateThisCache() { - this.flatTextCache = null; + this.__flatTextCache = null; this.__attributeCache = null; this.__inheritableAttributeCache = null; - this.ancestorCache = null; + this.__ancestorCache = null; } invalidateSubTree(path = []) { @@ -875,24 +875,6 @@ class BNote extends AbstractBeccaEntity { } } - invalidateSubtreeFlatText() { - this.flatTextCache = null; - - for (const childNote of this.children) { - childNote.invalidateSubtreeFlatText(); - } - - for (const targetRelation of this.targetRelations) { - if (targetRelation.name === 'template' || targetRelation.name === 'inherit') { - const note = targetRelation.note; - - if (note) { - note.invalidateSubtreeFlatText(); - } - } - } - } - getRelationDefinitions() { return this.getLabels() .filter(l => l.name.startsWith("relation:")); @@ -1083,28 +1065,28 @@ class BNote extends AbstractBeccaEntity { /** @returns {BNote[]} */ getAncestors() { - if (!this.ancestorCache) { + if (!this.__ancestorCache) { const noteIds = new Set(); - this.ancestorCache = []; + this.__ancestorCache = []; for (const parent of this.parents) { if (noteIds.has(parent.noteId)) { continue; } - this.ancestorCache.push(parent); + this.__ancestorCache.push(parent); noteIds.add(parent.noteId); for (const ancestorNote of parent.getAncestors()) { if (!noteIds.has(ancestorNote.noteId)) { - this.ancestorCache.push(ancestorNote); + this.__ancestorCache.push(ancestorNote); noteIds.add(ancestorNote.noteId); } } } } - return this.ancestorCache; + return this.__ancestorCache; } /** @returns {boolean} */ @@ -1491,7 +1473,7 @@ class BNote extends AbstractBeccaEntity { if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) { try { this.title = protectedSessionService.decryptString(this.title); - this.flatTextCache = null; + this.__flatTextCache = null; this.isDecrypted = true; } diff --git a/src/public/app/widgets/title_bar_buttons.js b/src/public/app/widgets/title_bar_buttons.js index 6a4f73ad5..89dd43601 100644 --- a/src/public/app/widgets/title_bar_buttons.js +++ b/src/public/app/widgets/title_bar_buttons.js @@ -56,13 +56,15 @@ export default class TitleBarButtonsWidget extends BasicWidget { const $maximizeBtn = this.$widget.find(".maximize-btn"); const $closeBtn = this.$widget.find(".close-btn"); - //When the window is restarted, the window will not be reset when it is set to the top, so get the window status and set the icon background - (function () { + // When the window is restarted, the window will not be reset when it is set to the top, + // so get the window status and set the icon background + setTimeout(() => { const remote = utils.dynamicRequire('@electron/remote'); - if (remote.BrowserWindow.getFocusedWindow().isAlwaysOnTop()) { + if (remote.BrowserWindow.getFocusedWindow()?.isAlwaysOnTop()) { $topBtn.addClass('active'); } - }()); + }, 1000); + $topBtn.on('click', () => { $topBtn.trigger('blur'); const remote = utils.dynamicRequire('@electron/remote'); diff --git a/src/services/notes.js b/src/services/notes.js index f3d219145..c9343bc4e 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -54,11 +54,10 @@ function deriveMime(type, mime) { } function copyChildAttributes(parentNote, childNote) { - const hasAlreadyTemplate = childNote.hasRelation('template'); - for (const attr of parentNote.getAttributes()) { if (attr.name.startsWith("child:")) { const name = attr.name.substr(6); + const hasAlreadyTemplate = childNote.hasRelation('template'); if (hasAlreadyTemplate && attr.type === 'relation' && name === 'template') { // if the note already has a template, it means the template was chosen by the user explicitly @@ -174,7 +173,7 @@ function createNewNote(params) { // TODO: think about what can happen if the note already exists with the forced ID // I guess on DB it's going to be fine, but becca references between entities - // might get messed up (two Note instance for the same ID existing in the references) + // might get messed up (two note instances for the same ID existing in the references) note = new BNote({ noteId: params.noteId, // optionally can force specific noteId title: params.title, @@ -195,7 +194,7 @@ function createNewNote(params) { } finally { if (!isEntityEventsDisabled) { - // re-enable entity events only if there were previously enabled + // re-enable entity events only if they were previously enabled // (they can be disabled in case of import) cls.enableEntityEvents(); } @@ -215,27 +214,14 @@ function createNewNote(params) { copyChildAttributes(parentNote, note); + eventService.emit(eventService.ENTITY_CREATED, { entityName: 'notes', entity: note }); + eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'notes', entity: note }); triggerNoteTitleChanged(note); - - eventService.emit(eventService.ENTITY_CREATED, { - entityName: 'notes', - entity: note - }); - - eventService.emit(eventService.ENTITY_CREATED, { - entityName: 'note_contents', - entity: note - }); - - eventService.emit(eventService.ENTITY_CREATED, { - entityName: 'branches', - entity: branch - }); - - eventService.emit(eventService.CHILD_NOTE_CREATED, { - childNote: note, - parentNote: parentNote - }); + // note_contents doesn't use "created" event + eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'note_contents', entity: note }); + eventService.emit(eventService.ENTITY_CREATED, { entityName: 'branches', entity: branch }); + eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'branches', entity: branch }); + eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote: parentNote }); log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`); From 758dba9ba547e1d178851677156c9b39ab472417 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 1 Jun 2023 00:13:08 +0200 Subject: [PATCH 4/8] fix event --- src/becca/entities/bnote.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 657de1e66..09dd7e679 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -12,6 +12,7 @@ const TaskContext = require("../../services/task_context"); const dayjs = require("dayjs"); const utc = require('dayjs/plugin/utc'); const eventService = require("../../services/events"); +const cls = require("../../services/cls.js"); dayjs.extend(utc); const LABEL = 'label'; @@ -316,10 +317,12 @@ class BNote extends AbstractBeccaEntity { isSynced: true }); - eventService.emit(eventService.ENTITY_CHANGED, { - entityName: 'note_contents', - entity: this - }); + if (!cls.isEntityEventsDisabled()) { + eventService.emit(eventService.ENTITY_CHANGED, { + entityName: 'note_contents', + entity: this + }); + } } setJsonContent(content) { From 86a080e7ecbd332f95688de971921b903bda87a0 Mon Sep 17 00:00:00 2001 From: mm21 <8033134+mm21@users.noreply.github.com> Date: Thu, 1 Jun 2023 17:20:11 +0000 Subject: [PATCH 5/8] Add ETAPI test to repro inherited attribute issue for cloned note #3994 --- ...inherited-attribute-cloned-workaround.http | 142 ++++++++++++++++++ .../get-inherited-attribute-cloned.http | 116 ++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 test-etapi/get-inherited-attribute-cloned-workaround.http create mode 100644 test-etapi/get-inherited-attribute-cloned.http diff --git a/test-etapi/get-inherited-attribute-cloned-workaround.http b/test-etapi/get-inherited-attribute-cloned-workaround.http new file mode 100644 index 000000000..3bd88402c --- /dev/null +++ b/test-etapi/get-inherited-attribute-cloned-workaround.http @@ -0,0 +1,142 @@ +POST {{triliumHost}}/etapi/create-note +Authorization: {{authToken}} +Content-Type: application/json + +{ + "parentNoteId": "root", + "title": "Hello parent", + "type": "text", + "content": "Hi there!" +} + +> {% +client.assert(response.status === 201); +client.global.set("parentNoteId", response.body.note.noteId); +client.global.set("parentBranchId", response.body.branch.branchId); +%} + +### Create inheritable parent attribute + +POST {{triliumHost}}/etapi/attributes +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "{{parentNoteId}}", + "type": "label", + "name": "mylabel", + "value": "", + "isInheritable": true, + "position": 10 +} + +> {% +client.assert(response.status === 201); +client.global.set("parentAttributeId", response.body.attributeId); +%} + +### Create child note under root + +POST {{triliumHost}}/etapi/create-note +Authorization: {{authToken}} +Content-Type: application/json + +{ + "parentNoteId": "root", + "title": "Hello child", + "type": "text", + "content": "Hi there!" +} + +> {% +client.assert(response.status === 201); +client.global.set("childNoteId", response.body.note.noteId); +client.global.set("childBranchId", response.body.branch.branchId); +%} + +### Create child attribute + +POST {{triliumHost}}/etapi/attributes +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "{{childNoteId}}", + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": false, + "position": 10 +} + +> {% +client.assert(response.status === 201); +client.global.set("childAttributeId", response.body.attributeId); +%} + +### Clone child to parent + +POST {{triliumHost}}/etapi/branches +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "{{childNoteId}}", + "parentNoteId": "{{parentNoteId}}" +} + +> {% +client.assert(response.status === 201); +client.assert(response.body.parentNoteId == client.global.get("parentNoteId")); +%} + +### Workaround: create dummy attribute + +POST {{triliumHost}}/etapi/attributes +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "{{childNoteId}}", + "type": "label", + "name": "childlabel", + "value": "val", + "isInheritable": false +} + +> {% +client.assert(response.status === 201); +client.global.set("dummyAttributeId", response.body.attributeId); +%} + +### Workaround: delete dummy attribute + +DELETE {{triliumHost}}/etapi/attributes/{{dummyAttributeId}} +Authorization: {{authToken}} + +> {% client.assert(response.status === 204); %} + +### + +GET {{triliumHost}}/etapi/notes/{{childNoteId}} +Authorization: {{authToken}} + +> {% + +function hasAttribute(list, attributeId) { + for (let i = 0; i < list.length; i++) { + if (list[i]["attributeId"] === attributeId) { + return true; + } + } + return false; +} + +client.assert(response.status === 200); +client.assert(response.body.noteId == client.global.get("childNoteId")); +client.assert(response.body.attributes.length == 2); +client.assert(hasAttribute(response.body.attributes, + client.global.get("parentAttributeId"))); +client.assert(hasAttribute(response.body.attributes, + client.global.get("childAttributeId"))); +%} diff --git a/test-etapi/get-inherited-attribute-cloned.http b/test-etapi/get-inherited-attribute-cloned.http new file mode 100644 index 000000000..06c1aa976 --- /dev/null +++ b/test-etapi/get-inherited-attribute-cloned.http @@ -0,0 +1,116 @@ +POST {{triliumHost}}/etapi/create-note +Authorization: {{authToken}} +Content-Type: application/json + +{ + "parentNoteId": "root", + "title": "Hello parent", + "type": "text", + "content": "Hi there!" +} + +> {% +client.assert(response.status === 201); +client.global.set("parentNoteId", response.body.note.noteId); +client.global.set("parentBranchId", response.body.branch.branchId); +%} + +### Create inheritable parent attribute + +POST {{triliumHost}}/etapi/attributes +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "{{parentNoteId}}", + "type": "label", + "name": "mylabel", + "value": "", + "isInheritable": true, + "position": 10 +} + +> {% +client.assert(response.status === 201); +client.global.set("parentAttributeId", response.body.attributeId); +%} + +### Create child note under root + +POST {{triliumHost}}/etapi/create-note +Authorization: {{authToken}} +Content-Type: application/json + +{ + "parentNoteId": "root", + "title": "Hello child", + "type": "text", + "content": "Hi there!" +} + +> {% +client.assert(response.status === 201); +client.global.set("childNoteId", response.body.note.noteId); +client.global.set("childBranchId", response.body.branch.branchId); +%} + +### Create child attribute + +POST {{triliumHost}}/etapi/attributes +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "{{childNoteId}}", + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": false, + "position": 10 +} + +> {% +client.assert(response.status === 201); +client.global.set("childAttributeId", response.body.attributeId); +%} + +### Clone child to parent + +POST {{triliumHost}}/etapi/branches +Authorization: {{authToken}} +Content-Type: application/json + +{ + "noteId": "{{childNoteId}}", + "parentNoteId": "{{parentNoteId}}" +} + +> {% +client.assert(response.status === 201); +client.assert(response.body.parentNoteId == client.global.get("parentNoteId")); +%} + +### + +GET {{triliumHost}}/etapi/notes/{{childNoteId}} +Authorization: {{authToken}} + +> {% + +function hasAttribute(list, attributeId) { + for (let i = 0; i < list.length; i++) { + if (list[i]["attributeId"] === attributeId) { + return true; + } + } + return false; +} + +client.assert(response.status === 200); +client.assert(response.body.noteId == client.global.get("childNoteId")); +client.assert(response.body.attributes.length == 2); +client.assert(hasAttribute(response.body.attributes, + client.global.get("parentAttributeId"))); +client.assert(hasAttribute(response.body.attributes, + client.global.get("childAttributeId"))); +%} From d9359c7c558dd5cec293d4fe4a4294cc5ebbaee7 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 1 Jun 2023 22:51:34 +0200 Subject: [PATCH 6/8] downgrade excalidraw to 0.14.2, #3979 --- package-lock.json | 15 ++++++++------- package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 682c4f5a0..14cc4892b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,13 +5,14 @@ "requires": true, "packages": { "": { + "name": "trilium", "version": "0.60.1-beta", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { "@braintree/sanitize-url": "6.0.2", "@electron/remote": "2.0.9", - "@excalidraw/excalidraw": "0.15.2", + "@excalidraw/excalidraw": "0.14.2", "archiver": "5.3.1", "async-mutex": "0.4.0", "axios": "1.4.0", @@ -461,9 +462,9 @@ } }, "node_modules/@excalidraw/excalidraw": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz", - "integrity": "sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz", + "integrity": "sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg==", "peerDependencies": { "react": "^17.0.2 || ^18.2.0", "react-dom": "^17.0.2 || ^18.2.0" @@ -13590,9 +13591,9 @@ "dev": true }, "@excalidraw/excalidraw": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz", - "integrity": "sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz", + "integrity": "sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg==", "requires": {} }, "@gar/promisify": { diff --git a/package.json b/package.json index a0a42ea5f..3353f12b9 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "@braintree/sanitize-url": "6.0.2", "@electron/remote": "2.0.9", - "@excalidraw/excalidraw": "0.15.2", + "@excalidraw/excalidraw": "0.14.2", "archiver": "5.3.1", "async-mutex": "0.4.0", "axios": "1.4.0", From 0c4492bcd033505c03dcb332d639d978262096e3 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 1 Jun 2023 23:14:17 +0200 Subject: [PATCH 7/8] invalidate attribute cache on branch create/update, fixes #3994 --- src/becca/becca_loader.js | 43 ++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/becca/becca_loader.js b/src/becca/becca_loader.js index 82dd09e42..dd8e990a4 100644 --- a/src/becca/becca_loader.js +++ b/src/becca/becca_loader.js @@ -69,25 +69,6 @@ function reload() { require('../services/ws').reloadFrontend(); } -/** - * This gets run on entity being created or updated. - * - * @param entityName - * @param entityRow - can be a becca entity (change comes from this trilium instance) or just a row (from sync). - * Should be therefore treated as a row. - */ -function postProcessEntityUpdate(entityName, entityRow) { - if (entityName === 'notes') { - noteUpdated(entityRow); - } else if (entityName === 'branches') { - branchUpdated(entityRow); - } else if (entityName === 'attributes') { - attributeUpdated(entityRow); - } else if (entityName === 'note_reordering') { - noteReorderingUpdated(entityRow); - } -} - eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entityName, entityRow}) => { if (!becca.loaded) { return; @@ -119,6 +100,25 @@ eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED, ({entityName, en postProcessEntityUpdate(entityName, entity); }); +/** + * This gets run on entity being created or updated. + * + * @param entityName + * @param entityRow - can be a becca entity (change comes from this trilium instance) or just a row (from sync). + * Should be therefore treated as a row. + */ +function postProcessEntityUpdate(entityName, entityRow) { + if (entityName === 'notes') { + noteUpdated(entityRow); + } else if (entityName === 'branches') { + branchUpdated(entityRow); + } else if (entityName === 'attributes') { + attributeUpdated(entityRow); + } else if (entityName === 'note_reordering') { + noteReorderingUpdated(entityRow); + } +} + eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED], ({entityName, entityId}) => { if (!becca.loaded) { return; @@ -156,6 +156,7 @@ function branchDeleted(branchId) { .filter(parentBranch => parentBranch.branchId !== branch.branchId); if (childNote.parents.length > 0) { + // subtree notes might lose some inherited attributes childNote.invalidateSubTree(); } } @@ -185,6 +186,10 @@ function branchUpdated(branchRow) { if (childNote) { childNote.flatTextCache = null; childNote.sortParents(); + + // notes in the subtree can get new inherited attributes + // this is in theory needed upon branch creation, but there's no create event for sync changes + childNote.invalidateSubTree(); } const parentNote = becca.notes[branchRow.parentNoteId]; From e47c3a27b0eb1966f8a791901948485301df1e57 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 1 Jun 2023 23:18:50 +0200 Subject: [PATCH 8/8] remove the workaround test --- ...inherited-attribute-cloned-workaround.http | 142 ------------------ 1 file changed, 142 deletions(-) delete mode 100644 test-etapi/get-inherited-attribute-cloned-workaround.http diff --git a/test-etapi/get-inherited-attribute-cloned-workaround.http b/test-etapi/get-inherited-attribute-cloned-workaround.http deleted file mode 100644 index 3bd88402c..000000000 --- a/test-etapi/get-inherited-attribute-cloned-workaround.http +++ /dev/null @@ -1,142 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello parent", - "type": "text", - "content": "Hi there!" -} - -> {% -client.assert(response.status === 201); -client.global.set("parentNoteId", response.body.note.noteId); -client.global.set("parentBranchId", response.body.branch.branchId); -%} - -### Create inheritable parent attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{parentNoteId}}", - "type": "label", - "name": "mylabel", - "value": "", - "isInheritable": true, - "position": 10 -} - -> {% -client.assert(response.status === 201); -client.global.set("parentAttributeId", response.body.attributeId); -%} - -### Create child note under root - -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello child", - "type": "text", - "content": "Hi there!" -} - -> {% -client.assert(response.status === 201); -client.global.set("childNoteId", response.body.note.noteId); -client.global.set("childBranchId", response.body.branch.branchId); -%} - -### Create child attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": false, - "position": 10 -} - -> {% -client.assert(response.status === 201); -client.global.set("childAttributeId", response.body.attributeId); -%} - -### Clone child to parent - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "parentNoteId": "{{parentNoteId}}" -} - -> {% -client.assert(response.status === 201); -client.assert(response.body.parentNoteId == client.global.get("parentNoteId")); -%} - -### Workaround: create dummy attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "type": "label", - "name": "childlabel", - "value": "val", - "isInheritable": false -} - -> {% -client.assert(response.status === 201); -client.global.set("dummyAttributeId", response.body.attributeId); -%} - -### Workaround: delete dummy attribute - -DELETE {{triliumHost}}/etapi/attributes/{{dummyAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/notes/{{childNoteId}} -Authorization: {{authToken}} - -> {% - -function hasAttribute(list, attributeId) { - for (let i = 0; i < list.length; i++) { - if (list[i]["attributeId"] === attributeId) { - return true; - } - } - return false; -} - -client.assert(response.status === 200); -client.assert(response.body.noteId == client.global.get("childNoteId")); -client.assert(response.body.attributes.length == 2); -client.assert(hasAttribute(response.body.attributes, - client.global.get("parentAttributeId"))); -client.assert(hasAttribute(response.body.attributes, - client.global.get("childAttributeId"))); -%}