From 1b68adf3e4d05e0b621aecf5f75963c945263d64 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 15 Jun 2023 21:34:02 +0200 Subject: [PATCH 01/11] compatibility with online excalidraw tool --- src/public/app/widgets/type_widgets/canvas.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index b4c92c4f0..ae4088d6c 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -285,6 +285,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget { }) const content = { + type: "excalidraw", + version: 2, _meta: "This note has type `canvas`. It uses excalidraw and stores an exported svg alongside.", elements, // excalidraw appState, // excalidraw From 74400dad9712a884adcb1bb6c17656db5e217d34 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 15 Jun 2023 21:51:41 +0200 Subject: [PATCH 02/11] fix race condition between script execution and saving, closes #4028 --- src/public/app/widgets/note_detail.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index aaf2c568d..2346c041e 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -230,6 +230,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { } } + async runActiveNoteCommand(params) { + if (this.isNoteContext(params.ntxId)) { + // make sure that script is saved before running it #4028 + await this.spacedUpdate.updateNowIfNecessary(); + } + + return await this.parent.triggerCommand('runActiveNote', params); + } + async printActiveNoteEvent() { if (!this.noteContext.isActive()) { return; From 3223e767875e5379c99ff58a562cb9c1a2641bdf Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 15 Jun 2023 23:21:40 +0200 Subject: [PATCH 03/11] etapi ZIP import --- src/etapi/etapi.openapi.yaml | 38 +++++++++++++++++++++++++++++------- src/etapi/notes.js | 15 ++++++++++++-- src/services/task_context.js | 2 +- test-etapi/import-zip.http | 12 ++++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 test-etapi/import-zip.http diff --git a/src/etapi/etapi.openapi.yaml b/src/etapi/etapi.openapi.yaml index 754fb05b3..c510fa0d2 100644 --- a/src/etapi/etapi.openapi.yaml +++ b/src/etapi/etapi.openapi.yaml @@ -33,13 +33,7 @@ paths: content: application/json; charset=utf-8: schema: - properties: - note: - $ref: '#/components/schemas/Note' - description: Created note - branch: - $ref: '#/components/schemas/Branch' - description: Created branch + $ref: '#/components/schemas/NoteWithBranch' default: description: unexpected error content: @@ -291,6 +285,29 @@ paths: application/json; charset=utf-8: schema: $ref: '#/components/schemas/Error' + /notes/{noteId}/import: + parameters: + - name: noteId + in: path + required: true + schema: + $ref: '#/components/schemas/EntityId' + post: + description: Imports ZIP file into a given note. + operationId: importZip + responses: + '201': + description: note created + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/NoteWithBranch' + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/Error' /notes/{noteId}/note-revision: parameters: - name: noteId @@ -852,6 +869,13 @@ components: utcDateModified: $ref: '#/components/schemas/UtcDateTime' readOnly: true + NoteWithBranch: + type: object + properties: + note: + $ref: '#/components/schemas/Note' + branch: + $ref: '#/components/schemas/Branch' Attribute: type: object description: Attribute (Label, Relation) is a key-value record attached to a note. diff --git a/src/etapi/notes.js b/src/etapi/notes.js index 683544fff..9d56cb966 100644 --- a/src/etapi/notes.js +++ b/src/etapi/notes.js @@ -8,6 +8,7 @@ const v = require("./validators"); const searchService = require("../services/search/services/search"); const SearchContext = require("../services/search/search_context"); const zipExportService = require("../services/export/zip"); +const zipImportService = require("../services/import/zip"); function register(router) { eu.route(router, 'get', '/etapi/notes', (req, res, next) => { @@ -141,11 +142,21 @@ function register(router) { // (e.g. branchIds are not seen in UI), that we export "note export" instead. const branch = note.getParentBranches()[0]; - console.log(note.getParentBranches()); - zipExportService.exportToZip(taskContext, branch, format, res); }); + eu.route(router, 'post' ,'/etapi/notes/:noteId/import', (req, res, next) => { + const note = eu.getAndCheckNote(req.params.noteId); + const taskContext = new TaskContext('no-progress-reporting'); + + zipImportService.importZip(taskContext, req.body, note).then(importedNote => { + res.status(201).json({ + note: mappers.mapNoteToPojo(importedNote), + branch: mappers.mapBranchToPojo(importedNote.getBranches()[0]), + }); + }); // we need better error handling here, async errors won't be properly processed. + }); + eu.route(router, 'post' ,'/etapi/notes/:noteId/note-revision', (req, res, next) => { const note = eu.getAndCheckNote(req.params.noteId); diff --git a/src/services/task_context.js b/src/services/task_context.js index 9aab9d43e..363bd3035 100644 --- a/src/services/task_context.js +++ b/src/services/task_context.js @@ -6,7 +6,7 @@ const ws = require('./ws'); const taskContexts = {}; class TaskContext { - constructor(taskId, taskType = null, data = null) { + constructor(taskId, taskType = null, data = {}) { this.taskId = taskId; this.taskType = taskType; this.data = data; diff --git a/test-etapi/import-zip.http b/test-etapi/import-zip.http new file mode 100644 index 000000000..e831a050a --- /dev/null +++ b/test-etapi/import-zip.http @@ -0,0 +1,12 @@ +POST {{triliumHost}}/etapi/notes/root/import +Authorization: {{authToken}} +Content-Type: application/octet-stream +Content-Transfer-Encoding: binary + +< ../db/demo.zip + +> {% + client.assert(response.status === 201); + client.assert(response.body.note.title == "Trilium Demo"); + client.assert(response.body.branch.parentNoteId == "root"); +%} From e22f77eae7ba6f6bbc9b89254db48fd03e3b81a2 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 15 Jun 2023 23:23:37 +0200 Subject: [PATCH 04/11] release 0.60.3 --- package.json | 2 +- src/services/build.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 43b1241f2..090d13924 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trilium", "productName": "Trilium Notes", "description": "Trilium Notes", - "version": "0.60.2-beta", + "version": "0.60.3", "license": "AGPL-3.0-only", "main": "electron.js", "bin": { diff --git a/src/services/build.js b/src/services/build.js index c539d9bd7..2386ee8af 100644 --- a/src/services/build.js +++ b/src/services/build.js @@ -1 +1 @@ -module.exports = { buildDate:"2023-06-08T22:46:52+02:00", buildRevision: "6e69cafe5419e8efcc6f652647f9227dbcfa1e18" }; +module.exports = { buildDate:"2023-06-15T23:23:37+02:00", buildRevision: "3223e767875e5379c99ff58a562cb9c1a2641bdf" }; From 691fccb769611a9e2e051cd2cc904f9ba7ad2050 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 18 Jun 2023 23:45:48 +0200 Subject: [PATCH 05/11] fix keyboard navigation in the note tree, fixes #4036 --- src/public/app/widgets/type_widgets/editable_text.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index 10004e736..06178c3d7 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -185,14 +185,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { async doRefresh(note) { const noteComplement = await froca.getNoteComplement(note.noteId); - await this.spacedUpdate.allowUpdateWithoutChange(() => { - // https://github.com/zadam/trilium/issues/3914 - // todo: quite hacky, but it works. remove it if ckeditor has fixed it. - this.$editor.trigger('focus'); - this.$editor.trigger('blur') - - this.watchdog.editor.setData(noteComplement.content || ""); - }); + await this.spacedUpdate.allowUpdateWithoutChange(() => + this.watchdog.editor.setData(noteComplement.content || "")); } getData() { From 5905950c17791ce0eb278e010c2c8b3450fdb447 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 19 Jun 2023 00:29:36 +0200 Subject: [PATCH 06/11] fix notePosition assignment for new children of root --- db/migrations/0214__fix_root_children_ordering.sql | 1 + src/services/app_info.js | 2 +- src/services/notes.js | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 db/migrations/0214__fix_root_children_ordering.sql diff --git a/db/migrations/0214__fix_root_children_ordering.sql b/db/migrations/0214__fix_root_children_ordering.sql new file mode 100644 index 000000000..056a76336 --- /dev/null +++ b/db/migrations/0214__fix_root_children_ordering.sql @@ -0,0 +1 @@ +UPDATE branches SET notePosition = notePosition - 999899999 WHERE parentNoteId = 'root' AND notePosition > 999999999; diff --git a/src/services/app_info.js b/src/services/app_info.js index 3f5f02f3b..88338defe 100644 --- a/src/services/app_info.js +++ b/src/services/app_info.js @@ -4,7 +4,7 @@ const build = require('./build'); const packageJson = require('../../package'); const {TRILIUM_DATA_DIR} = require('./data_dir'); -const APP_DB_VERSION = 213; +const APP_DB_VERSION = 214; const SYNC_VERSION = 29; const CLIPPER_PROTOCOL_VERSION = "1.0"; diff --git a/src/services/notes.js b/src/services/notes.js index c9343bc4e..1ec012186 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -26,11 +26,13 @@ const fs = require("fs"); function getNewNotePosition(parentNote) { if (parentNote.isLabelTruthy('newNotesOnTop')) { const minNotePos = parentNote.getChildBranches() + .filter(branch => branch.noteId !== '_hidden') // has "always last" note position .reduce((min, note) => Math.min(min, note.notePosition), 0); return minNotePos - 10; } else { const maxNotePos = parentNote.getChildBranches() + .filter(branch => branch.noteId !== '_hidden') // has "always last" note position .reduce((max, note) => Math.max(max, note.notePosition), 0); return maxNotePos + 10; From defd99742460422491632cd358b6dea4450020de Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 19 Jun 2023 23:26:50 +0200 Subject: [PATCH 07/11] release 0.60.4 --- package.json | 2 +- src/services/build.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 090d13924..f55889ddb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trilium", "productName": "Trilium Notes", "description": "Trilium Notes", - "version": "0.60.3", + "version": "0.60.4", "license": "AGPL-3.0-only", "main": "electron.js", "bin": { diff --git a/src/services/build.js b/src/services/build.js index 2386ee8af..04a133b33 100644 --- a/src/services/build.js +++ b/src/services/build.js @@ -1 +1 @@ -module.exports = { buildDate:"2023-06-15T23:23:37+02:00", buildRevision: "3223e767875e5379c99ff58a562cb9c1a2641bdf" }; +module.exports = { buildDate:"2023-06-19T23:26:50+02:00", buildRevision: "5905950c17791ce0eb278e010c2c8b3450fdb447" }; From 6cfd18b29b38ad2ccbde5bf72f5b74c1e458687a Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 20 Jun 2023 21:19:56 +0200 Subject: [PATCH 08/11] read filter values in icon selector only after async events, #4044 --- src/public/app/widgets/note_icon.js | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/public/app/widgets/note_icon.js b/src/public/app/widgets/note_icon.js index ba2bbb683..bfd7754ca 100644 --- a/src/public/app/widgets/note_icon.js +++ b/src/public/app/widgets/note_icon.js @@ -93,11 +93,11 @@ export default class NoteIconWidget extends NoteContextAwareWidget { }); this.$iconCategory = this.$widget.find("select[name='icon-category']"); - this.$iconCategory.on('change', () => this.renderFilteredDropdown()); + this.$iconCategory.on('change', () => this.renderDropdown()); this.$iconCategory.on('click', e => e.stopPropagation()); this.$iconSearch = this.$widget.find("input[name='icon-search']"); - this.$iconSearch.on('input', () => this.renderFilteredDropdown()); + this.$iconSearch.on('input', () => this.renderDropdown()); this.$notePathList = this.$widget.find(".note-path-list"); this.$widget.on('show.bs.dropdown', async () => { @@ -140,14 +140,7 @@ export default class NoteIconWidget extends NoteContextAwareWidget { } } - renderFilteredDropdown() { - const categoryId = parseInt(this.$iconCategory.find('option:selected').val()); - const search = this.$iconSearch.val(); - - this.renderDropdown(categoryId, search); - } - - async renderDropdown(categoryId, search) { + async renderDropdown() { const iconToCountPromise = this.getIconToCountMap(); this.$iconList.empty(); @@ -165,8 +158,10 @@ export default class NoteIconWidget extends NoteContextAwareWidget { } const {icons} = (await import('./icon_list.js')).default; + const iconToCount = await iconToCountPromise; - search = search?.trim()?.toLowerCase(); + const categoryId = parseInt(this.$iconCategory.find('option:selected').val()); + const search = this.$iconSearch.val().trim().toLowerCase(); const filteredIcons = icons.filter(icon => { if (categoryId && icon.category_id !== categoryId) { @@ -182,8 +177,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget { return true; }); - const iconToCount = await iconToCountPromise; - filteredIcons.sort((a, b) => { const countA = iconToCount[a.className] || 0; const countB = iconToCount[b.className] || 0; From 8095c77b9188b7e40bd00a331912114b68b56893 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 20 Jun 2023 21:31:25 +0200 Subject: [PATCH 09/11] cache icon count to make filtering more responsive on slower connections, #4044 --- src/public/app/widgets/note_icon.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/public/app/widgets/note_icon.js b/src/public/app/widgets/note_icon.js index bfd7754ca..da3582d8f 100644 --- a/src/public/app/widgets/note_icon.js +++ b/src/public/app/widgets/note_icon.js @@ -141,7 +141,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget { } async renderDropdown() { - const iconToCountPromise = this.getIconToCountMap(); + const iconToCount = await this.getIconToCountMap(); + const {icons} = (await import('./icon_list.js')).default; this.$iconList.empty(); @@ -157,9 +158,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget { ); } - const {icons} = (await import('./icon_list.js')).default; - const iconToCount = await iconToCountPromise; - const categoryId = parseInt(this.$iconCategory.find('option:selected').val()); const search = this.$iconSearch.val().trim().toLowerCase(); @@ -192,9 +190,12 @@ export default class NoteIconWidget extends NoteContextAwareWidget { } async getIconToCountMap() { - const {iconClassToCountMap} = await server.get('other/icon-usage'); + if (!this.iconToCountCache) { + this.iconToCountCache = server.get('other/icon-usage'); + setTimeout(() => this.iconToCountCache = null, 20000); // invalidate cache after 20 seconds + } - return iconClassToCountMap; + return (await this.iconToCountCache).iconClassToCountMap; } renderIcon(icon) { From 08398a1417bc9e09b6492eca6c22f32c865e2fff Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 22 Jun 2023 22:30:26 +0200 Subject: [PATCH 10/11] fix search scripts, closes #4048 --- package-lock.json | 4 ++-- src/services/search/services/search.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d9b63fc7..6ba6312f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trilium", - "version": "0.60.2-beta", + "version": "0.60.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "trilium", - "version": "0.60.2-beta", + "version": "0.60.4", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index 3e36040ab..cb565a252 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -10,7 +10,6 @@ const becca = require('../../../becca/becca'); const beccaService = require('../../../becca/becca_service'); const utils = require('../../utils'); const log = require('../../log'); -const scriptService = require("../../script"); const hoistedNoteService = require("../../hoisted_note"); function searchFromNote(note) { @@ -73,6 +72,7 @@ function searchFromRelation(note, relationName) { return []; } + const scriptService = require("../../script"); // to avoid circular dependency const result = scriptService.executeNote(scriptNote, {originEntity: note}); if (!Array.isArray(result)) { From ddda4d9867a16c17f6f6ff675e77de5fec146503 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 27 Jun 2023 22:57:13 +0200 Subject: [PATCH 11/11] fix constructing result note path in the jump to note dialog, closes #4054 --- .../search/expressions/note_flat_text.js | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/src/services/search/expressions/note_flat_text.js b/src/services/search/expressions/note_flat_text.js index 62985f590..3703b5a2f 100644 --- a/src/services/search/expressions/note_flat_text.js +++ b/src/services/search/expressions/note_flat_text.js @@ -19,20 +19,22 @@ class NoteFlatTextExp extends Expression { /** * @param {BNote} note - * @param {string[]} tokens - * @param {string[]} path + * @param {string[]} remainingTokens - tokens still needed to be found in the path towards root + * @param {string[]} takenPath - path so far taken towards from candidate note towards the root. + * It contains the suffix fragment of the full note path. */ - const searchDownThePath = (note, tokens, path) => { - if (tokens.length === 0) { - const retPath = this.getNotePath(note, path); + const searchPathTowardsRoot = (note, remainingTokens, takenPath) => { + if (remainingTokens.length === 0) { + // we're done, just build the result + const resultPath = this.getNotePath(note, takenPath); - if (retPath) { - const noteId = retPath[retPath.length - 1]; + if (resultPath) { + const noteId = resultPath[resultPath.length - 1]; if (!resultNoteSet.hasNoteId(noteId)) { // we could get here from multiple paths, the first one wins because the paths // are sorted by importance - executionContext.noteIdToNotePath[noteId] = retPath; + executionContext.noteIdToNotePath[noteId] = resultPath; resultNoteSet.add(becca.notes[noteId]); } @@ -42,22 +44,23 @@ class NoteFlatTextExp extends Expression { } if (note.parents.length === 0 || note.noteId === 'root') { + // we've reached root, but there are still remaining tokens -> this candidate note produced no result return; } const foundAttrTokens = []; - for (const token of tokens) { + for (const token of remainingTokens) { if (note.type.includes(token) || note.mime.includes(token)) { foundAttrTokens.push(token); } } - for (const attribute of note.ownedAttributes) { + for (const attribute of note.getOwnedAttributes()) { const normalizedName = utils.normalize(attribute.name); const normalizedValue = utils.normalize(attribute.value); - for (const token of tokens) { + for (const token of remainingTokens) { if (normalizedName.includes(token) || normalizedValue.includes(token)) { foundAttrTokens.push(token); } @@ -68,19 +71,19 @@ class NoteFlatTextExp extends Expression { const title = utils.normalize(beccaService.getNoteTitle(note.noteId, parentNote.noteId)); const foundTokens = foundAttrTokens.slice(); - for (const token of tokens) { + for (const token of remainingTokens) { if (title.includes(token)) { foundTokens.push(token); } } if (foundTokens.length > 0) { - const remainingTokens = tokens.filter(token => !foundTokens.includes(token)); + const newRemainingTokens = remainingTokens.filter(token => !foundTokens.includes(token)); - searchDownThePath(parentNote, remainingTokens, [...path, note.noteId]); + searchPathTowardsRoot(parentNote, newRemainingTokens, [note.noteId, ...takenPath]); } else { - searchDownThePath(parentNote, tokens, [...path, note.noteId]); + searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId, ...takenPath]); } } } @@ -90,7 +93,7 @@ class NoteFlatTextExp extends Expression { for (const note of candidateNotes) { // autocomplete should be able to find notes by their noteIds as well (only leafs) if (this.tokens.length === 1 && note.noteId.toLowerCase() === this.tokens[0]) { - searchDownThePath(note, [], []); + searchPathTowardsRoot(note, [], [note.noteId]); continue; } @@ -123,7 +126,7 @@ class NoteFlatTextExp extends Expression { if (foundTokens.length > 0) { const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token)); - searchDownThePath(parentNote, remainingTokens, [note.noteId]); + searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId]); } } } @@ -131,14 +134,22 @@ class NoteFlatTextExp extends Expression { return resultNoteSet; } - getNotePath(note, path) { - if (path.length === 0) { + /** + * @param {BNote} note + * @param {string[]} takenPath + * @returns {string[]} + */ + getNotePath(note, takenPath) { + if (takenPath.length === 0) { + throw new Error("Path is not expected to be empty."); + } else if (takenPath.length === 1 && takenPath[0] === note.noteId) { return note.getBestNotePath(); } else { - const closestNoteId = path[0]; - const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePath(); + // this note is the closest to root containing the last matching token(s), thus completing the requirements + // what's in this note's predecessors does not matter, thus we'll choose the best note path + const topMostMatchingTokenNotePath = becca.getNote(takenPath[0]).getBestNotePath(); - return [...closestNoteBestNotePath, ...path.slice(1)]; + return [...topMostMatchingTokenNotePath, ...takenPath.slice(1)]; } }