Merge branch 'master' into next61

# Conflicts:
#	package-lock.json
#	src/public/app/services/note_content_renderer.js
#	src/public/app/widgets/note_tree.js
#	src/routes/routes.js
#	src/services/consistency_checks.js
#	src/services/notes.js
#	src/services/task_context.js
This commit is contained in:
zadam 2023-07-10 18:20:36 +02:00
commit b7f0fd2db3
41 changed files with 253 additions and 192 deletions

View File

@ -16,7 +16,7 @@ noBackup=false
# host=0.0.0.0 # host=0.0.0.0
# port setting is relevant only for web deployments, desktop builds run on a fixed port (changeable with TRILIUM_PORT environment variable) # port setting is relevant only for web deployments, desktop builds run on a fixed port (changeable with TRILIUM_PORT environment variable)
port=8080 port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). # true for TLS/SSL/HTTPS (secure), false for HTTP (insecure).
https=false https=false
# path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true # path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
certPath= certPath=

View File

@ -4,7 +4,7 @@ const fs = require("fs");
const dataDir = require("./src/services/data_dir"); const dataDir = require("./src/services/data_dir");
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8')); const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8'));
if (config.https) { if (config.Network.https) {
// built-in TLS (terminated by trilium) is not supported yet, PRs are welcome // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
// for reverse proxy terminated TLS this will works since config.https will be false // for reverse proxy terminated TLS this will works since config.https will be false
process.exit(0); process.exit(0);

View File

@ -667,7 +667,7 @@ class BNote extends AbstractBeccaEntity {
return this.ownedAttributes.filter(attr => attr.name === name); return this.ownedAttributes.filter(attr => attr.name === name);
} }
else { else {
return this.ownedAttributes.slice(); return this.ownedAttributes;
} }
} }

View File

@ -367,7 +367,7 @@ async function findSimilarNotes(noteId) {
* We want to improve the standing of notes which have been created in similar time to each other since * We want to improve the standing of notes which have been created in similar time to each other since
* there's a good chance they are related. * there's a good chance they are related.
* *
* But there's an exception - if they were created really close to each other (withing few seconds) then * But there's an exception - if they were created really close to each other (within few seconds) then
* they are probably part of the import and not created by hand - these OTOH should not benefit. * they are probably part of the import and not created by hand - these OTOH should not benefit.
*/ */
const {utcDateCreated} = candidateNote; const {utcDateCreated} = candidateNote;

View File

@ -231,7 +231,7 @@ paths:
schema: schema:
$ref: '#/components/schemas/EntityId' $ref: '#/components/schemas/EntityId'
get: get:
description: Returns note content idenfied by its ID description: Returns note content identified by its ID
operationId: getNoteContent operationId: getNoteContent
responses: responses:
'200': '200':
@ -241,7 +241,7 @@ paths:
schema: schema:
type: string type: string
put: put:
description: Updates note content idenfied by its ID description: Updates note content identified by its ID
operationId: putNoteContentById operationId: putNoteContentById
requestBody: requestBody:
description: html content of note description: html content of note

View File

@ -41,8 +41,8 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
async function initLabelValueAutocomplete({ $el, open, nameCallback }) { async function initLabelValueAutocomplete({ $el, open, nameCallback }) {
if ($el.hasClass("aa-input")) { if ($el.hasClass("aa-input")) {
// we reinit everytime because autocomplete seems to have a bug where it retains state from last // we reinit every time because autocomplete seems to have a bug where it retains state from last
// open even though the value was resetted // open even though the value was reset
$el.autocomplete('destroy'); $el.autocomplete('destroy');
} }

View File

@ -133,7 +133,7 @@ function initNoteAutocomplete($el, options) {
showRecentNotes($el); showRecentNotes($el);
// this will cause the click not give focus to the "show recent notes" button // this will cause the click not give focus to the "show recent notes" button
// this is important because otherwise input will lose focus immediatelly and not show the results // this is important because otherwise input will lose focus immediately and not show the results
return false; return false;
}); });

View File

