mirror of
https://github.com/zadam/trilium.git
synced 2025-11-10 15:25:51 +01:00
backend API to create a launcher
This commit is contained in:
@@ -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 = "";
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -450,7 +450,9 @@ function handleException(e, method, path, res) {
|
||||
});
|
||||
} else {
|
||||
res.status(500)
|
||||
.send(e.message);
|
||||
.json({
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user