diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js
index 0e375a209..64302d2fb 100644
--- a/src/public/app/layouts/desktop_layout.js
+++ b/src/public/app/layouts/desktop_layout.js
@@ -79,6 +79,7 @@ import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
import MermaidExportButton from "../widgets/floating_buttons/mermaid_export_button.js";
import EditableCodeButtonsWidget from "../widgets/type_widgets/editable_code_buttons.js";
+import ApiLogWidget from "../widgets/api_log.js";
export default class DesktopLayout {
constructor(customWidgets) {
@@ -197,6 +198,7 @@ export default class DesktopLayout {
.child(new SqlResultWidget())
)
.child(new EditableCodeButtonsWidget())
+ .child(new ApiLogWidget())
.child(new FindWidget())
.child(
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC
diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js
index 5ae0eaa05..6036c9c5b 100644
--- a/src/public/app/services/frontend_script_api.js
+++ b/src/public/app/services/frontend_script_api.js
@@ -13,6 +13,7 @@ import appContext from "./app_context.js";
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
import NoteContextCachingWidget from "../widgets/note_context_caching_widget.js";
import BasicWidget from "../widgets/basic_widget.js";
+import SpacedUpdate from "./spaced_update.js";
/**
* This is the main frontend API interface for scripts. It's published in the local "api" object.
@@ -594,6 +595,33 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @returns {string} random string
*/
this.randomString = utils.randomString;
+
+ this.logMessages = {};
+ this.logSpacedUpdates = {};
+
+ /**
+ * Log given message to the log pane in UI
+ *
+ * @param message
+ */
+ this.log = message => {
+ const {noteId} = this.startNote;
+
+ message = utils.now() + ": " + message;
+
+ console.log(`Script ${noteId}: ${message}`);
+
+ this.logMessages[noteId] = this.logMessages[noteId] || [];
+ this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
+ const messages = this.logMessages[noteId];
+ this.logMessages[noteId] = [];
+
+ appContext.triggerEvent("apiLogMessages", {noteId, messages});
+ }, 100);
+
+ this.logMessages[noteId].push(message);
+ this.logSpacedUpdates[noteId].scheduleUpdate();
+ };
}
export default FrontendScriptApi;
diff --git a/src/public/app/services/ws.js b/src/public/app/services/ws.js
index 113d1f0ad..6dd34c1c6 100644
--- a/src/public/app/services/ws.js
+++ b/src/public/app/services/ws.js
@@ -3,6 +3,7 @@ import toastService from "./toast.js";
import server from "./server.js";
import options from "./options.js";
import frocaUpdater from "./froca_updater.js";
+import appContext from "./app_context.js";
const messageHandlers = [];
@@ -118,6 +119,9 @@ async function handleMessage(event) {
else if (message.type === 'consistency-checks-failed') {
toastService.showError("Consistency checks failed! See logs for details.", 50 * 60000);
}
+ else if (message.type === 'api-log-messages') {
+ appContext.triggerEvent("apiLogMessages", {noteId: message.noteId, messages: message.messages});
+ }
}
let entityChangeIdReachedListeners = [];
diff --git a/src/public/app/widgets/api_log.js b/src/public/app/widgets/api_log.js
new file mode 100644
index 000000000..72d12691a
--- /dev/null
+++ b/src/public/app/widgets/api_log.js
@@ -0,0 +1,54 @@
+import NoteContextAwareWidget from "./note_context_aware_widget.js";
+
+const TPL = `
+
`;
+
+export default class ApiLogWidget extends NoteContextAwareWidget {
+ isEnabled() {
+ return this.note
+ && this.note.mime.startsWith('application/javascript;env=')
+ && super.isEnabled();
+ }
+
+ doRender() {
+ this.$widget = $(TPL);
+ this.$widget.addClass("hidden-api-log");
+
+ this.$logContainer = this.$widget.find('.api-log-container');
+ }
+
+ async refreshWithNote(note) {
+ this.$logContainer.empty();
+ }
+
+ apiLogMessagesEvent({messages, noteId}) {
+ if (!this.isNote(noteId)) {
+ return;
+ }
+
+ this.$widget.removeClass("hidden-api-log");
+
+ for (const message of messages) {
+ this.$logContainer.append(message).append($("
"));
+ }
+ }
+}
diff --git a/src/public/app/widgets/script_log.js b/src/public/app/widgets/script_log.js
deleted file mode 100644
index 681a14d48..000000000
--- a/src/public/app/widgets/script_log.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import NoteContextAwareWidget from "./note_context_aware_widget.js";
-
-const TPL = `
-`;
-
-export default class ScriptLogWidget extends NoteContextAwareWidget {
- isEnabled() {
- return this.note
- && this.note.mime.startsWith('application/javascript;env=')
- && super.isEnabled();
- }
-
- doRender() {
- this.$widget = $(TPL);
-
- this.$logContainer = this.$widget.find('.script-log-container');
- }
-}
diff --git a/src/public/app/widgets/type_widgets/editable_code_buttons.js b/src/public/app/widgets/type_widgets/editable_code_buttons.js
index 6386fde43..006ace51f 100644
--- a/src/public/app/widgets/type_widgets/editable_code_buttons.js
+++ b/src/public/app/widgets/type_widgets/editable_code_buttons.js
@@ -87,4 +87,10 @@ export default class EditableCodeButtonsWidget extends NoteContextAwareWidget {
this.$openTriliumApiDocsButton.toggle(note.mime.startsWith('application/javascript;env='));
}
+
+ async noteTypeMimeChangedEvent({noteId}) {
+ if (this.isNote(noteId)) {
+ await this.refresh();
+ }
+ }
}
diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js
index c457bbaf6..0f4f378d8 100644
--- a/src/services/backend_script_api.js
+++ b/src/services/backend_script_api.js
@@ -14,6 +14,8 @@ const appInfo = require('./app_info');
const searchService = require('./search/services/search');
const SearchContext = require("./search/search_context");
const becca = require("../becca/becca");
+const ws = require("./ws");
+const SpacedUpdate = require("./spaced_update");
/**
* This is the main backend API interface for scripts. It's published in the local "api" object.
@@ -288,12 +290,34 @@ function BackendScriptApi(currentNote, apiParams) {
});
};
+ this.logMessages = {};
+ this.logSpacedUpdates = {};
+
/**
- * Log given message to trilium logs.
+ * Log given message to trilium logs and log pane in UI
*
* @param message
*/
- this.log = message => log.info(message);
+ this.log = message => {
+ log.info(message);
+
+ const {noteId} = this.startNote;
+
+ this.logMessages[noteId] = this.logMessages[noteId] || [];
+ this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
+ const messages = this.logMessages[noteId];
+ this.logMessages[noteId] = [];
+
+ ws.sendMessageToAllClients({
+ type: 'api-log-messages',
+ noteId,
+ messages
+ });
+ }, 100);
+
+ this.logMessages[noteId].push(message);
+ this.logSpacedUpdates[noteId].scheduleUpdate();
+ };
/**
* Returns root note of the calendar.
diff --git a/src/services/spaced_update.js b/src/services/spaced_update.js
new file mode 100644
index 000000000..d038a09db
--- /dev/null
+++ b/src/services/spaced_update.js
@@ -0,0 +1,67 @@
+class SpacedUpdate {
+ constructor(updater, updateInterval = 1000) {
+ this.updater = updater;
+ this.lastUpdated = Date.now();
+ this.changed = false;
+ this.updateInterval = updateInterval;
+ }
+
+ scheduleUpdate() {
+ if (!this.changeForbidden) {
+ this.changed = true;
+ setTimeout(() => this.triggerUpdate());
+ }
+ }
+
+ async updateNowIfNecessary() {
+ if (this.changed) {
+ this.changed = false; // optimistic...
+
+ try {
+ await this.updater();
+ }
+ catch (e) {
+ this.changed = true;
+
+ throw e;
+ }
+ }
+ }
+
+ isAllSavedAndTriggerUpdate() {
+ const allSaved = !this.changed;
+
+ this.updateNowIfNecessary();
+
+ return allSaved;
+ }
+
+ triggerUpdate() {
+ if (!this.changed) {
+ return;
+ }
+
+ if (Date.now() - this.lastUpdated > this.updateInterval) {
+ this.updater();
+ this.lastUpdated = Date.now();
+ this.changed = false;
+ }
+ else {
+ // update not triggered but changes are still pending so we need to schedule another check
+ this.scheduleUpdate();
+ }
+ }
+
+ async allowUpdateWithoutChange(callback) {
+ this.changeForbidden = true;
+
+ try {
+ await callback();
+ }
+ finally {
+ this.changeForbidden = false;
+ }
+ }
+}
+
+module.exports = SpacedUpdate;
diff --git a/src/services/ws.js b/src/services/ws.js
index 0231b200d..4581e8942 100644
--- a/src/services/ws.js
+++ b/src/services/ws.js
@@ -67,7 +67,7 @@ function sendMessageToAllClients(message) {
const jsonStr = JSON.stringify(message);
if (webSocketServer) {
- if (message.type !== 'sync-failed') {
+ if (message.type !== 'sync-failed' && message.type !== 'api-log-messages') {
log.info("Sending message to all clients: " + jsonStr);
}