@ -99,7 +99,7 @@ function parseSelectedHtml(selectedHtml) {
if (dom.length > 0 && dom[0].tagName && dom[0].tagName.match(/h[1-6]/i)) { if (dom.length > 0 && dom[0].tagName && dom[0].tagName.match(/h[1-6]/i)) {
const title = $(dom[0]).text(); const title = $(dom[0]).text();
// remove the title from content (only first occurence) // remove the title from content (only first occurrence)
const content = selectedHtml.replace(dom[0].outerHTML, ""); const content = selectedHtml.replace(dom[0].outerHTML, "");
return [title, content]; return [title, content];

View File

@ -161,7 +161,7 @@ class NoteListRenderer {
constructor($parent, parentNote, noteIds, showNotePath = false) { constructor($parent, parentNote, noteIds, showNotePath = false) {
this.$noteList = $(TPL); this.$noteList = $(TPL);
// note list must be added to the DOM immediatelly, otherwise some functionality scripting (canvas) won't work // note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work
$parent.empty(); $parent.empty();
this.parentNote = parentNote; this.parentNote = parentNote;

View File

@ -21,7 +21,7 @@ async function getHeaders(headers) {
} }
if (utils.isElectron()) { if (utils.isElectron()) {
// passing it explicitely here because of the electron HTTP bypass // passing it explicitly here because of the electron HTTP bypass
allHeaders.cookie = document.cookie; allHeaders.cookie = document.cookie;
} }

View File

@ -1,7 +1,7 @@
/** /**
* Fetch note with given ID from backend * Fetch note with given ID from backend
* *
* @param noteId of the given note to be fetched. If falsy, fetches current note. * @param noteId of the given note to be fetched. If false, fetches current note.
*/ */
async function fetchNote(noteId = null) { async function fetchNote(noteId = null) {
if (!noteId) { if (!noteId) {

View File

@ -26,7 +26,7 @@ export default class AbstractBulkAction {
} }
} }
// to be overriden // to be overridden
doRender() {} doRender() {}
async saveAction(data) { async saveAction(data) {

View File

@ -50,7 +50,7 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.$widget.find(".dropdown-menu").append(this.$dropdownContent); this.$widget.find(".dropdown-menu").append(this.$dropdownContent);
} }
// to be overriden // to be overridden
async dropdownShow() {} async dropdownShow() {}
hideDropdown() { hideDropdown() {

View File

@ -25,7 +25,7 @@ const TPL = `
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label title="Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediatelly and it won't be possible to undelete the notes."> <label title="Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediately and it won't be possible to undelete the notes.">
<input class="erase-notes" value="1" type="checkbox"> <input class="erase-notes" value="1" type="checkbox">
erase notes permanently (can't be undone), including all clones. This will force application reload. erase notes permanently (can't be undone), including all clones. This will force application reload.

View File

@ -10,20 +10,20 @@ import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js"; import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.js"; import OnClickButtonWidget from "./buttons/onclick_button.js";
const TPL = `<div class="highlists-list-widget"> const TPL = `<div class="highlights-list-widget">
<style> <style>
.highlists-list-widget { .highlights-list-widget {
padding: 10px; padding: 10px;
contain: none; contain: none;
overflow: auto; overflow: auto;
position: relative; position: relative;
} }
.highlists-list > ol { .highlights-list > ol {
padding-left: 20px; padding-left: 20px;
} }
.highlists-list li { .highlights-list li {
cursor: pointer; cursor: pointer;
margin-bottom: 3px; margin-bottom: 3px;
text-align: justify; text-align: justify;
@ -32,18 +32,18 @@ const TPL = `<div class="highlists-list-widget">
hyphens: auto; hyphens: auto;
} }
.highlists-list li:hover { .highlights-list li:hover {
font-weight: bold; font-weight: bold;
} }
.close-highlists-list { .close-highlights-list {
position: absolute; position: absolute;
top: 2px; top: 2px;
right: 0px; right: 0px;
} }
</style> </style>
<span class="highlists-list"></span> <span class="highlights-list"></span>
</div>`; </div>`;
export default class HighlightsListWidget extends RightPanelWidget { export default class HighlightsListWidget extends RightPanelWidget {
@ -55,61 +55,61 @@ export default class HighlightsListWidget extends RightPanelWidget {
} }
get widgetTitle() { get widgetTitle() {
return "Highlighted Text"; return "Highlights List";
} }
isEnabled() { isEnabled() {
return super.isEnabled() return super.isEnabled()
&& this.note.type === 'text' && this.note.type === 'text'
&& !this.noteContext.viewScope.highlightedTextTemporarilyHidden && !this.noteContext.viewScope.highlightsListTemporarilyHidden
&& this.noteContext.viewScope.viewMode === 'default'; && this.noteContext.viewScope.viewMode === 'default';
} }
async doRenderBody() { async doRenderBody() {
this.$body.empty().append($(TPL)); this.$body.empty().append($(TPL));
this.$highlightsList = this.$body.find('.highlists-list'); this.$highlightsList = this.$body.find('.highlights-list');
this.$body.find('.highlists-list-widget').append(this.closeHltButton.render()); this.$body.find('.highlights-list-widget').append(this.closeHltButton.render());
} }
async refreshWithNote(note) { async refreshWithNote(note) {
/* The reason for adding highlightedTextPreviousVisible is to record whether the previous state /* The reason for adding highlightsListPreviousVisible is to record whether the previous state
of the highlightedText is hidden or displayed, and then let it be displayed/hidden at the initial time. of the highlightsList is hidden or displayed, and then let it be displayed/hidden at the initial time.
If there is no such value, when the right panel needs to display toc but not highlighttext, If there is no such value, when the right panel needs to display toc but not highlighttext,
every time the note content is changed, highlighttext Widget will appear and then close immediately, every time the note content is changed, highlighttext Widget will appear and then close immediately,
because getHlt function will consume time */ because getHlt function will consume time */
if (this.noteContext.viewScope.highlightedTextPreviousVisible) { if (this.noteContext.viewScope.highlightsListPreviousVisible) {
this.toggleInt(true); this.toggleInt(true);
} else { } else {
this.toggleInt(false); this.toggleInt(false);
} }
const optionsHlt = JSON.parse(options.get('highlightedText')); const optionsHighlightsList = JSON.parse(options.get('highlightsList'));
if (note.isLabelTruthy('hideHighlightWidget') || !optionsHlt) { if (note.isLabelTruthy('hideHighlightWidget') || !optionsHighlightsList) {
this.toggleInt(false); this.toggleInt(false);
this.triggerCommand("reEvaluateRightPaneVisibility"); this.triggerCommand("reEvaluateRightPaneVisibility");
return; return;
} }
let $highlightsList = "", hltLiCount = -1; let $highlightsList = "", hlLiCount = -1;
// Check for type text unconditionally in case alwaysShowWidget is set // Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === 'text') { if (this.note.type === 'text') {
const {content} = await note.getNoteComplement(); const {content} = await note.getNoteComplement();
({$highlightsList, hltLiCount} = this.getHighlightList(content, optionsHlt)); ({$highlightsList, hlLiCount} = this.getHighlightList(content, optionsHighlightsList));
} }
this.$highlightsList.empty().append($highlightsList); this.$highlightsList.empty().append($highlightsList);
if (hltLiCount > 0) { if (hlLiCount > 0) {
this.toggleInt(true); this.toggleInt(true);
this.noteContext.viewScope.highlightedTextPreviousVisible = true; this.noteContext.viewScope.highlightsListPreviousVisible = true;
} else { } else {
this.toggleInt(false); this.toggleInt(false);
this.noteContext.viewScope.highlightedTextPreviousVisible = false; this.noteContext.viewScope.highlightsListPreviousVisible = false;
} }
this.triggerCommand("reEvaluateRightPaneVisibility"); this.triggerCommand("reEvaluateRightPaneVisibility");
} }
getHighlightList(content, optionsHlt) { getHighlightList(content, optionsHighlightsList) {
// matches a span containing background-color // matches a span containing background-color
const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi; const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi;
// matches a span containing color // matches a span containing color
@ -120,27 +120,27 @@ export default class HighlightsListWidget extends RightPanelWidget {
const regex4 = /<strong>[\s\S]*?<\/strong>/gi; const regex4 = /<strong>[\s\S]*?<\/strong>/gi;
// match underline // match underline
const regex5 = /<u>[\s\S]*?<\/u>/g; const regex5 = /<u>[\s\S]*?<\/u>/g;
// Possible values in optionsHlt '["bold","italic","underline","color","bgColor"]' // Possible values in optionsHighlightsList '["bold","italic","underline","color","bgColor"]'
// element priority span>i>strong>u // element priority span>i>strong>u
let findSubStr = "", combinedRegexStr = ""; let findSubStr = "", combinedRegexStr = "";
if (optionsHlt.includes("bgColor")) { if (optionsHighlightsList.includes("bgColor")) {
findSubStr += `,span[style*="background-color"]`; findSubStr += `,span[style*="background-color"]:not(section.include-note span[style*="background-color"])`;
combinedRegexStr += `|${regex1.source}`; combinedRegexStr += `|${regex1.source}`;
} }
if (optionsHlt.includes("color")) { if (optionsHighlightsList.includes("color")) {
findSubStr += `,span[style*="color"]`; findSubStr += `,span[style*="color"]:not(section.include-note span[style*="color"])`;
combinedRegexStr += `|${regex2.source}`; combinedRegexStr += `|${regex2.source}`;
} }
if (optionsHlt.includes("italic")) { if (optionsHighlightsList.includes("italic")) {
findSubStr += `,i`; findSubStr += `,i:not(section.include-note i)`;
combinedRegexStr += `|${regex3.source}`; combinedRegexStr += `|${regex3.source}`;
} }
if (optionsHlt.indexOf("bold")) { if (optionsHighlightsList.includes("bold")) {
findSubStr += `,strong`; findSubStr += `,strong:not(section.include-note strong)`;
combinedRegexStr += `|${regex4.source}`; combinedRegexStr += `|${regex4.source}`;
} }
if (optionsHlt.includes("underline")) { if (optionsHighlightsList.includes("underline")) {
findSubStr += `,u`; findSubStr += `,u:not(section.include-note u)`;
combinedRegexStr += `|${regex5.source}`; combinedRegexStr += `|${regex5.source}`;
} }
@ -148,7 +148,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`; combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`;
const combinedRegex = new RegExp(combinedRegexStr, 'gi'); const combinedRegex = new RegExp(combinedRegexStr, 'gi');
const $highlightsList = $("<ol>"); const $highlightsList = $("<ol>");
let prevEndIndex = -1, hltLiCount = 0; let prevEndIndex = -1, hlLiCount = 0;
for (let match = null, hltIndex = 0; ((match = combinedRegex.exec(content)) !== null); hltIndex++) { for (let match = null, hltIndex = 0; ((match = combinedRegex.exec(content)) !== null); hltIndex++) {
const subHtml = match[0]; const subHtml = match[0];
const startIndex = match.index; const startIndex = match.index;
@ -158,16 +158,18 @@ export default class HighlightsListWidget extends RightPanelWidget {
$highlightsList.children().last().append(subHtml); $highlightsList.children().last().append(subHtml);
} else { } else {
// TODO: can't be done with $(subHtml).text()? // TODO: can't be done with $(subHtml).text()?
const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim(); //Cant remember why regular expressions are used here, but modified to $(subHtml).text() works as expected
//const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim();
const hasText = $(subHtml).text().trim();
if (hasText) { if (hasText) {
$highlightsList.append( $highlightsList.append(
$('<li>') $('<li>')
.html(subHtml) .html(subHtml)
.on("click", () => this.jumpToHighlightedText(findSubStr, hltIndex)) .on("click", () => this.jumpToHighlightsList(findSubStr, hltIndex))
); );
hltLiCount++; hlLiCount++;
} else { } else {
// hide li if its text content is empty // hide li if its text content is empty
continue; continue;
@ -177,11 +179,11 @@ export default class HighlightsListWidget extends RightPanelWidget {
} }
return { return {
$highlightsList, $highlightsList,
hltLiCount hlLiCount
}; };
} }
async jumpToHighlightedText(findSubStr, itemIndex) { async jumpToHighlightsList(findSubStr, itemIndex) {
const isReadOnly = await this.noteContext.isReadOnly(); const isReadOnly = await this.noteContext.isReadOnly();
let targetElement; let targetElement;
if (isReadOnly) { if (isReadOnly) {
@ -224,7 +226,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
} }
async closeHltCommand() { async closeHltCommand() {
this.noteContext.viewScope.highlightedTextTemporarilyHidden = true; this.noteContext.viewScope.highlightsListTemporarilyHidden = true;
await this.refresh(); await this.refresh();
this.triggerCommand('reEvaluateRightPaneVisibility'); this.triggerCommand('reEvaluateRightPaneVisibility');
} }
@ -245,13 +247,13 @@ class CloseHltButton extends OnClickButtonWidget {
super(); super();
this.icon("bx-x") this.icon("bx-x")
.title("Close HighlightedTextWidget") .title("Close HighlightsListWidget")
.titlePlacement("bottom") .titlePlacement("bottom")
.onClick((widget, e) => { .onClick((widget, e) => {
e.stopPropagation(); e.stopPropagation();
widget.triggerCommand("closeHlt"); widget.triggerCommand("closeHlt");
}) })
.class("icon-action close-highlists-list"); .class("icon-action close-highlights-list");
} }
} }

View File

@ -9584,7 +9584,7 @@ const icons = [
"term": [ "term": [
"honor", "honor",
"honour", "honour",
"acheivement" "achievement"
] ]
}, },
{ {
@ -9595,7 +9595,7 @@ const icons = [
"term": [ "term": [
"honor", "honor",
"honour", "honour",
"acheivement" "achievement"
] ]
}, },
{ {

View File

@ -166,7 +166,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
generateColorFromString(str) { generateColorFromString(str) {
if (this.themeStyle === "dark") { if (this.themeStyle === "dark") {
str = `0${str}`; // magic lightening modifier str = `0${str}`; // magic lightning modifier
} }
let hash = 0; let hash = 0;

View File

@ -1116,7 +1116,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const note = froca.getNoteFromCache(ecAttr.noteId); const note = froca.getNoteFromCache(ecAttr.noteId);
if (note && note.getChildNoteIds().includes(ecAttr.value)) { if (note && note.getChildNoteIds().includes(ecAttr.value)) {
// there's a new /deleted imageLink betwen note and its image child - which can show/hide // there's a new /deleted imageLink between note and its image child - which can show/hide
// the image (if there is an imageLink relation between parent and child, // the image (if there is an imageLink relation between parent and child,
// then it is assumed to be "contained" in the note and thus does not have to be displayed in the tree) // then it is assumed to be "contained" in the note and thus does not have to be displayed in the tree)
noteIdsToReload.add(ecAttr.noteId); noteIdsToReload.add(ecAttr.noteId);

View File

@ -39,7 +39,7 @@ export default class AbstractSearchOption extends Component {
} }
} }
// to be overriden // to be overridden
doRender() {} doRender() {}
async deleteOption() { async deleteOption() {

View File

@ -2,6 +2,7 @@ import AbstractSearchOption from "./abstract_search_option.js";
import SpacedUpdate from "../../services/spaced_update.js"; import SpacedUpdate from "../../services/spaced_update.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import shortcutService from "../../services/shortcuts.js"; import shortcutService from "../../services/shortcuts.js";
import appContext from "../../components/app_context.js";
const TPL = ` const TPL = `
<tr> <tr>
@ -56,6 +57,7 @@ export default class SearchString extends AbstractSearchOption {
this.spacedUpdate = new SpacedUpdate(async () => { this.spacedUpdate = new SpacedUpdate(async () => {
const searchString = this.$searchString.val(); const searchString = this.$searchString.val();
appContext.lastSearchString = searchString;
await this.setAttribute('label', 'searchString', searchString); await this.setAttribute('label', 'searchString', searchString);
@ -84,6 +86,7 @@ export default class SearchString extends AbstractSearchOption {
} }
focusOnSearchDefinitionEvent() { focusOnSearchDefinitionEvent() {
this.$searchString.focus(); this.$searchString.val(appContext.lastSearchString).focus().select();
this.spacedUpdate.scheduleUpdate();
} }
} }

View File

@ -187,7 +187,7 @@ export default class TocWidget extends RightPanelWidget {
if (isReadOnly) { if (isReadOnly) {
const $container = await this.noteContext.getContentElement(); const $container = await this.noteContext.getContentElement();
const headingElement = $container.find(":header")[headingIndex]; const headingElement = $container.find(":header:not(section.include-note :header)")[headingIndex];
if (headingElement != null) { if (headingElement != null) {
headingElement.scrollIntoView({ behavior: "smooth" }); headingElement.scrollIntoView({ behavior: "smooth" });
@ -206,7 +206,7 @@ export default class TocWidget extends RightPanelWidget {
// navigate (note that the TOC rendering and other TOC // navigate (note that the TOC rendering and other TOC
// entries' navigation could be wrong too) // entries' navigation could be wrong too)
if (headingNode != null) { if (headingNode != null) {
$(textEditor.editing.view.domRoots.values().next().value).find(':header')[headingIndex].scrollIntoView({ $(textEditor.editing.view.domRoots.values().next().value).find(':header:not(section.include-note :header)')[headingIndex].scrollIntoView({
behavior: 'smooth' behavior: 'smooth'
}); });
} }

View File

@ -77,7 +77,7 @@ const TPL = `
* *
* Discussion of storing svg in the note: * Discussion of storing svg in the note:
* - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there. * - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there.
* - Con: The note will get bigger (~40-50%?), we will generate more bandwith. However, using trilium * - Con: The note will get bigger (~40-50%?), we will generate more bandwidth. However, using trilium
* desktop instance mitigates that issue. * desktop instance mitigates that issue.
* *
* Roadmap: * Roadmap:

View File

@ -7,7 +7,7 @@ import MaxContentWidthOptions from "./options/appearance/max_content_width.js";
import KeyboardShortcutsOptions from "./options/shortcuts.js"; import KeyboardShortcutsOptions from "./options/shortcuts.js";
import HeadingStyleOptions from "./options/text_notes/heading_style.js"; import HeadingStyleOptions from "./options/text_notes/heading_style.js";
import TableOfContentsOptions from "./options/text_notes/table_of_contents.js"; import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
import HighlightedTextOptions from "./options/text_notes/highlighted_text.js"; import HighlightsListOptions from "./options/text_notes/highlights_list.js";
import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js"; import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js"; import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js";
import WrapLinesOptions from "./options/code_notes/wrap_lines.js"; import WrapLinesOptions from "./options/code_notes/wrap_lines.js";
@ -63,7 +63,7 @@ const CONTENT_WIDGETS = {
_optionsTextNotes: [ _optionsTextNotes: [
HeadingStyleOptions, HeadingStyleOptions,
TableOfContentsOptions, TableOfContentsOptions,
HighlightedTextOptions, HighlightsListOptions,
TextAutoReadOnlySizeOptions TextAutoReadOnlySizeOptions
], ],
_optionsCodeNotes: [ _optionsCodeNotes: [

View File

@ -96,7 +96,7 @@ export default class EtapiOptions extends OptionsWidget {
.append($("<td>").append( .append($("<td>").append(
$('<span class="bx bx-pen token-table-button" title="Rename this token"></span>') $('<span class="bx bx-pen token-table-button" title="Rename this token"></span>')
.on("click", () => this.renameToken(token.etapiTokenId, token.name)), .on("click", () => this.renameToken(token.etapiTokenId, token.name)),
$('<span class="bx bx-trash token-table-button" title="Delete / deactive this token"></span>') $('<span class="bx bx-trash token-table-button" title="Delete / deactivate this token"></span>')
.on("click", () => this.deleteToken(token.etapiTokenId, token.name)) .on("click", () => this.deleteToken(token.etapiTokenId, token.name))
)) ))
); );

View File

@ -1,40 +0,0 @@
import OptionsWidget from "../options_widget.js";
const TPL = `
<div class="options-section">
<h4>Highlighted Text</h4>
<p>You can customize the highlighted text displayed in the right panel:</p>
</div>
<label><input type="checkbox" class="highlighted-text-check" value="bold"> Bold font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="italic"> Italic font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="underline"> Underlined font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="color"> Font with color &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="bgColor"> Font with background color &nbsp;</label>
</div>
</div>`;
export default class HighlightedTextOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$hlt = this.$widget.find("input.highlighted-text-check");
this.$hlt.on('change', () => {
const hltVals = this.$widget.find('input.highlighted-text-check[type="checkbox"]:checked').map(function () {
return this.value;
}).get();
this.updateOption('highlightedText', JSON.stringify(hltVals));
});
}
async optionsLoaded(options) {
const hltVals = JSON.parse(options.highlightedText);
this.$widget.find('input.highlighted-text-check[type="checkbox"]').each(function () {
if ($.inArray($(this).val(), hltVals) !== -1) {
$(this).prop("checked", true);
} else {
$(this).prop("checked", false);
}
});
}
}

View File

@ -0,0 +1,40 @@
import OptionsWidget from "../options_widget.js";
const TPL = `
<div class="options-section">
<h4>Highlights List</h4>
<p>You can customize the highlights list displayed in the right panel:</p>
</div>
<label><input type="checkbox" class="highlights-list-check" value="bold"> Bold font &nbsp;</label>
<label><input type="checkbox" class="highlights-list-check" value="italic"> Italic font &nbsp;</label>
<label><input type="checkbox" class="highlights-list-check" value="underline"> Underlined font &nbsp;</label>
<label><input type="checkbox" class="highlights-list-check" value="color"> Font with color &nbsp;</label>
<label><input type="checkbox" class="highlights-list-check" value="bgColor"> Font with background color &nbsp;</label>
</div>
</div>`;
export default class HighlightsListOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$hlt = this.$widget.find("input.highlights-list-check");
this.$hlt.on('change', () => {
const hltVals = this.$widget.find('input.highlights-list-check[type="checkbox"]:checked').map(function () {
return this.value;
}).get();
this.updateOption('highlightsList', JSON.stringify(hltVals));
});
}
async optionsLoaded(options) {
const hltVals = JSON.parse(options.highlightsList);
this.$widget.find('input.highlights-list-check[type="checkbox"]').each(function () {
if ($.inArray($(this).val(), hltVals) !== -1) {
$(this).prop("checked", true);
} else {
$(this).prop("checked", false);
}
});
}
}

View File

@ -412,7 +412,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
} }
}); });
// if there's no event, then this has been triggered programatically // if there's no event, then this has been triggered programmatically
if (!originalEvent) { if (!originalEvent) {
return; return;
} }

View File

@ -1,6 +1,7 @@
"use strict"; "use strict";
const attributeService = require("../../services/attributes"); const attributeService = require("../../services/attributes");
const cloneService = require("../../services/cloning");
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const dateNoteService = require('../../services/date_notes'); const dateNoteService = require('../../services/date_notes');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
@ -13,46 +14,25 @@ const path = require('path');
const BAttribute = require('../../becca/entities/battribute'); const BAttribute = require('../../becca/entities/battribute');
const htmlSanitizer = require('../../services/html_sanitizer'); const htmlSanitizer = require('../../services/html_sanitizer');
const {formatAttrForSearch} = require("../../services/attribute_formatter"); const {formatAttrForSearch} = require("../../services/attribute_formatter");
const jsdom = require("jsdom");
function findClippingNote(clipperInboxNote, pageUrl) { const { JSDOM } = jsdom;
const notes = clipperInboxNote.searchNotesInSubtree(
formatAttrForSearch({
type: 'label',
name: "pageUrl",
value: pageUrl
}, true)
);
for (const note of notes) {
if (note.getOwnedLabelValue('clipType') === 'clippings') {
return note;
}
}
return null;
}
function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) {
clipperInbox = dateNoteService.getDayNote(dateUtils.localNowDate());
}
return clipperInbox;
}
function addClipping(req) { function addClipping(req) {
// if a note under the clipperInbox as the same 'pageUrl' attribute,
// add the content to that note and clone it under today's inbox
// otherwise just create a new note under today's inbox
let {title, content, pageUrl, images} = req.body; let {title, content, pageUrl, images} = req.body;
const clipType = 'clippings';
const clipperInbox = getClipperInboxNote(); const clipperInbox = getClipperInboxNote();
const dailyNote = dateNoteService.getDayNote(dateUtils.localNowDate());
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
let clippingNote = findClippingNote(clipperInbox, pageUrl); let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType);
if (!clippingNote) { if (!clippingNote) {
clippingNote = noteService.createNewNote({ clippingNote = noteService.createNewNote({
parentNoteId: clipperInbox.noteId, parentNoteId: dailyNote.noteId,
title: title, title: title,
content: '', content: '',
type: 'text' type: 'text'
@ -67,13 +47,45 @@ function addClipping(req) {
const existingContent = clippingNote.getContent(); const existingContent = clippingNote.getContent();
clippingNote.setContent(`${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`); clippingNote.setContent(`${existingContent}${existingContent.trim() ? "<br>" : ""}${rewrittenContent}`);
if (clippingNote.parentNoteId !== dailyNote.noteId) {
cloneService.cloneNoteToParentNote(clippingNote.noteId, dailyNote.noteId);
}
return { return {
noteId: clippingNote.noteId noteId: clippingNote.noteId
}; };
} }
function findClippingNote(clipperInboxNote, pageUrl, clipType) {
if (!pageUrl) {
return null;
}
const notes = clipperInboxNote.searchNotesInSubtree(
formatAttrForSearch({
type: 'label',
name: "pageUrl",
value: pageUrl
}, true)
);
return clipType
? notes.find(note => note.getOwnedLabelValue('clipType') === clipType)
: notes[0];
}
function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) {
clipperInbox = dateNoteService.getRootCalendarNote();
}
return clipperInbox;
}
function createNote(req) { function createNote(req) {
let {title, content, pageUrl, images, clipType, labels} = req.body; let {title, content, pageUrl, images, clipType, labels} = req.body;
@ -81,26 +93,31 @@ function createNote(req) {
title = `Clipped note from ${pageUrl}`; title = `Clipped note from ${pageUrl}`;
} }
const clipperInbox = getClipperInboxNote();
const {note} = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
title,
content,
type: 'text'
});
clipType = htmlSanitizer.sanitize(clipType); clipType = htmlSanitizer.sanitize(clipType);
note.setLabel('clipType', clipType); const clipperInbox = getClipperInboxNote();
const dailyNote = dateNoteService.getDayNote(dateUtils.localNowDate());
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
let note = findClippingNote(clipperInbox, pageUrl, clipType);
if (pageUrl) { if (!note) {
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); note = noteService.createNewNote({
parentNoteId: dailyNote.noteId,
title,
content: '',
type: 'text'
}).note;
note.setLabel('pageUrl', pageUrl); note.setLabel('clipType', clipType);
note.setLabel('iconClass', 'bx bx-globe');
if (pageUrl) {
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
note.setLabel('pageUrl', pageUrl);
note.setLabel('iconClass', 'bx bx-globe');
}
} }
if (labels) { if (labels) {
for (const labelName in labels) { for (const labelName in labels) {
const labelValue = htmlSanitizer.sanitize(labels[labelName]); const labelValue = htmlSanitizer.sanitize(labels[labelName]);
@ -108,9 +125,9 @@ function createNote(req) {
} }
} }
const existingContent = note.getContent();
const rewrittenContent = processContent(images, note, content); const rewrittenContent = processContent(images, note, content);
note.setContent(`${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`);
note.setContent(rewrittenContent);
return { return {
noteId: note.noteId noteId: note.noteId
@ -158,6 +175,15 @@ function processContent(images, note, content) {
// fallback if parsing/downloading images fails for some reason on the extension side ( // fallback if parsing/downloading images fails for some reason on the extension side (
rewrittenContent = noteService.downloadImages(note.noteId, rewrittenContent); rewrittenContent = noteService.downloadImages(note.noteId, rewrittenContent);
// Check if rewrittenContent contains at least one HTML tag
if (!/<.+?>/.test(rewrittenContent)) {
rewrittenContent = `<p>${rewrittenContent}</p>`;
}
// Create a JSDOM object from the existing HTML content
const dom = new JSDOM(rewrittenContent);
// Get the content inside the body tag and serialize it
rewrittenContent = dom.window.document.body.innerHTML;
return rewrittenContent; return rewrittenContent;
} }
@ -187,9 +213,19 @@ function handshake() {
} }
} }
function findNotesByUrl(req){
let pageUrl = req.params.noteUrl;
const clipperInbox = getClipperInboxNote();
let foundPage = findClippingNote(clipperInbox, pageUrl, null);
return {
noteId: foundPage ? foundPage.noteId : null
}
}
module.exports = { module.exports = {
createNote, createNote,
addClipping, addClipping,
openNote, openNote,
handshake handshake,
findNotesByUrl
}; };

View File

@ -49,7 +49,7 @@ const ALLOWED_OPTIONS = new Set([
'compressImages', 'compressImages',
'downloadImagesAutomatically', 'downloadImagesAutomatically',
'minTocHeadings', 'minTocHeadings',
'highlightedText', 'highlightsList',
'checkForUpdates', 'checkForUpdates',
'disableTray', 'disableTray',
'eraseUnusedAttachmentsAfterSeconds', 'eraseUnusedAttachmentsAfterSeconds',

View File

@ -11,7 +11,7 @@ function addRecentNote(req) {
}).save(); }).save();
if (Math.random() < 0.05) { if (Math.random() < 0.05) {
// it's not necessary to run this everytime ... // it's not necessary to run this every time ...
const cutOffDate = dateUtils.utcDateTimeStr(new Date(Date.now() - 24 * 3600 * 1000)); const cutOffDate = dateUtils.utcDateTimeStr(new Date(Date.now() - 24 * 3600 * 1000));
sql.execute(`DELETE FROM recent_notes WHERE utcDateCreated < ?`, [cutOffDate]); sql.execute(`DELETE FROM recent_notes WHERE utcDateCreated < ?`, [cutOffDate]);

View File

@ -28,6 +28,12 @@ function execute(req) {
for (let query of queries) { for (let query of queries) {
query = query.trim(); query = query.trim();
while (query.startsWith('-- ')) {
// Query starts with one or more SQL comments, discard these before we execute.
const pivot = query.indexOf('\n');
query = pivot > 0 ? query.substr(pivot + 1).trim() : "";
}
if (!query) { if (!query) {
continue; continue;
} }

View File

@ -62,7 +62,7 @@ function checkSync() {
function syncNow() { function syncNow() {
log.info("Received request to trigger sync now."); log.info("Received request to trigger sync now.");
// when explicitly asked for set in progress status immediatelly for faster user feedback // when explicitly asked for set in progress status immediately for faster user feedback
ws.syncPullInProgress(); ws.syncPullInProgress();
return syncService.sync(); return syncService.sync();

View File

@ -269,6 +269,7 @@ function register(app) {
route(PST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler); route(PST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
route(PST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler); route(PST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler);
route(PST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler); route(PST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler);
route(GET, '/api/clipper/notes-by-url/:noteUrl', clipperMiddleware, clipperRoute.findNotesByUrl, apiResultHandler);
apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote); apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote);

View File

@ -395,7 +395,7 @@ class ConsistencyChecks {
({noteId, isProtected, type, mime}) => { ({noteId, isProtected, type, mime}) => {
if (this.autoFix) { if (this.autoFix) {
// it might be possible that the blob is not available only because of the interrupted // it might be possible that the blob is not available only because of the interrupted
// sync, and it will come later. It's therefore important to guarantee that this artifical // sync, and it will come later. It's therefore important to guarantee that this artificial
// record won't overwrite the real one coming from the sync. // record won't overwrite the real one coming from the sync.
const fakeDate = "2000-01-01 00:00:00Z"; const fakeDate = "2000-01-01 00:00:00Z";

View File

@ -57,5 +57,7 @@ function sanitize(dirtyHtml) {
module.exports = { module.exports = {
sanitize, sanitize,
sanitizeUrl sanitizeUrl: url => {
return sanitizeUrl(url).trim();
}
}; };

View File

@ -83,7 +83,7 @@ const defaultOptions = [
{ name: 'compressImages', value: 'true', isSynced: true }, { name: 'compressImages', value: 'true', isSynced: true },
{ name: 'downloadImagesAutomatically', value: 'true', isSynced: true }, { name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
{ name: 'minTocHeadings', value: '5', isSynced: true }, { name: 'minTocHeadings', value: '5', isSynced: true },
{ name: 'highlightedText', value: '["bold","italic","underline","color","bgColor"]', isSynced: true }, { name: 'highlightsList', value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
{ name: 'checkForUpdates', value: 'true', isSynced: true }, { name: 'checkForUpdates', value: 'true', isSynced: true },
{ name: 'disableTray', value: 'false', isSynced: false }, { name: 'disableTray', value: 'false', isSynced: false },
{ name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true }, { name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true },

View File

@ -55,7 +55,7 @@ ${bundle.script}\r
} }
/** /**
* THIS METHOD CANT BE ASYNC, OTHERWISE TRANSACTION WRAPPER WON'T BE EFFECTIVE AND WE WILL BE LOSING THE * THIS METHOD CAN'T BE ASYNC, OTHERWISE TRANSACTION WRAPPER WON'T BE EFFECTIVE AND WE WILL BE LOSING THE
* ENTITY CHANGES IN CLS. * ENTITY CHANGES IN CLS.
* *
* This method preserves frontend startNode - that's why we start execution from currentNote and override * This method preserves frontend startNode - that's why we start execution from currentNote and override

View File

@ -19,20 +19,22 @@ class NoteFlatTextExp extends Expression {
/** /**
* @param {BNote} note * @param {BNote} note
* @param {string[]} tokens * @param {string[]} remainingTokens - tokens still needed to be found in the path towards root
* @param {string[]} path * @param {string[]} takenPath - path so far taken towards from candidate note towards the root.
* It contains the suffix fragment of the full note path.
*/ */
const searchDownThePath = (note, tokens, path) => { const searchPathTowardsRoot = (note, remainingTokens, takenPath) => {
if (tokens.length === 0) { if (remainingTokens.length === 0) {
const retPath = this.getNotePath(note, path); // we're done, just build the result
const resultPath = this.getNotePath(note, takenPath);
if (retPath) { if (resultPath) {
const noteId = retPath[retPath.length - 1]; const noteId = resultPath[resultPath.length - 1];
if (!resultNoteSet.hasNoteId(noteId)) { if (!resultNoteSet.hasNoteId(noteId)) {
// we could get here from multiple paths, the first one wins because the paths // we could get here from multiple paths, the first one wins because the paths
// are sorted by importance // are sorted by importance
executionContext.noteIdToNotePath[noteId] = retPath; executionContext.noteIdToNotePath[noteId] = resultPath;
resultNoteSet.add(becca.notes[noteId]); resultNoteSet.add(becca.notes[noteId]);
} }
@ -42,22 +44,23 @@ class NoteFlatTextExp extends Expression {
} }
if (note.parents.length === 0 || note.noteId === 'root') { if (note.parents.length === 0 || note.noteId === 'root') {
// we've reached root, but there are still remaining tokens -> this candidate note produced no result
return; return;
} }
const foundAttrTokens = []; const foundAttrTokens = [];
for (const token of tokens) { for (const token of remainingTokens) {
if (note.type.includes(token) || note.mime.includes(token)) { if (note.type.includes(token) || note.mime.includes(token)) {
foundAttrTokens.push(token); foundAttrTokens.push(token);
} }
} }
for (const attribute of note.ownedAttributes) { for (const attribute of note.getOwnedAttributes()) {
const normalizedName = utils.normalize(attribute.name); const normalizedName = utils.normalize(attribute.name);
const normalizedValue = utils.normalize(attribute.value); const normalizedValue = utils.normalize(attribute.value);
for (const token of tokens) { for (const token of remainingTokens) {
if (normalizedName.includes(token) || normalizedValue.includes(token)) { if (normalizedName.includes(token) || normalizedValue.includes(token)) {
foundAttrTokens.push(token); foundAttrTokens.push(token);
} }
@ -68,19 +71,19 @@ class NoteFlatTextExp extends Expression {
const title = utils.normalize(beccaService.getNoteTitle(note.noteId, parentNote.noteId)); const title = utils.normalize(beccaService.getNoteTitle(note.noteId, parentNote.noteId));
const foundTokens = foundAttrTokens.slice(); const foundTokens = foundAttrTokens.slice();
for (const token of tokens) { for (const token of remainingTokens) {
if (title.includes(token)) { if (title.includes(token)) {
foundTokens.push(token); foundTokens.push(token);
} }
} }
if (foundTokens.length > 0) { if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token)); const newRemainingTokens = remainingTokens.filter(token => !foundTokens.includes(token));
searchDownThePath(parentNote, remainingTokens, [...path, note.noteId]); searchPathTowardsRoot(parentNote, newRemainingTokens, [note.noteId, ...takenPath]);
} }
else { else {
searchDownThePath(parentNote, tokens, [...path, note.noteId]); searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId, ...takenPath]);
} }
} }
} }
@ -90,7 +93,7 @@ class NoteFlatTextExp extends Expression {
for (const note of candidateNotes) { for (const note of candidateNotes) {
// autocomplete should be able to find notes by their noteIds as well (only leafs) // autocomplete should be able to find notes by their noteIds as well (only leafs)
if (this.tokens.length === 1 && note.noteId.toLowerCase() === this.tokens[0]) { if (this.tokens.length === 1 && note.noteId.toLowerCase() === this.tokens[0]) {
searchDownThePath(note, [], []); searchPathTowardsRoot(note, [], [note.noteId]);
continue; continue;
} }
@ -123,7 +126,7 @@ class NoteFlatTextExp extends Expression {
if (foundTokens.length > 0) { if (foundTokens.length > 0) {
const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token)); const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
searchDownThePath(parentNote, remainingTokens, [note.noteId]); searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId]);
} }
} }
} }
@ -131,14 +134,22 @@ class NoteFlatTextExp extends Expression {
return resultNoteSet; return resultNoteSet;
} }
getNotePath(note, path) { /**
if (path.length === 0) { * @param {BNote} note
* @param {string[]} takenPath
* @returns {string[]}
*/
getNotePath(note, takenPath) {
if (takenPath.length === 0) {
throw new Error("Path is not expected to be empty.");
} else if (takenPath.length === 1 && takenPath[0] === note.noteId) {
return note.getBestNotePath(); return note.getBestNotePath();
} else { } else {
const closestNoteId = path[0]; // this note is the closest to root containing the last matching token(s), thus completing the requirements
const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePath(); // what's in this note's predecessors does not matter, thus we'll choose the best note path
const topMostMatchingTokenNotePath = becca.getNote(takenPath[0]).getBestNotePath();
return [...closestNoteBestNotePath, ...path.slice(1)]; return [...topMostMatchingTokenNotePath, ...takenPath.slice(1)];
} }
} }

View File

@ -10,7 +10,6 @@ const becca = require('../../../becca/becca');
const beccaService = require('../../../becca/becca_service'); const beccaService = require('../../../becca/becca_service');
const utils = require('../../utils'); const utils = require('../../utils');
const log = require('../../log'); const log = require('../../log');
const scriptService = require("../../script");
const hoistedNoteService = require("../../hoisted_note"); const hoistedNoteService = require("../../hoisted_note");
function searchFromNote(note) { function searchFromNote(note) {
@ -73,6 +72,7 @@ function searchFromRelation(note, relationName) {
return []; return [];
} }
const scriptService = require("../../script"); // to avoid circular dependency
const result = scriptService.executeNote(scriptNote, {originEntity: note}); const result = scriptService.executeNote(scriptNote, {originEntity: note});
if (!Array.isArray(result)) { if (!Array.isArray(result)) {

View File

@ -13,7 +13,7 @@ class TaskContext {
this.noteDeletionHandlerTriggered = false; this.noteDeletionHandlerTriggered = false;
// progressCount is meant to represent just some progress - to indicate the task is not stuck // progressCount is meant to represent just some progress - to indicate the task is not stuck
this.progressCount = -1; // we're incrementing immediatelly this.progressCount = -1; // we're incrementing immediately
this.lastSentCountTs = 0; // 0 will guarantee the first message will be sent this.lastSentCountTs = 0; // 0 will guarantee the first message will be sent
// just the fact this has been initialized is a progress which should be sent to clients // just the fact this has been initialized is a progress which should be sent to clients

View File

@ -96,7 +96,7 @@
<li>From the Trilium Menu, click Options.</li> <li>From the Trilium Menu, click Options.</li>
<li>Click on Sync tab.</li> <li>Click on Sync tab.</li>
<li>Change server instance address to: <span id="current-host"></span> and click save.</li> <li>Change server instance address to: <span id="current-host"></span> and click save.</li>
<li>Click "Test sync" button to verify connection is successfull.</li> <li>Click "Test sync" button to verify connection is successful.</li>
<li>Once you've completed these steps, click <a href="/">here</a>.</li> <li>Once you've completed these steps, click <a href="/">here</a>.</li>
</ol> </ol>