diff --git a/db/demo.zip b/db/demo.zip index aa395f325..f9efd3e44 100644 Binary files a/db/demo.zip and b/db/demo.zip differ diff --git a/docs/backend_api/Attribute.html b/docs/backend_api/Attribute.html index 434612be7..28e49bde4 100644 --- a/docs/backend_api/Attribute.html +++ b/docs/backend_api/Attribute.html @@ -1176,7 +1176,7 @@ and relation (representing named relationship between source and target note)Source:
@@ -1370,7 +1370,7 @@ and relation (representing named relationship between source and target note)Source:
@@ -1564,7 +1564,7 @@ and relation (representing named relationship between source and target note)Source:
diff --git a/docs/backend_api/BackendScriptApi.html b/docs/backend_api/BackendScriptApi.html index ea272a14d..c80876aac 100644 --- a/docs/backend_api/BackendScriptApi.html +++ b/docs/backend_api/BackendScriptApi.html @@ -81,7 +81,7 @@
Source:
@@ -185,7 +185,7 @@
Source:
@@ -295,7 +295,7 @@
Source:
@@ -405,7 +405,7 @@
Source:
@@ -515,7 +515,7 @@
Source:
@@ -625,7 +625,7 @@
Source:
@@ -735,7 +735,7 @@
Source:
@@ -845,7 +845,7 @@
Source:
@@ -955,7 +955,7 @@
Source:
@@ -1131,7 +1131,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -1298,7 +1298,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -1584,7 +1584,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -1636,6 +1636,143 @@ JSON MIME type. See also createNewNote() for more options. + + + + + + +

createOrUpdateLauncher(opts)

