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", diff --git a/src/becca/becca_loader.js b/src/becca/becca_loader.js index f7659e964..dd8e990a4 100644 --- a/src/becca/becca_loader.js +++ b/src/becca/becca_loader.js @@ -69,18 +69,6 @@ function reload() { require('../services/ws').reloadFrontend(); } -function postProcessEntityUpdate(entityName, entity) { - if (entityName === 'notes') { - noteUpdated(entity); - } else if (entityName === 'branches') { - branchUpdated(entity); - } else if (entityName === 'attributes') { - attributeUpdated(entity); - } else if (entityName === 'note_reordering') { - noteReorderingUpdated(entity); - } -} - eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entityName, entityRow}) => { if (!becca.loaded) { return; @@ -112,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; @@ -149,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(); } } @@ -163,8 +171,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 +180,19 @@ 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(); + + // 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[branch.parentNoteId]; + const parentNote = becca.notes[branchRow.parentNoteId]; if (parentNote) { parentNote.sortChildren(); @@ -222,8 +234,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 0be9a3ab4..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'; @@ -84,7 +85,7 @@ class BNote extends AbstractBeccaEntity { this.decrypt(); /** @type {string|null} */ - this.flatTextCache = null; + this.__flatTextCache = null; return this; } @@ -108,7 +109,7 @@ class BNote extends AbstractBeccaEntity { this.__attributeCache = null; /** @type {BAttribute[]|null} * @private */ - this.inheritableAttributeCache = null; + this.__inheritableAttributeCache = null; /** @type {BAttribute[]} * @private */ @@ -118,7 +119,7 @@ class BNote extends AbstractBeccaEntity { /** @type {BNote[]|null} * @private */ - this.ancestorCache = null; + this.__ancestorCache = null; // following attributes are filled during searching from database @@ -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) { @@ -454,11 +457,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 +478,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) { @@ -813,40 +816,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.__inheritableAttributeCache = null; + this.__ancestorCache = null; } invalidateSubTree(path = []) { @@ -875,24 +878,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 +1068,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 +1476,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/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/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}'`); 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-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"))); +%} 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")); +%}