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) [![Join the chat at https://gitter.im/trilium-notes/Lobby](https://badges.gitter.im/trilium-notes/Lobby.svg)](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)以快速了解: ![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png) @@ -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 [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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 = `