diff --git a/db/demo.zip b/db/demo.zip index f9efd3e44..5344f44c2 100644 Binary files a/db/demo.zip and b/db/demo.zip differ diff --git a/db/migrations/0204__migrate_bookmarks_to_clones.js b/db/migrations/0204__migrate_bookmarks_to_clones.js index 82cc11ebe..6b180a446 100644 --- a/db/migrations/0204__migrate_bookmarks_to_clones.js +++ b/db/migrations/0204__migrate_bookmarks_to_clones.js @@ -12,5 +12,10 @@ module.exports = () => { attr.markAsDeleted("0204__migrate_bookmarks_to_clones"); } + + // bookmarkFolder used to work in 0.57 without the bookmarked label + for (const attr of becca.findAttributes('label','bookmarkFolder')) { + cloningService.toggleNoteInParent(true, attr.noteId, '_lbBookmarks'); + } }); }; diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js index d22c1054e..9d19edcd7 100644 --- a/src/becca/becca_service.js +++ b/src/becca/becca_service.js @@ -82,10 +82,8 @@ function getNoteTitleArrayForPath(notePathArray) { throw new Error(`${notePathArray} is not an array.`); } - const hoistedNoteId = cls.getHoistedNoteId(); - - if (notePathArray.length === 1 && notePathArray[0] === hoistedNoteId) { - return [getNoteTitle(hoistedNoteId)]; + if (notePathArray.length === 1) { + return [getNoteTitle(notePathArray[0])]; } const titles = []; @@ -94,6 +92,7 @@ function getNoteTitleArrayForPath(notePathArray) { let hoistedNotePassed = false; // this is a notePath from outside of hoisted subtree so full title path needs to be returned + const hoistedNoteId = cls.getHoistedNoteId(); const outsideOfHoistedSubtree = !notePathArray.includes(hoistedNoteId); for (const noteId of notePathArray) { diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 71e351f38..c41c202b3 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -11,6 +11,7 @@ const BNoteRevision = require("./bnote_revision"); const TaskContext = require("../../services/task_context"); const dayjs = require("dayjs"); const utc = require('dayjs/plugin/utc'); +const eventService = require("../../services/events"); dayjs.extend(utc) const LABEL = 'label'; @@ -314,6 +315,11 @@ class BNote extends AbstractBeccaEntity { utcDateChanged: pojo.utcDateModified, isSynced: true }); + + eventService.emit(eventService.ENTITY_CHANGED, { + entityName: 'note_contents', + entity: this + }); } setJsonContent(content) { @@ -1124,6 +1130,13 @@ class BNote extends AbstractBeccaEntity { return notePaths; } + /** + * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree + */ + isHiddenCompletely() { + return !this.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden')); + } + /** * @param ancestorNoteId * @returns {boolean} - true if ancestorNoteId occurs in at least one of the note's paths @@ -1368,7 +1381,7 @@ class BNote extends AbstractBeccaEntity { } isOptions() { - return this.noteId.startsWith("options"); + return this.noteId.startsWith("_options"); } get isDeleted() { diff --git a/src/public/app/doc_notes/launchbar_history_navigation.html b/src/public/app/doc_notes/launchbar_history_navigation.html new file mode 100644 index 000000000..bf4411572 --- /dev/null +++ b/src/public/app/doc_notes/launchbar_history_navigation.html @@ -0,0 +1,3 @@ +
Back and Forward buttons allow you to move in the navigation history.
+ +These launchers are active only in the desktop build and will be ignored in the server edition where you can use the native browser navigation buttons instead.
diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js index fa08995f2..42e8eadbe 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -360,6 +360,13 @@ class FNote { return notePaths; } + /** + * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree + */ + isHiddenCompletely() { + return !this.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden')); + } + __filterAttrs(attributes, type, name) { this.__validateTypeName(type, name); @@ -852,7 +859,7 @@ class FNote { } isOptions() { - return this.noteId.startsWith("options"); + return this.noteId.startsWith("_options"); } } diff --git a/src/public/app/services/note_autocomplete.js b/src/public/app/services/note_autocomplete.js index 157c24261..07c75d447 100644 --- a/src/public/app/services/note_autocomplete.js +++ b/src/public/app/services/note_autocomplete.js @@ -230,6 +230,10 @@ function init() { $.fn.getSelectedNoteId = function () { const notePath = $(this).getSelectedNotePath(); + if (!notePath) { + return null; + } + const chunks = notePath.split('/'); return chunks.length >= 1 ? chunks[chunks.length - 1] : null; diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.js b/src/public/app/widgets/attribute_widgets/attribute_detail.js index 92d7d6b9b..7ceb9bbcb 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.js +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.js @@ -247,7 +247,7 @@ const ATTR_HELP = { "runOnNoteCreation": "executes when note is created on backend. Use this relation if you want to run the script for all notes created under a specific subtree. In that case, create it on the subtree root note and make it inheritable. A new note created within the subtree (any depth) will trigger the script.", "runOnChildNoteCreation": "executes when new note is created under the note where this relation is defined", "runOnNoteTitleChange": "executes when note title is changed (includes note creation as well)", - "runOnNoteContentChange": "executes when note content is changed (includes note creation as well).", + "runOnNoteContentChange": "executes when note content is changed (includes note creation as well).", "runOnNoteChange": "executes when note is changed (includes note creation as well). Does not include content changes", "runOnNoteDeletion": "executes when note is being deleted", "runOnBranchCreation": "executes when a branch is created. Branch is a link between parent note and child note and is created e.g. when cloning or moving note.", diff --git a/src/public/app/widgets/buttons/command_button.js b/src/public/app/widgets/buttons/command_button.js index 376c5a822..12e0dd821 100644 --- a/src/public/app/widgets/buttons/command_button.js +++ b/src/public/app/widgets/buttons/command_button.js @@ -39,7 +39,7 @@ export default class CommandButtonWidget extends AbstractButtonWidget { /** * @param {function|string} command - * @returns {CommandButtonWidget} + * @returns {this} */ command(command) { this.settings.command = command; diff --git a/src/public/app/widgets/buttons/global_menu.js b/src/public/app/widgets/buttons/global_menu.js index ff881aa20..63045532a 100644 --- a/src/public/app/widgets/buttons/global_menu.js +++ b/src/public/app/widgets/buttons/global_menu.js @@ -303,7 +303,7 @@ export default class GlobalMenuWidget extends BasicWidget { const resp = await fetch(RELEASES_API_URL); const data = await resp.json(); - return data.tag_name.substring(1); + return data?.tag_name?.substring(1); } downloadLatestVersionCommand() { diff --git a/src/public/app/widgets/buttons/history_navigation.js b/src/public/app/widgets/buttons/history_navigation.js index 430aeae21..08a86810f 100644 --- a/src/public/app/widgets/buttons/history_navigation.js +++ b/src/public/app/widgets/buttons/history_navigation.js @@ -23,6 +23,10 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget { doRender() { super.doRender(); + if (!utils.isElectron()) { + return; + } + this.webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents(); // without this the history is preserved across frontend reloads diff --git a/src/public/app/widgets/dialogs/note_revisions.js b/src/public/app/widgets/dialogs/note_revisions.js index 17d578c2d..f8c6584ab 100644 --- a/src/public/app/widgets/dialogs/note_revisions.js +++ b/src/public/app/widgets/dialogs/note_revisions.js @@ -234,14 +234,14 @@ export default class NoteRevisionsDialog extends BasicWidget { renderMathInElement($content[0], {trust: true}); } } - else if (revisionItem.type === 'code') { + else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') { this.$content.html($("").text(fullNoteRevision.content)); } else if (revisionItem.type === 'image') { this.$content.html($("") // reason why we put this inline as base64 is that we do not want to let user copy this // as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be an uploaded as a new note - .attr("src", `data:${note.mime};base64,${fullNoteRevision.content}`) + .attr("src", `data:${fullNoteRevision.mime};base64,${fullNoteRevision.content}`) .css("max-width", "100%") .css("max-height", "100%")); } diff --git a/src/public/app/widgets/floating_buttons/code_buttons.js b/src/public/app/widgets/floating_buttons/code_buttons.js index 9363856a5..f1763870b 100644 --- a/src/public/app/widgets/floating_buttons/code_buttons.js +++ b/src/public/app/widgets/floating_buttons/code_buttons.js @@ -76,7 +76,7 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget { this.$saveToNoteButton.toggle( note.mime === 'text/x-sqlite;schema=trilium' - && !note.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden')) + && note.isHiddenCompletely() ); this.$openTriliumApiDocsButton.toggle(note.mime.startsWith('application/javascript;env=')); diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 8e9fcdf04..204bb77db 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -1309,6 +1309,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { await this.tree.reload([rootNode]); }); + await this.filterHoistedBranch(); + if (activeNotePath) { const node = await this.getNodeFromPath(activeNotePath, true); diff --git a/src/public/app/widgets/ribbon_widgets/search_definition.js b/src/public/app/widgets/ribbon_widgets/search_definition.js index 0dc4d4841..4ca8e17d9 100644 --- a/src/public/app/widgets/ribbon_widgets/search_definition.js +++ b/src/public/app/widgets/ribbon_widgets/search_definition.js @@ -270,7 +270,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget { async refreshWithNote(note) { this.$component.show(); - this.$saveToNoteButton.toggle(!note.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden'))); + this.$saveToNoteButton.toggle(note.isHiddenCompletely()); this.$searchOptions.empty(); diff --git a/src/routes/api/options.js b/src/routes/api/options.js index a176bb45f..ddb4321ec 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -112,7 +112,7 @@ function update(name, value) { } function getUserThemes() { - const notes = searchService.searchNotes("#appTheme"); + const notes = searchService.searchNotes("#appTheme", {ignoreHoistedNote: true}); const ret = []; for (const note of notes) { diff --git a/src/services/build.js b/src/services/build.js index f3031a049..dc5dc5172 100644 --- a/src/services/build.js +++ b/src/services/build.js @@ -1 +1 @@ -module.exports = { buildDate:"2023-01-11T23:44:33+01:00", buildRevision: "bdfdc0402ddb23e9af002580f368bc52e4268b3a" }; +module.exports = { buildDate:"2023-01-16T22:39:28+01:00", buildRevision: "9fd0b85ff2be264be35ec2052c956b654f0dac9e" }; diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js index d3558b884..1597c6599 100644 --- a/src/services/builtin_attributes.js +++ b/src/services/builtin_attributes.js @@ -71,6 +71,7 @@ module.exports = [ { type: 'relation', name: 'runOnNoteCreation', isDangerous: true }, { type: 'relation', name: 'runOnNoteTitleChange', isDangerous: true }, { type: 'relation', name: 'runOnNoteChange', isDangerous: true }, + { type: 'relation', name: 'runOnNoteContentChange', isDangerous: true }, { type: 'relation', name: 'runOnNoteDeletion', isDangerous: true }, { type: 'relation', name: 'runOnBranchCreation', isDangerous: true }, { type: 'relation', name: 'runOnBranchDeletion', isDangerous: true }, diff --git a/src/services/handlers.js b/src/services/handlers.js index 588b80555..4bb21f910 100644 --- a/src/services/handlers.js +++ b/src/services/handlers.js @@ -4,6 +4,8 @@ const treeService = require('./tree'); const noteService = require('./notes'); const becca = require('../becca/becca'); const BAttribute = require('../becca/entities/battribute'); +const hiddenSubtreeService = require("./hidden_subtree"); +const oneTimeTimer = require("./one_time_timer"); function runAttachedRelations(note, relationName, originEntity) { if (!note) { @@ -206,6 +208,16 @@ eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) => if (entityName === 'branches') { runAttachedRelations(entity.getNote(), 'runOnBranchDeletion', entity); } + + if (entityName === 'notes' && entity.noteId.startsWith("_")) { + // "named" note has been deleted, we will probably need to rebuild the hidden subtree + // scheduling so that bulk deletes won't trigger so many checks + oneTimeTimer.scheduleExecution('hidden-subtree-check', 1000, () => { + console.log("Checking hidden subtree"); + + hiddenSubtreeService.checkHiddenSubtree(); + }); + } }); module.exports = { diff --git a/src/services/hidden_subtree.js b/src/services/hidden_subtree.js index ff8036c1c..ba62ab288 100644 --- a/src/services/hidden_subtree.js +++ b/src/services/hidden_subtree.js @@ -1,6 +1,8 @@ const becca = require("../becca/becca"); const noteService = require("./notes"); const BAttribute = require("../becca/entities/battribute"); +const log = require("./log"); +const migrationService = require("./migration"); const LBTPL_ROOT = "_lbTplRoot"; const LBTPL_BASE = "_lbTplBase"; @@ -179,8 +181,10 @@ const HIDDEN_SUBTREE_DEFINITION = { isExpanded: true, attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ], children: [ - { id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square' }, - { id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square' }, + { id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square', + attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]}, + { id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square', + attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]}, { id: '_lbBackendLog', title: 'Backend Log', type: 'launcher', targetNoteId: '_backendLog', icon: 'bx bx-terminal' }, ] }, @@ -237,6 +241,12 @@ const HIDDEN_SUBTREE_DEFINITION = { }; function checkHiddenSubtree() { + if (!migrationService.isDbUpToDate()) { + // on-delete hook might get triggered during some future migration and cause havoc + log.info("Will not check hidden subtree until migration is finished."); + return; + } + checkHiddenSubtreeRecursively('root', HIDDEN_SUBTREE_DEFINITION); } diff --git a/src/services/import/enex.js b/src/services/import/enex.js index b982d642a..cb1c3dcaa 100644 --- a/src/services/import/enex.js +++ b/src/services/import/enex.js @@ -76,8 +76,8 @@ function importEnex(taskContext, file, parentNote) { content = content.replace(/<\/ol>\s*
${result.content}+
${escapeHtml(result.content)}