diff --git a/README-ZH_CN.md b/README-ZH_CN.md
index 70d08a8c1..23612ce07 100644
--- a/README-ZH_CN.md
+++ b/README-ZH_CN.md
@@ -1,9 +1,9 @@
-# Trilium笔记
+# Trilium Notes
[English](https://github.com/zadam/trilium/blob/master/README.md) | [Chinese](https://github.com/zadam/trilium/blob/master/README-ZH_CN.md) | [Russian](https://github.com/zadam/trilium/blob/master/README.ru.md)
[](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-Trilium Notes是一个分层的笔记应用程序,专注于建立大型个人知识库。请参阅[屏幕截图](https://github.com/zadam/trilium/wiki/Screenshot-tour)以快速了解:
+Trilium Notes 是一个层次化的笔记应用程序,专注于建立大型个人知识库。请参阅[屏幕截图](https://github.com/zadam/trilium/wiki/Screenshot-tour)以快速了解:

@@ -14,36 +14,43 @@ Ukraine is currently suffering from Russian aggression, please consider donating
## 特性
* 笔记可以排列成任意深的树。单个笔记可以放在树中的多个位置(请参阅[克隆](https://github.com/zadam/trilium/wiki/Cloning-notes))
-* 丰富的所见即所得笔记编辑功能,包括带有markdown[自动格式化功能的](https://github.com/zadam/trilium/wiki/Text-notes#autoformat)表格,图像和[数学](https://github.com/zadam/trilium/wiki/Text-notes#math-support)
+* 丰富的所见即所得笔记编辑功能,包括带有 Markdown [自动格式化功能的](https://github.com/zadam/trilium/wiki/Text-notes#autoformat)表格,图像和[数学](https://github.com/zadam/trilium/wiki/Text-notes#math-support)
* 支持编辑[使用源代码的笔记](https://github.com/zadam/trilium/wiki/Code-notes),包括语法高亮显示
-* 笔记之间快速[导航](https://github.com/zadam/trilium/wiki/Note-navigation),全文搜索和[笔记挂起](https://github.com/zadam/trilium/wiki/Note-hoisting)
+* 笔记之间快速[导航](https://github.com/zadam/trilium/wiki/Note-navigation),全文搜索和[笔记聚焦](https://github.com/zadam/trilium/wiki/Note-hoisting)
* 无缝[笔记版本控制](https://github.com/zadam/trilium/wiki/Note-revisions)
* 笔记[属性](https://github.com/zadam/trilium/wiki/Attributes)可用于笔记组织,查询和高级[脚本编写](https://github.com/zadam/trilium/wiki/Scripts)
* [同步](https://github.com/zadam/trilium/wiki/Synchronization)与自托管同步服务器
+ * 有一个[第三方提供的同步服务器托管服务](https://trilium.cc/paid-hosting)
+* 公开地[分享](https://github.com/zadam/trilium/wiki/Sharing)(发布)笔记到互联网
* 具有按笔记粒度的强大的[笔记加密](https://github.com/zadam/trilium/wiki/Protected-notes)
+* 使用自带的 Excalidraw 来绘制图表(笔记类型“画布”)
* [关系图](https://github.com/zadam/trilium/wiki/Relation-map)和[链接图](https://github.com/zadam/trilium/wiki/Link-map),用于可视化笔记及其关系
-* [脚本](https://github.com/zadam/trilium/wiki/Scripts)-请参阅[高级展示](https://github.com/zadam/trilium/wiki/Advanced-showcases)
-* 可用性和性能均能很好地扩展至超过10万个笔记
-* 针对智能手机和平板电脑进行触摸优化的[移动前端](https://github.com/zadam/trilium/wiki/Mobile-frontend)
+* [脚本](https://github.com/zadam/trilium/wiki/Scripts) - 请参阅[高级功能展示](https://github.com/zadam/trilium/wiki/Advanced-showcases)
+* 在拥有超过 10 万条笔记时仍能保持良好的可用性和性能
+* 针对智能手机和平板电脑进行优化的[用于移动设备的前端](https://github.com/zadam/trilium/wiki/Mobile-frontend)
* [夜间主题](https://github.com/zadam/trilium/wiki/Themes)
-* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import)和[Markdown导入导出](https://github.com/zadam/trilium/wiki/Markdown)
-* [Web Clipper](https://github.com/zadam/trilium/wiki/Web-clipper)可轻松保存Web内容
+* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) 和 [Markdown 导入导出](https://github.com/zadam/trilium/wiki/Markdown)功能
+* 使用[网页剪藏](https://github.com/zadam/trilium/wiki/Web-clipper)轻松保存互联网上的内容
## 构建
-Trilium是作为桌面应用程序(Linux和Windows)或服务器上托管的Web应用程序(Linux)提供的。Mac OS桌面版本可用,但[不受支持](https://github.com/zadam/trilium/wiki/FAQ#mac-os-support)。
+Trilium 可以用作桌面应用程序(Linux 和 Windows)或服务器(Linux)上托管的 Web 应用程序。虽然有 macOS 版本的桌面应用程序,但它[不受支持](https://github.com/zadam/trilium/wiki/FAQ#mac-os-support)。
-* 如果要在桌面上使用Trilium,请从[最新版本](https://github.com/zadam/trilium/releases/latest)下载适用于您平台的二进制[版本](https://github.com/zadam/trilium/releases/latest),解压缩该软件包并运行`trilium`可执行文件。
-* 如果要在服务器上安装Trilium,请遵循[此页面](https://github.com/zadam/trilium/wiki/Server-installation)。
- * 当前仅支持(经过测试)最新的Chrome和Firefox浏览器。
+* 如果要在桌面上使用 Trilium,请从[最新版本](https://github.com/zadam/trilium/releases/latest)下载适用于您平台的二进制版本,解压缩该软件包并运行`trilium`可执行文件。
+* 如果要在服务器上安装 Trilium,请参考[此页面](https://github.com/zadam/trilium/wiki/Server-installation)。
+ * 当前仅支持(测试过)最近发布的 Chrome 和 Firefox 浏览器。
+
+Trilium 也提供 Flatpak:
+
+[](https://flathub.org/apps/details/com.github.zadam.trilium)
## 文档
-[有关文档页面的完整列表,请参见Wiki。](https://github.com/zadam/trilium/wiki/)
+[有关文档页面的完整列表,请参见 Wiki。](https://github.com/zadam/trilium/wiki/)
-[中文Wiki在这里](https://github.com/baddate/trilium/wiki/)
+* [Wiki 的中文翻译版本](https://github.com/baddate/trilium/wiki/)
-您还可以阅读[个人知识库模式](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base),以获取有关如何使用Trilium的灵感。
+您还可以阅读[个人知识库模式](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base),以获取有关如何使用 Trilium 的灵感。
## 贡献
@@ -51,7 +58,7 @@ Trilium是作为桌面应用程序(Linux和Windows)或服务器上托管的W
[](https://gitpod.io/#https://github.com/zadam/trilium)
-或在本地克隆并运行
+或者克隆本仓库到本地,并运行
```
npm install
@@ -60,7 +67,15 @@ npm run start-server
## 致谢
-* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - 市场上最好的所见即所得编辑器,互动性强且聆听能力强的团队
-* [FancyTree](https://github.com/mar10/fancytree) - 一个非常丰富的关于树的库,强大的没有对手。没有它,Trilium Notes将不会如此。
+* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - 市面上最好的所见即所得编辑器,拥有互动性强且聆听能力强的团队
+* [FancyTree](https://github.com/mar10/fancytree) - 一个非常丰富的关于树的库,强大到没有对手。没有它,Trilium Notes 将不会如此。
* [CodeMirror](https://github.com/codemirror/CodeMirror) - 支持大量语言的代码编辑器
-* [jsPlumb](https://github.com/jsplumb/jsplumb)强大的可视化连接库。- 用于[关系图](https://github.com/zadam/trilium/wiki/Relation-map)和[链接图](https://github.com/zadam/trilium/wiki/Link-map)
+* [jsPlumb](https://github.com/jsplumb/jsplumb) - 强大的可视化连接库。用于[关系图](https://github.com/zadam/trilium/wiki/Relation-map)和[链接图](https://github.com/zadam/trilium/wiki/Link-map)
+
+## 捐赠
+
+你可以通过 GitHub Sponsors,[PayPal](https://paypal.me/za4am) 或者比特币 (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2) 来捐赠。
+
+## 许可证
+
+本程序是自由软件:你可以再发布本软件和/或修改本软件,只要你遵循 Free Software Foundation 发布的 GNU Affero General Public License 的第三版或者任何(由你选择)更晚的版本。
diff --git a/db/migrations/0198__randomize_branchIds.sql b/db/migrations/0198__randomize_branchIds.sql
deleted file mode 100644
index 4e5a1bfe4..000000000
--- a/db/migrations/0198__randomize_branchIds.sql
+++ /dev/null
@@ -1,7 +0,0 @@
--- "randomize" branchIds so it's clear user should not rely on them
-UPDATE branches SET branchId = '7LSsI2FnZPW2' WHERE parentNoteId = 'hidden' AND noteId = 'search';
-UPDATE branches SET branchId = 'wEcmxk4CNC7G' WHERE parentNoteId = 'singles' AND noteId = 'globalnotemap';
-UPDATE branches SET branchId = '191uVR6Cu6fA' WHERE parentNoteId = 'hidden' AND noteId = 'sqlconsole';
-UPDATE branches SET branchId = 'OjX5Phxp6A4N' WHERE parentNoteId = 'root' AND noteId = 'hidden';
-UPDATE branches SET branchId = 'glNBYFYZRH8P' WHERE parentNoteId = 'hidden' AND noteId = 'bulkaction';
-UPDATE branches SET branchId = 'cAT25wvGMg3K' WHERE parentNoteId = 'root' AND noteId = 'share';
diff --git a/db/migrations/0198__rename_branchIds.sql b/db/migrations/0198__rename_branchIds.sql
new file mode 100644
index 000000000..3c6cd0e64
--- /dev/null
+++ b/db/migrations/0198__rename_branchIds.sql
@@ -0,0 +1,6 @@
+UPDATE branches SET branchId = '_hidden__search' WHERE parentNoteId = 'hidden' AND noteId = 'search';
+UPDATE branches SET branchId = 'root__globalNoteMap' WHERE parentNoteId = 'singles' AND noteId = 'globalnotemap';
+UPDATE branches SET branchId = '_hidden__sqlConsole' WHERE parentNoteId = 'hidden' AND noteId = 'sqlconsole';
+UPDATE branches SET branchId = 'root__hidden' WHERE parentNoteId = 'root' AND noteId = 'hidden';
+UPDATE branches SET branchId = '_hidden__bulkAction' WHERE parentNoteId = 'hidden' AND noteId = 'bulkaction';
+UPDATE branches SET branchId = '_hidden__share' WHERE parentNoteId = 'root' AND noteId = 'share';
diff --git a/db/migrations/0202__move_global_note_map_under_hidden.sql b/db/migrations/0202__move_global_note_map_under_hidden.sql
index 5c5f42812..0b634f69f 100644
--- a/db/migrations/0202__move_global_note_map_under_hidden.sql
+++ b/db/migrations/0202__move_global_note_map_under_hidden.sql
@@ -1,2 +1,2 @@
-DELETE FROM branches WHERE noteId = '_globalNoteMap' AND parentNoteId != 'singles'; -- make sure there are no clones which would fail at the next line
+DELETE FROM branches WHERE noteId = '_globalNoteMap' AND parentNoteId != 'singles' AND parentNoteId != '_hidden'; -- make sure there are no clones which would fail at the next line
UPDATE branches SET parentNoteId = '_hidden' WHERE noteId = '_globalNoteMap';
diff --git a/db/migrations/0210__consistency_checks.js b/db/migrations/0210__consistency_checks.js
new file mode 100644
index 000000000..f3809fa06
--- /dev/null
+++ b/db/migrations/0210__consistency_checks.js
@@ -0,0 +1,24 @@
+module.exports = async () => {
+ const cls = require("../../src/services/cls");
+ const beccaLoader = require("../../src/becca/becca_loader");
+ const log = require("../../src/services/log");
+ const consistencyChecks = require("../../src/services/consistency_checks");
+ const noteService = require("../../src/services/notes");
+
+ await cls.init(async () => {
+ // precaution for the 0211 migration
+ noteService.eraseDeletedNotesNow();
+
+ beccaLoader.load();
+
+ try {
+ // precaution before running 211 which might produce unique constraint problems if the DB was not consistent
+ consistencyChecks.runOnDemandChecksWithoutExclusiveLock(true);
+ }
+ catch (e) {
+ // consistency checks might start failing in the future if there's some incompatible migration down the road
+ // we can optimistically assume the DB is consistent and still continue
+ log.error(`Consistency checks failed in migration 0210: ${e.message} ${e.stack}`);
+ }
+ });
+};
diff --git a/db/migrations/0211__rename_branchIds.sql b/db/migrations/0211__rename_branchIds.sql
new file mode 100644
index 000000000..be18ec00c
--- /dev/null
+++ b/db/migrations/0211__rename_branchIds.sql
@@ -0,0 +1,12 @@
+-- case based on isDeleted is needed, otherwise 2 branches (1 deleted, 1 not) might get the same ID
+UPDATE entity_changes SET entityId = COALESCE((
+ SELECT
+ CASE isDeleted
+ WHEN 0 THEN parentNoteId || '_' || noteId
+ WHEN 1 THEN branchId
+ END
+ FROM branches WHERE branchId = entityId
+), entityId)
+WHERE entityName = 'branches' AND isErased = 0;
+
+UPDATE branches SET branchId = parentNoteId || '_' || noteId WHERE isDeleted = 0;
diff --git a/db/migrations/0212__delete_all_attributes_of_named_notes.js b/db/migrations/0212__delete_all_attributes_of_named_notes.js
new file mode 100644
index 000000000..47ddb9114
--- /dev/null
+++ b/db/migrations/0212__delete_all_attributes_of_named_notes.js
@@ -0,0 +1,21 @@
+module.exports = () => {
+ const cls = require("../../src/services/cls");
+ const beccaLoader = require("../../src/becca/becca_loader");
+ const becca = require("../../src/becca/becca");
+
+ cls.init(() => {
+ beccaLoader.load();
+
+ const hidden = becca.getNote("_hidden");
+
+ for (const noteId of hidden.getSubtreeNoteIds({includeHidden: true})) {
+ if (noteId.startsWith("_")) { // is "named" note
+ const note = becca.getNote(noteId);
+
+ for (const attr of note.getOwnedAttributes()) {
+ attr.markAsDeleted("0212__delete_all_attributes_of_named_notes");
+ }
+ }
+ }
+ });
+};
diff --git a/package.json b/package.json
index 185f5266f..957ca7b29 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
- "version": "0.58.0-beta",
+ "version": "0.58.2-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
diff --git a/spec/search/search.spec.js b/spec/search/search.spec.js
index ce826ed16..83eba56e7 100644
--- a/spec/search/search.spec.js
+++ b/spec/search/search.spec.js
@@ -13,7 +13,7 @@ describe("Search", () => {
becca.reset();
rootNote = new NoteBuilder(new Note({noteId: 'root', title: 'root', type: 'text'}));
- new Branch({branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
+ new Branch({branchId: 'none_root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
});
it("simple path match", () => {
diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js
index b96296bed..2cb924a5c 100644
--- a/src/becca/becca_service.js
+++ b/src/becca/becca_service.js
@@ -180,7 +180,7 @@ function getNotePath(noteId) {
let branchId;
if (note.isRoot()) {
- branchId = 'root';
+ branchId = 'none_root';
}
else {
const parentNote = note.parents[0];
diff --git a/src/becca/entities/abstract_entity.js b/src/becca/entities/abstract_entity.js
index 8c1b7b755..8d28604ca 100644
--- a/src/becca/entities/abstract_entity.js
+++ b/src/becca/entities/abstract_entity.js
@@ -46,7 +46,10 @@ class AbstractEntity {
return this.utcDateModified || this.utcDateCreated;
}
- /** @protected */
+ /**
+ * @protected
+ * @returns {Becca}
+ */
get becca() {
if (!becca) {
becca = require('../becca');
@@ -75,7 +78,7 @@ class AbstractEntity {
/**
* Saves entity - executes SQL, but doesn't commit the transaction on its own
*
- * @returns {AbstractEntity}
+ * @returns {this}
*/
save() {
const entityName = this.constructor.entityName;
diff --git a/src/becca/entities/branch.js b/src/becca/entities/branch.js
index 127c58b3c..cd647775e 100644
--- a/src/becca/entities/branch.js
+++ b/src/becca/entities/branch.js
@@ -78,7 +78,7 @@ class Branch extends AbstractEntity {
childNote.parentBranches.push(this);
}
- if (this.branchId === 'root') {
+ if (this.noteId === 'root') {
return;
}
@@ -165,8 +165,7 @@ class Branch extends AbstractEntity {
}
}
- if (this.branchId === 'root'
- || this.noteId === 'root'
+ if (this.noteId === 'root'
|| this.noteId === cls.getHoistedNoteId()) {
throw new Error("Can't delete root or hoisted branch/note");
@@ -209,11 +208,19 @@ class Branch extends AbstractEntity {
}
beforeSaving() {
+ if (!this.noteId || !this.parentNoteId) {
+ throw new Error(`noteId and parentNoteId are mandatory properties for Branch`);
+ }
+
+ this.branchId = `${this.parentNoteId}_${this.noteId}`;
+
if (this.notePosition === undefined || this.notePosition === null) {
let maxNotePos = 0;
for (const childBranch of this.parentNote.getChildBranches()) {
- if (maxNotePos < childBranch.notePosition && childBranch.noteId !== '_hidden') {
+ if (maxNotePos < childBranch.notePosition
+ && childBranch.noteId !== '_hidden' // hidden has very large notePosition to always stay last
+ ) {
maxNotePos = childBranch.notePosition;
}
}
@@ -225,6 +232,10 @@ class Branch extends AbstractEntity {
this.isExpanded = false;
}
+ if (!this.prefix?.trim()) {
+ this.prefix = null;
+ }
+
this.utcDateModified = dateUtils.utcNowDateTime();
super.beforeSaving();
@@ -246,13 +257,20 @@ class Branch extends AbstractEntity {
}
createClone(parentNoteId, notePosition) {
- return new Branch({
- noteId: this.noteId,
- parentNoteId: parentNoteId,
- notePosition: notePosition,
- prefix: this.prefix,
- isExpanded: this.isExpanded
- });
+ const existingBranch = this.becca.getBranchFromChildAndParent(this.noteId, parentNoteId);
+
+ if (existingBranch) {
+ existingBranch.notePosition = notePosition;
+ return existingBranch;
+ } else {
+ return new Branch({
+ noteId: this.noteId,
+ parentNoteId: parentNoteId,
+ notePosition: notePosition,
+ prefix: this.prefix,
+ isExpanded: this.isExpanded
+ });
+ }
}
}
diff --git a/src/becca/entities/note.js b/src/becca/entities/note.js
index 58ecf51fb..ae1b1df52 100644
--- a/src/becca/entities/note.js
+++ b/src/becca/entities/note.js
@@ -945,13 +945,14 @@ class Note extends AbstractEntity {
};
}
- /** @returns {String[]} */
- getSubtreeNoteIds({includeArchived = true, resolveSearch = false} = {}) {
- return this.getSubtree({includeArchived, resolveSearch})
+ /** @returns {String[]} - includes the subtree node as well */
+ getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) {
+ return this.getSubtree({includeArchived, includeHidden, resolveSearch})
.notes
.map(note => note.noteId);
}
+ /** @deprecated use getSubtreeNoteIds() instead */
getDescendantNoteIds() {
return this.getSubtreeNoteIds();
}
@@ -1171,7 +1172,8 @@ class Note extends AbstractEntity {
* @param {string} type - attribute type (label / relation)
* @param {string} name - name of the attribute, not including the leading ~/#
* @param {string} [value] - value of the attribute - text for labels, target note ID for relations; optional.
- *
+ * @param {boolean} [isInheritable=false]
+ * @param {int} [position]
* @return {Attribute}
*/
addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
@@ -1192,7 +1194,7 @@ class Note extends AbstractEntity {
*
* @param {string} name - name of the label, not including the leading #
* @param {string} [value] - text value of the label; optional
- *
+ * @param {boolean} [isInheritable=false]
* @return {Attribute}
*/
addLabel(name, value = "", isInheritable = false) {
@@ -1204,8 +1206,8 @@ class Note extends AbstractEntity {
* returned.
*
* @param {string} name - name of the relation, not including the leading ~
- * @param {string} value - ID of the target note of the relation
- *
+ * @param {string} targetNoteId
+ * @param {boolean} [isInheritable=false]
* @return {Attribute}
*/
addRelation(name, targetNoteId, isInheritable = false) {
diff --git a/src/etapi/branches.js b/src/etapi/branches.js
index bac449322..ba06afee1 100644
--- a/src/etapi/branches.js
+++ b/src/etapi/branches.js
@@ -35,15 +35,14 @@ function register(router) {
existing.save();
return res.status(200).json(mappers.mapBranchToPojo(existing));
- }
+ } else {
+ try {
+ const branch = new Branch(params).save();
- try {
- const branch = new Branch(params).save();
-
- res.status(201).json(mappers.mapBranchToPojo(branch));
- }
- catch (e) {
- throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
+ res.status(201).json(mappers.mapBranchToPojo(branch));
+ } catch (e) {
+ throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
+ }
}
});
diff --git a/src/public/app/services/branches.js b/src/public/app/services/branches.js
index 86eb96c08..b94c95b2a 100644
--- a/src/public/app/services/branches.js
+++ b/src/public/app/services/branches.js
@@ -10,7 +10,9 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
branchIdsToMove = filterRootNote(branchIdsToMove);
branchIdsToMove = filterSearchBranches(branchIdsToMove);
- if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranchId)) {
+ const beforeBranch = await froca.getBranch(beforeBranchId);
+
+ if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) {
toastService.showError('Cannot move notes here.');
return;
}
diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js
index 14b714ba2..5f89a9297 100644
--- a/src/public/app/services/frontend_script_api.js
+++ b/src/public/app/services/frontend_script_api.js
@@ -41,6 +41,24 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
/** @property {NoteContextAwareWidget} */
this.NoteContextAwareWidget = NoteContextAwareWidget;
+ /**
+ * @property {NoteContextAwareWidget}
+ * @deprecated use NoteContextAwareWidget instead
+ */
+ this.TabAwareWidget = NoteContextAwareWidget;
+
+ /**
+ * @property {NoteContextAwareWidget}
+ * @deprecated use NoteContextAwareWidget instead
+ */
+ this.TabCachingWidget = NoteContextAwareWidget;
+
+ /**
+ * @property {NoteContextAwareWidget}
+ * @deprecated use NoteContextAwareWidget instead
+ */
+ this.NoteContextCachingWidget = NoteContextAwareWidget;
+
/** @property {BasicWidget} */
this.BasicWidget = BasicWidget;
diff --git a/src/public/app/widgets/note_map.js b/src/public/app/widgets/note_map.js
index acacedae3..ccd659683 100644
--- a/src/public/app/widgets/note_map.js
+++ b/src/public/app/widgets/note_map.js
@@ -5,6 +5,9 @@ import hoistedNoteService from "../services/hoisted_note.js";
import appContext from "../components/app_context.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import linkContextMenuService from "../menus/link_context_menu.js";
+import utils from "../services/utils.js";
+
+const esc = utils.escapeHtml;
const TPL = `