backend API to create a launcher

This commit is contained in:
zadam
2022-12-22 14:57:00 +01:00
parent 059c339c09
commit 8ec2547b4a
32 changed files with 947 additions and 173 deletions

View File

@@ -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 = "";
}

View File

@@ -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) {

View File

@@ -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}`);

View File

@@ -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.");

View File

@@ -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}`);
}
}

View File

@@ -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,

View File

@@ -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.");

View File

@@ -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);
}

View File

@@ -8,6 +8,10 @@ const TPL = `
padding: 12px;
color: var(--muted-text-color);
}
.execute-description {
margin-bottom: 10px;
}
</style>
<div class="execute-description"></div>
@@ -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');

View File

@@ -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 = `<div class="spacer"></div>`;
@@ -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);
}
});
});
}
}

View File

@@ -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) {

View File

@@ -450,7 +450,9 @@ function handleException(e, method, path, res) {
});
} else {
res.status(500)
.send(e.message);
.json({
message: e.message
});
}
}

View File

@@ -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.
*

View File

@@ -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)}`);
}

View File

@@ -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)