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:
+ Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - 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:
- you can now create/modify launchers in the
+ - Deprecated:
- you can now create/modify launchers in the top-left Menu -> Configure Launchbar
@@ -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
+
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)
|