+ + + + + + +
+ Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +CreateOrUpdateLauncher + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -1789,7 +1926,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -1971,7 +2108,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2172,7 +2309,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2323,7 +2460,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2429,7 +2566,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2587,7 +2724,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2741,7 +2878,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -2944,7 +3081,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -3145,7 +3282,7 @@ JSON MIME type. See also createNewNote() for more options.
Source:
@@ -3255,7 +3392,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -3456,7 +3593,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -3610,7 +3747,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -3811,7 +3948,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4012,7 +4149,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4118,7 +4255,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4288,7 +4425,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4522,7 +4659,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4723,7 +4860,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4876,7 +5013,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -5013,7 +5150,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -5121,7 +5258,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -5302,7 +5439,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -5504,7 +5641,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -5713,7 +5850,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
Source:
@@ -5846,7 +5983,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
Source:
@@ -6052,7 +6189,7 @@ This method looks similar to toggleNoteInParent() but differs because we're look
Source:
@@ -6208,7 +6345,7 @@ exists, then we'll use that transaction.
Source:
@@ -6363,7 +6500,7 @@ exists, then we'll use that transaction.
Source:
diff --git a/docs/backend_api/becca_entities_abstract_entity.js.html b/docs/backend_api/becca_entities_abstract_entity.js.html index f06a0c9f1..b7b47377a 100644 --- a/docs/backend_api/becca_entities_abstract_entity.js.html +++ b/docs/backend_api/becca_entities_abstract_entity.js.html @@ -59,7 +59,7 @@ class AbstractEntity { let contentToHash = ""; for (const propertyName of this.constructor.hashedProperties) { - contentToHash += "|" + this[propertyName]; + contentToHash += `|${this[propertyName]}`; } if (isDeleted) { diff --git a/docs/backend_api/becca_entities_attribute.js.html b/docs/backend_api/becca_entities_attribute.js.html index fb653b6d2..dbf8a63ba 100644 --- a/docs/backend_api/becca_entities_attribute.js.html +++ b/docs/backend_api/becca_entities_attribute.js.html @@ -117,11 +117,15 @@ class Attribute extends AbstractEntity { validate() { if (!["label", "relation"].includes(this.type)) { - throw new Error(`Invalid attribute type '${this.type}' in attribute '${this.attributeId}'`); + throw new Error(`Invalid attribute type '${this.type}' in attribute '${this.attributeId}' of note '${this.noteId}'`); } if (!this.name?.trim()) { - throw new Error(`Invalid empty name in attribute '${this.attributeId}'`); + throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`); + } + + if (this.type === 'relation' && !(this.value in this.becca.notes)) { + throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it target not existing note '${this.value}'.`); } } @@ -204,11 +208,7 @@ class Attribute extends AbstractEntity { beforeSaving() { this.validate(); - if (this.type === 'relation') { - if (!(this.value in this.becca.notes)) { - throw new Error(`Cannot save relation '${this.name}' since it target not existing note '${this.value}'.`); - } - } else if (!this.value) { + if (!this.value) { // null value isn't allowed this.value = ""; } diff --git a/docs/backend_api/becca_entities_branch.js.html b/docs/backend_api/becca_entities_branch.js.html index 5644529d0..36f0cfa45 100644 --- a/docs/backend_api/becca_entities_branch.js.html +++ b/docs/backend_api/becca_entities_branch.js.html @@ -157,7 +157,7 @@ class Branch extends AbstractEntity { * @returns {boolean} */ get isWeak() { - return ['share', 'lbBookmarks'].includes(this.parentNoteId); + return ['_share', 'lbBookmarks'].includes(this.parentNoteId); } /** @@ -213,7 +213,7 @@ class Branch extends AbstractEntity { // first delete children and then parent - this will show up better in recent changes - log.info("Deleting note " + note.noteId); + log.info(`Deleting note ${note.noteId}`); this.becca.notes[note.noteId].isBeingDeleted = true; @@ -239,7 +239,7 @@ class Branch extends AbstractEntity { let maxNotePos = 0; for (const childBranch of this.parentNote.getChildBranches()) { - if (maxNotePos < childBranch.notePosition && childBranch.branchId !== 'hidden') { + if (maxNotePos < childBranch.notePosition && childBranch.branchId !== '_hidden') { maxNotePos = childBranch.notePosition; } } diff --git a/docs/backend_api/becca_entities_note.js.html b/docs/backend_api/becca_entities_note.js.html index bcca49db7..689030bf5 100644 --- a/docs/backend_api/becca_entities_note.js.html +++ b/docs/backend_api/becca_entities_note.js.html @@ -239,7 +239,7 @@ class Note extends AbstractEntity { return undefined; } else { - throw new Error("Cannot find note content for noteId=" + this.noteId); + throw new Error(`Cannot find note content for noteId=${this.noteId}`); } } @@ -332,7 +332,7 @@ class Note extends AbstractEntity { sql.upsert("note_contents", "noteId", pojo); - const hash = utils.hash(this.noteId + "|" + pojo.content.toString()); + const hash = utils.hash(`${this.noteId}|${pojo.content.toString()}`); entityChangesService.addEntityChange({ entityName: 'note_contents', @@ -767,22 +767,22 @@ class Note extends AbstractEntity { */ getFlatText() { if (!this.flatTextCache) { - this.flatTextCache = this.noteId + ' ' + this.type + ' ' + this.mime + ' '; + 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 += ' '; @@ -932,7 +932,7 @@ class Note extends AbstractEntity { function addSubtreeNotesInner(note, parentNote = null) { // share can be removed after 0.57 since it will be put under hidden - if (note.noteId === 'hidden' || note.noteId === 'share') { + if (note.noteId === '_hidden' || note.noteId === '_share') { return; } @@ -1152,7 +1152,7 @@ class Note extends AbstractEntity { const attributes = this.getOwnedAttributes(); const attr = attributes.find(attr => attr.type === type && attr.name === name); - value = value !== null && value !== undefined ? value.toString() : ""; + value = value?.toString() || ""; if (attr) { if (attr.value !== value) { @@ -1370,7 +1370,7 @@ class Note extends AbstractEntity { } isLaunchBarConfig() { - return this.type === 'launcher' || ['lbRoot', 'lbAvailableLaunchers', 'lbVisibleLaunchers'].includes(this.noteId); + return this.type === 'launcher' || ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(this.noteId); } isOptions() { diff --git a/docs/backend_api/becca_entities_note_revision.js.html b/docs/backend_api/becca_entities_note_revision.js.html index f82037b93..701317705 100644 --- a/docs/backend_api/becca_entities_note_revision.js.html +++ b/docs/backend_api/becca_entities_note_revision.js.html @@ -109,7 +109,7 @@ class NoteRevision extends AbstractEntity { return undefined; } else { - throw new Error("Cannot find note revision content for noteRevisionId=" + this.noteRevisionId); + throw new Error(`Cannot find note revision content for noteRevisionId=${this.noteRevisionId}`); } } @@ -152,7 +152,7 @@ class NoteRevision extends AbstractEntity { sql.upsert("note_revision_contents", "noteRevisionId", pojo); - const hash = utils.hash(this.noteRevisionId + "|" + pojo.content.toString()); + const hash = utils.hash(`${this.noteRevisionId}|${pojo.content.toString()}`); entityChangesService.addEntityChange({ entityName: 'note_revision_contents', diff --git a/docs/backend_api/global.html b/docs/backend_api/global.html index 571777ef8..a50f15612 100644 --- a/docs/backend_api/global.html +++ b/docs/backend_api/global.html @@ -391,7 +391,7 @@
Source:
@@ -579,7 +579,7 @@
Source:
@@ -767,7 +767,7 @@
Source:
@@ -1053,7 +1053,422 @@
Source:
+ + + + + + + + + + + + + + + + +

CreateOrUpdateLauncher

+ + + + + + +
Type:
+ + + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
id + + +string + + + + + + + + + + id of the launcher, only alphanumeric at least 6 characters long
type + + +string + + + + + + + + + + one of + * "note" - activating the launcher will navigate to the target note (specified in targetNoteId param) + * "script" - activating the launcher will execute the script (specified in scriptNoteId param) + * "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param)
title + + +string + + + + + + + + + +
isVisible + + +boolean + + + + + + <optional>
+ + + +
+ + false + + if true, will be created in the "Visible launchers", otherwise in "Available launchers"
icon + + +string + + + + + + <optional>
+ + + +
+ + name of the boxicon to be used (e.g. "bx-time")
keyboardShortcut + + +string + + + + + + <optional>
+ + + +
+ + will activate the target note/script upon pressing, e.g. "ctrl+e"
targetNoteId + + +string + + + + + + <optional>
+ + + +
+ + for type "note"
scriptNoteId + + +string + + + + + + <optional>
+ + + +
+ + for type "script"
widgetNoteId + + +string + + + + + + <optional>
+ + + +
+ + for type "customWidget"
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
diff --git a/docs/backend_api/module-sql.html b/docs/backend_api/module-sql.html index 5279b0454..58ca58289 100644 --- a/docs/backend_api/module-sql.html +++ b/docs/backend_api/module-sql.html @@ -250,7 +250,7 @@
Source:
@@ -430,7 +430,7 @@
Source:
@@ -632,7 +632,7 @@
Source:
@@ -834,7 +834,7 @@
Source:
@@ -1036,7 +1036,7 @@
Source:
@@ -1238,7 +1238,7 @@
Source:
diff --git a/docs/backend_api/services_backend_script_api.js.html b/docs/backend_api/services_backend_script_api.js.html index b74145bd8..77cbc836e 100644 --- a/docs/backend_api/services_backend_script_api.js.html +++ b/docs/backend_api/services_backend_script_api.js.html @@ -44,6 +44,8 @@ const SearchContext = require("./search/search_context"); const becca = require("../becca/becca"); const ws = require("./ws"); const SpacedUpdate = require("./spaced_update"); +const specialNotesService = require("./special_notes"); +const branchService = require("./branches.js"); /** * This is the main backend API interface for scripts. It's published in the local "api" object. @@ -478,13 +480,93 @@ function BackendScriptApi(currentNote, apiParams) { * @method * @deprecated - this is now no-op since all the changes should be gracefully handled per widget */ - this.refreshTree = () => {}; + this.refreshTree = () => { + console.warn("api.refreshTree() is a NO-OP and can be removed from your script.") + }; /** * @return {{syncVersion, appVersion, buildRevision, dbVersion, dataDirectory, buildDate}|*} - object representing basic info about running Trilium version */ this.getAppInfo = () => appInfo + /** + * @typedef {Object} CreateOrUpdateLauncher + * @property {string} id - id of the launcher, only alphanumeric at least 6 characters long + * @property {string} type - one of + * * "note" - activating the launcher will navigate to the target note (specified in targetNoteId param) + * * "script" - activating the launcher will execute the script (specified in scriptNoteId param) + * * "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param) + * @property {string} title + * @property {boolean} [isVisible=false] - if true, will be created in the "Visible launchers", otherwise in "Available launchers" + * @property {string} [icon] - name of the boxicon to be used (e.g. "bx-time") + * @property {string} [keyboardShortcut] - will activate the target note/script upon pressing, e.g. "ctrl+e" + * @property {string} [targetNoteId] - for type "note" + * @property {string} [scriptNoteId] - for type "script" + * @property {string} [widgetNoteId] - for type "customWidget" + */ + + /** + * Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated. + * + * @param {CreateOrUpdateLauncher} opts + */ + this.createOrUpdateLauncher = opts => { + if (!opts.id) { throw new Error("ID is a mandatory parameter for api.createOrUpdateLauncher(opts)"); } + if (!opts.id.match(/[a-z0-9]{6,1000}/i)) { throw new Error(`ID must be an alphanumeric string at least 6 characters long.`); } + if (!opts.type) { throw new Error("Launcher Type is a mandatory parameter for api.createOrUpdateLauncher(opts)"); } + if (!["note", "script", "customWidget"].includes(opts.type)) { throw new Error(`Given launcher type '${opts.type}'`); } + if (!opts.title?.trim()) { throw new Error("Title is a mandatory parameter for api.createOrUpdateLauncher(opts)"); } + if (opts.type === 'note' && !opts.targetNoteId) { throw new Error("targetNoteId is mandatory for launchers of type 'note'"); } + if (opts.type === 'script' && !opts.scriptNoteId) { throw new Error("scriptNoteId is mandatory for launchers of type 'script'"); } + if (opts.type === 'customWidget' && !opts.widgetNoteId) { throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'"); } + + const parentNoteId = !!opts.isVisible ? '_lbVisibleLaunchers' : '_lbAvailableLaunchers'; + const actualId = 'al_' + opts.id; + + const launcherNote = + becca.getNote(opts.id) || + specialNotesService.createLauncher({ + id: actualId, + parentNoteId: parentNoteId, + launcherType: opts.type, + }).note; + + if (launcherNote.title !== opts.title) { + launcherNote.title = opts.title; + launcherNote.save(); + } + + if (launcherNote.getParentBranches().length === 1) { + const branch = launcherNote.getParentBranches()[0]; + + if (branch.parentNoteId !== parentNoteId) { + branchService.moveBranchToNote(branch, parentNoteId); + } + } + + if (opts.type === 'note') { + launcherNote.setRelation('target', opts.targetNoteId); + } else if (opts.type === 'script') { + launcherNote.setRelation('script', opts.scriptNoteId); + } else if (opts.type === 'customWidget') { + launcherNote.setRelation('widget', opts.widgetNoteId); + } else { + throw new Error(`Unrecognized launcher type '${opts.type}'`); + } + + if (opts.keyboardShortcut) { + launcherNote.setLabel('keyboardShortcut', opts.keyboardShortcut); + } else { + launcherNote.removeLabel('keyboardShortcut'); + } + + if (opts.icon) { + launcherNote.setLabel('iconClass', `bx ${opts.icon}`); + } else { + launcherNote.removeLabel('keyboardShortcut'); + } + }; + /** * This object contains "at your risk" and "no BC guarantees" objects for advanced use cases. * diff --git a/docs/backend_api/services_sql.js.html b/docs/backend_api/services_sql.js.html index 56e81262e..91b2d3863 100644 --- a/docs/backend_api/services_sql.js.html +++ b/docs/backend_api/services_sql.js.html @@ -56,14 +56,20 @@ const LOG_ALL_QUERIES = false; function insert(tableName, rec, replace = false) { const keys = Object.keys(rec); if (keys.length === 0) { - log.error("Can't insert empty object into table " + tableName); + log.error(`Can't insert empty object into table ${tableName}`); return; } const columns = keys.join(", "); const questionMarks = keys.map(p => "?").join(", "); - const query = "INSERT " + (replace ? "OR REPLACE" : "") + " INTO " + tableName + "(" + columns + ") VALUES (" + questionMarks + ")"; + const query = `INSERT + ${replace ? "OR REPLACE" : ""} INTO + ${tableName} + ( + ${columns} + ) + VALUES (${questionMarks})`; const res = execute(query, Object.values(rec)); @@ -77,13 +83,13 @@ function replace(tableName, rec) { function upsert(tableName, primaryKey, rec) { const keys = Object.keys(rec); if (keys.length === 0) { - log.error("Can't upsert empty object into table " + tableName); + log.error(`Can't upsert empty object into table ${tableName}`); return; } const columns = keys.join(", "); - const questionMarks = keys.map(colName => "@" + colName).join(", "); + const questionMarks = keys.map(colName => `@${colName}`).join(", "); const updateMarks = keys.map(colName => `${colName} = @${colName}`).join(", "); @@ -300,7 +306,7 @@ function fillParamList(paramIds, truncate = true) { } // doing it manually to avoid this showing up on the sloq query list - const s = stmt(`INSERT INTO param_list VALUES ` + paramIds.map(paramId => `(?)`).join(','), paramIds); + const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`, paramIds); s.run(paramIds); } diff --git a/docs/frontend_api/FrontendScriptApi.html b/docs/frontend_api/FrontendScriptApi.html index 4b296420c..83f1f1a75 100644 --- a/docs/frontend_api/FrontendScriptApi.html +++ b/docs/frontend_api/FrontendScriptApi.html @@ -1262,7 +1262,7 @@ -CreateOrUpdateLauncherOptions +AddButtonToToolbarOptions @@ -1301,7 +1301,7 @@ -
Deprecated:
+
Deprecated:
@@ -1452,7 +1452,7 @@
Source:
@@ -1591,7 +1591,7 @@
Source:
@@ -1799,7 +1799,7 @@
Source:
@@ -2163,7 +2163,7 @@
Source:
@@ -2296,7 +2296,7 @@
Source:
@@ -2406,7 +2406,7 @@
Source:
@@ -2512,7 +2512,7 @@
Source:
@@ -2618,7 +2618,7 @@
Source:
@@ -2728,7 +2728,7 @@
Source:
@@ -2839,7 +2839,7 @@ implementation of actual widget type.
Source:
@@ -2943,7 +2943,7 @@ implementation of actual widget type.
Source:
@@ -3051,7 +3051,7 @@ implementation of actual widget type.
Source:
@@ -3219,7 +3219,7 @@ implementation of actual widget type.
Source:
@@ -3356,7 +3356,7 @@ implementation of actual widget type.
Source:
@@ -3513,7 +3513,7 @@ implementation of actual widget type.
Source:
@@ -3668,7 +3668,7 @@ implementation of actual widget type.
Source:
@@ -3775,7 +3775,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -3930,7 +3930,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4086,7 +4086,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4287,7 +4287,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4393,7 +4393,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4548,7 +4548,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4703,7 +4703,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4853,7 +4853,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5342,7 +5342,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5450,7 +5450,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5606,7 +5606,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5762,7 +5762,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5899,7 +5899,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6053,7 +6053,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6139,7 +6139,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6276,7 +6276,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6437,7 +6437,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6545,7 +6545,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6683,7 +6683,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6839,7 +6839,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6994,7 +6994,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7145,7 +7145,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7282,7 +7282,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7419,7 +7419,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7579,7 +7579,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7739,7 +7739,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7831,7 +7831,7 @@ Typical use case is when new note has been created, we should wait until it is s
Source:
diff --git a/docs/frontend_api/entities_note_short.js.html b/docs/frontend_api/entities_note_short.js.html index d07ab3507..32fef2a2e 100644 --- a/docs/frontend_api/entities_note_short.js.html +++ b/docs/frontend_api/entities_note_short.js.html @@ -147,7 +147,7 @@ class NoteShort { async getContent() { // we're not caching content since these objects are in froca and as such pretty long lived - const note = await server.get("notes/" + this.noteId); + const note = await server.get(`notes/${this.noteId}`); return note.content; } @@ -372,7 +372,7 @@ class NoteShort { isInHoistedSubTree: path.includes(hoistedNotePath), isArchived: path.find(noteId => froca.notes[noteId].hasLabel('archived')), isSearch: path.find(noteId => froca.notes[noteId].type === 'search'), - isHidden: path.includes("hidden") + isHidden: path.includes('_hidden') })); notePaths.sort((a, b) => { @@ -454,7 +454,7 @@ class NoteShort { else if (this.noteId === 'root') { return "bx bx-chevrons-right"; } - if (this.noteId === 'share') { + if (this.noteId === '_share') { return "bx bx-share-alt"; } else if (this.type === 'text') { @@ -841,7 +841,7 @@ class NoteShort { return await bundleService.getAndExecuteBundle(this.noteId); } else if (env === "backend") { - return await server.post('script/run/' + this.noteId); + const resp = await server.post(`script/run/${this.noteId}`); } else { throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`); @@ -860,7 +860,7 @@ class NoteShort { continue; } - if (parentNote.noteId === 'share' || parentNote.isShared()) { + if (parentNote.noteId === '_share' || parentNote.isShared()) { return true; } } @@ -873,7 +873,7 @@ class NoteShort { } isLaunchBarConfig() { - return this.type === 'launcher' || ['lbRoot', 'lbAvailableLaunchers', 'lbVisibleLaunchers'].includes(this.noteId); + return this.type === 'launcher' || ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(this.noteId); } isOptions() { diff --git a/docs/frontend_api/global.html b/docs/frontend_api/global.html index ebe94b12e..70445bbc2 100644 --- a/docs/frontend_api/global.html +++ b/docs/frontend_api/global.html @@ -194,7 +194,7 @@ -

CreateOrUpdateLauncherOptions

+

AddButtonToToolbarOptions

diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html index 2a2bf55af..488f338be 100644 --- a/docs/frontend_api/services_frontend_script_api.js.html +++ b/docs/frontend_api/services_frontend_script_api.js.html @@ -134,7 +134,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain }; /** - * @typedef {Object} CreateOrUpdateLauncherOptions + * @typedef {Object} AddButtonToToolbarOptions * @property {string} [id] - id of the button, used to identify the old instances of this button to be replaced * ID is optional because of BC, but not specifying it is deprecated. ID can be alphanumeric only. * @property {string} title @@ -146,10 +146,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * Adds a new launcher to the launchbar. If the launcher (id) already exists, it will be updated. * - * @deprecated you can now create/modify launchers in the - * @param {CreateOrUpdateLauncherOptions} opts + * @deprecated you can now create/modify launchers in the top-left Menu -> Configure Launchbar + * @param {AddButtonToToolbarOptions} opts */ this.addButtonToToolbar = async opts => { + console.warn("api.addButtonToToolbar() has been deprecated since v0.58 and may be removed in the future. Use Menu -> Configure Launchbar to create/update launchers instead."); + const {action, ...reqBody} = opts; reqBody.action = action.toString(); @@ -163,7 +165,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain return params.map(p => { if (typeof p === "function") { - return "!@#Function: " + p.toString(); + return `!@#Function: ${p.toString()}`; } else { return p; @@ -199,7 +201,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain return ret.executionResult; } else { - throw new Error("server error: " + ret.error); + throw new Error(`server error: ${ret.error}`); } }; @@ -590,7 +592,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain this.log = message => { const {noteId} = this.startNote; - message = utils.now() + ": " + message; + message = `${utils.now()}: ${message}`; console.log(`Script ${noteId}: ${message}`); diff --git a/docs/frontend_api/widgets_collapsible_widget.js.html b/docs/frontend_api/widgets_collapsible_widget.js.html index b5871ed12..7df84e660 100644 --- a/docs/frontend_api/widgets_collapsible_widget.js.html +++ b/docs/frontend_api/widgets_collapsible_widget.js.html @@ -48,7 +48,7 @@ export default class CollapsibleWidget extends NoteContextAwareWidget { doRender() { this.$widget = $(WIDGET_TPL); this.contentSized(); - this.$widget.find('[data-target]').attr('data-target', "#" + this.componentId); + this.$widget.find('[data-target]').attr('data-target', `#${this.componentId}`); this.$bodyWrapper = this.$widget.find('.body-wrapper'); this.$bodyWrapper.attr('id', this.componentId); // for toggle to work we need id diff --git a/src/becca/entities/attribute.js b/src/becca/entities/attribute.js index 834631add..045092583 100644 --- a/src/becca/entities/attribute.js +++ b/src/becca/entities/attribute.js @@ -89,11 +89,15 @@ class Attribute extends AbstractEntity { validate() { if (!["label", "relation"].includes(this.type)) { - throw new Error(`Invalid attribute type '${this.type}' in attribute '${this.attributeId}'`); + throw new Error(`Invalid attribute type '${this.type}' in attribute '${this.attributeId}' of note '${this.noteId}'`); } if (!this.name?.trim()) { - throw new Error(`Invalid empty name in attribute '${this.attributeId}'`); + throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`); + } + + if (this.type === 'relation' && !(this.value in this.becca.notes)) { + throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it target not existing note '${this.value}'.`); } } @@ -176,11 +180,7 @@ class Attribute extends AbstractEntity { beforeSaving() { this.validate(); - if (this.type === 'relation') { - if (!(this.value in this.becca.notes)) { - throw new Error(`Cannot save relation '${this.name}' since it target not existing note '${this.value}'.`); - } - } else if (!this.value) { + if (!this.value) { // null value isn't allowed this.value = ""; } diff --git a/src/becca/entities/note.js b/src/becca/entities/note.js index c6dabba29..787c6f4f8 100644 --- a/src/becca/entities/note.js +++ b/src/becca/entities/note.js @@ -1124,7 +1124,7 @@ class Note extends AbstractEntity { const attributes = this.getOwnedAttributes(); const attr = attributes.find(attr => attr.type === type && attr.name === name); - value = value !== null && value !== undefined ? value.toString() : ""; + value = value?.toString() || ""; if (attr) { if (attr.value !== value) { diff --git a/src/public/app/entities/note_short.js b/src/public/app/entities/note_short.js index dcd026edb..65ae54d7f 100644 --- a/src/public/app/entities/note_short.js +++ b/src/public/app/entities/note_short.js @@ -813,7 +813,7 @@ class NoteShort { return await bundleService.getAndExecuteBundle(this.noteId); } else if (env === "backend") { - return await server.post(`script/run/${this.noteId}`); + const resp = await server.post(`script/run/${this.noteId}`); } else { throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`); diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index 9cbceff34..4792810db 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -106,7 +106,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain }; /** - * @typedef {Object} CreateOrUpdateLauncherOptions + * @typedef {Object} AddButtonToToolbarOptions * @property {string} [id] - id of the button, used to identify the old instances of this button to be replaced * ID is optional because of BC, but not specifying it is deprecated. ID can be alphanumeric only. * @property {string} title @@ -119,7 +119,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * Adds a new launcher to the launchbar. If the launcher (id) already exists, it will be updated. * * @deprecated you can now create/modify launchers in the top-left Menu -> Configure Launchbar - * @param {CreateOrUpdateLauncherOptions} opts + * @param {AddButtonToToolbarOptions} opts */ this.addButtonToToolbar = async opts => { console.warn("api.addButtonToToolbar() has been deprecated since v0.58 and may be removed in the future. Use Menu -> Configure Launchbar to create/update launchers instead."); diff --git a/src/public/app/services/server.js b/src/public/app/services/server.js index 783fe598b..d23b1e90d 100644 --- a/src/public/app/services/server.js +++ b/src/public/app/services/server.js @@ -105,21 +105,23 @@ async function call(method, url, data, headers = {}) { async function reportError(method, url, statusCode, response) { const toastService = (await import("./toast.js")).default; + let message = response; if (typeof response === 'string') { try { response = JSON.parse(response); + message = response.message; } - catch (e) { throw e;} + catch (e) {} } if ([400, 404].includes(statusCode) && response && typeof response === 'object') { - toastService.showError(response.message); + toastService.showError(message); throw new ValidationError(response); } else { - const message = `Error when calling ${method} ${url}: ${statusCode} - ${response}`; - toastService.showError(message); - toastService.throwError(message); + const title = `${statusCode} ${method} ${url}`; + toastService.showErrorTitleAndMessage(title, message); + toastService.throwError(`${title} - ${message}`); } } diff --git a/src/public/app/services/toast.js b/src/public/app/services/toast.js index dd7d05c48..09c17ce7f 100644 --- a/src/public/app/services/toast.js +++ b/src/public/app/services/toast.js @@ -84,6 +84,18 @@ function showError(message, delay = 10000) { }); } +function showErrorTitleAndMessage(title, message, delay = 10000) { + console.log(utils.now(), "error: ", message); + + toast({ + title: title, + icon: 'alert', + message: message, + autohide: true, + delay + }); +} + function throwError(message) { ws.logError(message); @@ -93,6 +105,7 @@ function throwError(message) { export default { showMessage, showError, + showErrorTitleAndMessage, showAndLogError, throwError, showPersistent, diff --git a/src/public/app/widgets/buttons/launcher/note_launcher.js b/src/public/app/widgets/buttons/launcher/note_launcher.js index 38f3ca2d2..956d03f38 100644 --- a/src/public/app/widgets/buttons/launcher/note_launcher.js +++ b/src/public/app/widgets/buttons/launcher/note_launcher.js @@ -55,7 +55,7 @@ export default class NoteLauncher extends AbstractLauncher { } getTargetNoteId() { - const targetNoteId = this.launcherNote.getRelationValue('targetNote'); + const targetNoteId = this.launcherNote.getRelationValue('target'); if (!targetNoteId) { dialogService.info("This launcher doesn't define target note."); diff --git a/src/public/app/widgets/floating_buttons/floating_buttons.js b/src/public/app/widgets/floating_buttons/floating_buttons.js index 42f8eaef2..1ba2d57cd 100644 --- a/src/public/app/widgets/floating_buttons/floating_buttons.js +++ b/src/public/app/widgets/floating_buttons/floating_buttons.js @@ -20,7 +20,7 @@ const TPL = ` margin-left: 10px; } - .floating-buttons-children > button { + .floating-buttons-children > button, .floating-buttons-children .floating-button { font-size: 150%; padding: 5px 10px 4px 10px; width: 40px; @@ -33,7 +33,7 @@ const TPL = ` justify-content: space-around; } - .floating-buttons-children > button:hover { + .floating-buttons-children > button:hover, .floating-buttons-children .floating-button:hover { text-decoration: none; border-color: var(--button-border-color); } diff --git a/src/public/app/widgets/ribbon_widgets/script_executor.js b/src/public/app/widgets/ribbon_widgets/script_executor.js index d4c49ee3c..892897b3a 100644 --- a/src/public/app/widgets/ribbon_widgets/script_executor.js +++ b/src/public/app/widgets/ribbon_widgets/script_executor.js @@ -8,6 +8,10 @@ const TPL = ` padding: 12px; color: var(--muted-text-color); } + + .execute-description { + margin-bottom: 10px; + }
@@ -52,7 +56,7 @@ export default class ScriptExecutorWidget extends NoteContextAwareWidget { this.$executeButton.text(executeTitle); this.$executeButton.attr('title', executeTitle); - keyboardActionService.updateDisplayedShortcuts(this.$widget);console.trace("ZZZ"); + keyboardActionService.updateDisplayedShortcuts(this.$widget); const executeDescription = note.getLabelValue('executeDescription'); diff --git a/src/public/app/widgets/spacer.js b/src/public/app/widgets/spacer.js index 9281853bb..173958e7a 100644 --- a/src/public/app/widgets/spacer.js +++ b/src/public/app/widgets/spacer.js @@ -1,4 +1,6 @@ import BasicWidget from "./basic_widget.js"; +import contextMenu from "../menus/context_menu.js"; +import appContext from "../components/app_context.js"; const TPL = `
`; @@ -15,5 +17,20 @@ export default class SpacerWidget extends BasicWidget { this.$widget.css("flex-basis", this.baseSize); this.$widget.css("flex-grow", this.growthFactor); this.$widget.css("flex-shrink", 1000); + + this.$widget.on("contextmenu", e => { + this.$widget.tooltip("hide"); + + contextMenu.show({ + x: e.pageX, + y: e.pageY, + items: [ + {title: "Configure Launchbar", command: "showLaunchBarSubtree", uiIcon: "bx bx-sidebar"} + ], + selectMenuItemHandler: ({command}) => { + appContext.triggerCommand(command); + } + }); + }); } } diff --git a/src/routes/api/special_notes.js b/src/routes/api/special_notes.js index 04e41e22b..5ed6e0656 100644 --- a/src/routes/api/special_notes.js +++ b/src/routes/api/special_notes.js @@ -67,7 +67,10 @@ function getHoistedNote() { } function createLauncher(req) { - return specialNotesService.createLauncher(req.params.parentNoteId, req.params.launcherType); + return specialNotesService.createLauncher({ + parentNoteId: req.params.parentNoteId, + launcherType: req.params.launcherType + }); } function resetLauncher(req) { diff --git a/src/routes/routes.js b/src/routes/routes.js index 03d7d1ee5..080cd3b14 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -450,7 +450,9 @@ function handleException(e, method, path, res) { }); } else { res.status(500) - .send(e.message); + .json({ + message: e.message + }); } } diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js index 238e604a8..15b885a77 100644 --- a/src/services/backend_script_api.js +++ b/src/services/backend_script_api.js @@ -16,6 +16,8 @@ const SearchContext = require("./search/search_context"); const becca = require("../becca/becca"); const ws = require("./ws"); const SpacedUpdate = require("./spaced_update"); +const specialNotesService = require("./special_notes"); +const branchService = require("./branches.js"); /** * This is the main backend API interface for scripts. It's published in the local "api" object. @@ -450,13 +452,93 @@ function BackendScriptApi(currentNote, apiParams) { * @method * @deprecated - this is now no-op since all the changes should be gracefully handled per widget */ - this.refreshTree = () => {}; + this.refreshTree = () => { + console.warn("api.refreshTree() is a NO-OP and can be removed from your script.") + }; /** * @return {{syncVersion, appVersion, buildRevision, dbVersion, dataDirectory, buildDate}|*} - object representing basic info about running Trilium version */ this.getAppInfo = () => appInfo + /** + * @typedef {Object} CreateOrUpdateLauncher + * @property {string} id - id of the launcher, only alphanumeric at least 6 characters long + * @property {string} type - one of + * * "note" - activating the launcher will navigate to the target note (specified in targetNoteId param) + * * "script" - activating the launcher will execute the script (specified in scriptNoteId param) + * * "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param) + * @property {string} title + * @property {boolean} [isVisible=false] - if true, will be created in the "Visible launchers", otherwise in "Available launchers" + * @property {string} [icon] - name of the boxicon to be used (e.g. "bx-time") + * @property {string} [keyboardShortcut] - will activate the target note/script upon pressing, e.g. "ctrl+e" + * @property {string} [targetNoteId] - for type "note" + * @property {string} [scriptNoteId] - for type "script" + * @property {string} [widgetNoteId] - for type "customWidget" + */ + + /** + * Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated. + * + * @param {CreateOrUpdateLauncher} opts + */ + this.createOrUpdateLauncher = opts => { + if (!opts.id) { throw new Error("ID is a mandatory parameter for api.createOrUpdateLauncher(opts)"); } + if (!opts.id.match(/[a-z0-9]{6,1000}/i)) { throw new Error(`ID must be an alphanumeric string at least 6 characters long.`); } + if (!opts.type) { throw new Error("Launcher Type is a mandatory parameter for api.createOrUpdateLauncher(opts)"); } + if (!["note", "script", "customWidget"].includes(opts.type)) { throw new Error(`Given launcher type '${opts.type}'`); } + if (!opts.title?.trim()) { throw new Error("Title is a mandatory parameter for api.createOrUpdateLauncher(opts)"); } + if (opts.type === 'note' && !opts.targetNoteId) { throw new Error("targetNoteId is mandatory for launchers of type 'note'"); } + if (opts.type === 'script' && !opts.scriptNoteId) { throw new Error("scriptNoteId is mandatory for launchers of type 'script'"); } + if (opts.type === 'customWidget' && !opts.widgetNoteId) { throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'"); } + + const parentNoteId = !!opts.isVisible ? '_lbVisibleLaunchers' : '_lbAvailableLaunchers'; + const actualId = 'al_' + opts.id; + + const launcherNote = + becca.getNote(opts.id) || + specialNotesService.createLauncher({ + id: actualId, + parentNoteId: parentNoteId, + launcherType: opts.type, + }).note; + + if (launcherNote.title !== opts.title) { + launcherNote.title = opts.title; + launcherNote.save(); + } + + if (launcherNote.getParentBranches().length === 1) { + const branch = launcherNote.getParentBranches()[0]; + + if (branch.parentNoteId !== parentNoteId) { + branchService.moveBranchToNote(branch, parentNoteId); + } + } + + if (opts.type === 'note') { + launcherNote.setRelation('target', opts.targetNoteId); + } else if (opts.type === 'script') { + launcherNote.setRelation('script', opts.scriptNoteId); + } else if (opts.type === 'customWidget') { + launcherNote.setRelation('widget', opts.widgetNoteId); + } else { + throw new Error(`Unrecognized launcher type '${opts.type}'`); + } + + if (opts.keyboardShortcut) { + launcherNote.setLabel('keyboardShortcut', opts.keyboardShortcut); + } else { + launcherNote.removeLabel('keyboardShortcut'); + } + + if (opts.icon) { + launcherNote.setLabel('iconClass', `bx ${opts.icon}`); + } else { + launcherNote.removeLabel('keyboardShortcut'); + } + }; + /** * This object contains "at your risk" and "no BC guarantees" objects for advanced use cases. * diff --git a/src/services/hidden_subtree.js b/src/services/hidden_subtree.js index b0c397948..eb125b138 100644 --- a/src/services/hidden_subtree.js +++ b/src/services/hidden_subtree.js @@ -96,7 +96,7 @@ const HIDDEN_SUBTREE_DEFINITION = { attributes: [ { type: 'relation', name: 'template', value: LBTPL_BASE }, { type: 'label', name: 'launcherType', value: 'note' }, - { type: 'label', name: 'relation:targetNote', value: 'promoted' }, + { type: 'label', name: 'relation:target', value: 'promoted' }, { type: 'label', name: 'relation:hoistedNote', value: 'promoted' }, { type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' }, { type: 'label', name: 'docName', value: 'launchbar_note_launcher' } @@ -271,7 +271,7 @@ function checkHiddenSubtreeRecursively(parentNoteId, item) { attrs.push({ type: 'label', name: 'builtinWidget', value: item.builtinWidget }); } else if (item.targetNoteId) { attrs.push({ type: 'relation', name: 'template', value: LBTPL_NOTE_LAUNCHER }); - attrs.push({ type: 'relation', name: 'targetNote', value: item.targetNoteId }); + attrs.push({ type: 'relation', name: 'target', value: item.targetNoteId }); } else { throw new Error(`No action defined for launcher ${JSON.stringify(item)}`); } diff --git a/src/services/special_notes.js b/src/services/special_notes.js index 40d4647eb..dbaf2ce54 100644 --- a/src/services/special_notes.js +++ b/src/services/special_notes.js @@ -144,9 +144,10 @@ function getHoistedNote() { return becca.getNote(cls.getHoistedNoteId()); } -function createScriptLauncher(parentNoteId, forceNoteId = null) { +function createScriptLauncher(parentNoteId, forceId = null) { const note = noteService.createNewNote({ - noteId: forceNoteId, + noteId: forceId, + branchId: forceId, title: "Script Launcher", type: 'launcher', content: '', @@ -157,11 +158,13 @@ function createScriptLauncher(parentNoteId, forceNoteId = null) { return note; } -function createLauncher(parentNoteId, launcherType) { +function createLauncher({parentNoteId, launcherType, id}) { let note; if (launcherType === 'note') { note = noteService.createNewNote({ + noteId: id, + branchId: id, title: "Note Launcher", type: 'launcher', content: '', @@ -170,9 +173,11 @@ function createLauncher(parentNoteId, launcherType) { note.addRelation('template', LBTPL_NOTE_LAUNCHER); } else if (launcherType === 'script') { - note = createScriptLauncher(parentNoteId); + note = createScriptLauncher(parentNoteId, id); } else if (launcherType === 'customWidget') { note = noteService.createNewNote({ + noteId: id, + branchId: id, title: "Widget Launcher", type: 'launcher', content: '', @@ -182,6 +187,8 @@ function createLauncher(parentNoteId, launcherType) { note.addRelation('template', LBTPL_CUSTOM_WIDGET); } else if (launcherType === 'spacer') { note = noteService.createNewNote({ + noteId: id, + branchId: id, title: "Spacer", type: 'launcher', content: '', @@ -233,12 +240,14 @@ function resetLauncher(noteId) { * could mess up the layout - e.g. the sync status being below. */ function createOrUpdateScriptLauncherFromApi(opts) { - const launcherId = opts.id || (`tb${opts.title.replace(/[^[a-z0-9]/gi, "")}`); + if (opts.id && !/^[a-z0-9]+$/i.test(opts.id)) { + throw new Error(`Launcher ID can be alphanumeric only, '${opts.id}' given`); + } + + const launcherId = opts.id || (`tb_${opts.title.toLowerCase().replace(/[^[a-z0-9]/gi, "")}`); if (!opts.title) { throw new Error("Title is mandatory property to create or update a launcher."); - } else if (!/^[a-z0-9]+$/i.test(launcherId)) { - throw new Error(`Launcher ID can be alphanumeric only, '${launcherId}' given`); } const launcherNote = becca.getNote(launcherId)