Compare commits
238 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1af98056bb | |||
| c0edcc1bfe | |||
| 597e595c32 | |||
| e212a37c84 | |||
| c41448f5da | |||
| aab49946f8 | |||
| cc4d04416b | |||
| 66c5606d46 | |||
| 50bcf45113 | |||
| 08cba1e1ce | |||
| f528799fed | |||
| a78c8ddad7 | |||
| 3cc93157bf | |||
| 3dbd80d5a6 | |||
| 076b4a6651 | |||
| 378987e61c | |||
| 5eb850bf59 | |||
| 4914160590 | |||
| 00c106aba6 | |||
| 5e6d2c8336 | |||
| d45a59aa4e | |||
| 9f19cf8046 | |||
| c0dfd23191 | |||
| b9539117d8 | |||
| d616a77d6b | |||
| 5b679930de | |||
| 3d7beefad0 | |||
| 8850de51f5 | |||
| 18b13657fd | |||
| 43ac712fca | |||
| bf231d31a2 | |||
| 7dd19c0366 | |||
| 0040334e89 | |||
| 96a5cb23f3 | |||
| 067ca9ab16 | |||
| be65e2e62f | |||
| bed7bdfd00 | |||
| cd5be59413 | |||
| 2a67d9eae2 | |||
| 0f05d6258e | |||
| 45516fa7b4 | |||
| 70708b36ef | |||
| 397f0d1326 | |||
| 6469937393 | |||
| 48cb4d2ab9 | |||
| 3a94b04ef4 | |||
| d406df48a8 | |||
| 9447d3f9b5 | |||
| 2ac78c2e03 | |||
| 17073096cf | |||
| 6068bd7c44 | |||
| 56a35b85a6 | |||
| eb1a6b78ff | |||
| 60d000b9cb | |||
| 61ddd8afc6 | |||
| 69bdcdd74a | |||
| 17c6f53397 | |||
| 1c51419db5 | |||
| 1ba1f28006 | |||
| 6b306ac426 | |||
| ae26f22114 | |||
| 552d213817 | |||
| 8fb7dd451e | |||
| 01a82bed09 | |||
| 77bc5f9944 | |||
| 6081c3540e | |||
| 0a10764ed4 | |||
| 8019e23ee4 | |||
| 5eb9b8fe42 | |||
| d19a929237 | |||
| 67e0e0888e | |||
| 810fd9e819 | |||
| 2dc742f5b8 | |||
| 48b7108da0 | |||
| fbabdef272 | |||
| a1f67e830d | |||
| ee78413ecb | |||
| b936a63db3 | |||
| 80354d09fb | |||
| 87925f72a3 | |||
| 898c941333 | |||
| 8b0a1e546d | |||
| ec351137d4 | |||
| cf5127dcec | |||
| 345de752d1 | |||
| 7b0fd639f6 | |||
| 6408a47a8a | |||
| 872e81fe1f | |||
| 480aec1667 | |||
| 35ea877c7d | |||
| 41ca4b8d2e | |||
| 8375a1b5ab | |||
| c33d496cf3 | |||
| 200982655f | |||
| d67e1552ee | |||
| ce09e4a1eb | |||
| 7bf6ec3ff2 | |||
| 1f923403de | |||
| cb2361e9c4 | |||
| d7e46263be | |||
| 138903cf28 | |||
| 3fa2535862 | |||
| 5bb490e1ff | |||
| f2e44e5617 | |||
| 58a93f748b | |||
| 199b49c1f7 | |||
| cbc1f63732 | |||
| 3f2e8717b3 | |||
| cd858a73c1 | |||
| 69353f12b5 | |||
| 7f8b19aee4 | |||
| cd653b9f0c | |||
| 9fcd659df4 | |||
| c08c4531db | |||
| f8bec35893 | |||
| 7aadd57d2f | |||
| 02d9752abf | |||
| 1d64129572 | |||
| 95760f1d28 | |||
| 75c3bd8ae3 | |||
| 479d6ba964 | |||
| 9f472b353a | |||
| 4c614aee90 | |||
| f17be0ef9b | |||
| 7dde78e98a | |||
| 8c0662ab82 | |||
| cc3addebd4 | |||
| a1b2e22ba5 | |||
| f8936f3bc8 | |||
| 015c9d2ccd | |||
| 95ccfd2521 | |||
| 6c15d299e0 | |||
| f8089ba370 | |||
| 053162fef2 | |||
| 71bbf2eb16 | |||
| f723433970 | |||
| 1cc7917b6e | |||
| cdc2721ac1 | |||
| 4a15127e58 | |||
| c5ec57e08e | |||
| d0578971f7 | |||
| 1b0bb22273 | |||
| bf653a9a5c | |||
| 5467dc62cd | |||
| 96eff4c410 | |||
| 3a12181a57 | |||
| fccce2ff1e | |||
| c949dcb449 | |||
| 8b99f065d5 | |||
| 6d7b9e0db3 | |||
| 248fa780e8 | |||
| 20c7c657da | |||
| 0a896cc19c | |||
| 0bb548d0d1 | |||
| de1584eab8 | |||
| 60e2026850 | |||
| 5098dda376 | |||
| 35e47f866a | |||
| 199d89c1a6 | |||
| be543737a9 | |||
| 0b6fa4ab06 | |||
| 8f1b2fa226 | |||
| 5010175b84 | |||
| 0ac42608f7 | |||
| 7f3ef2cb8b | |||
| 16c4b8aa43 | |||
| 4078b32e9d | |||
| 90d33f56c3 | |||
| b0e5ab7533 | |||
| 526bd81b50 | |||
| a1fb84f14d | |||
| bc520edd19 | |||
| 5f4f0d3086 | |||
| a68c61b2f0 | |||
| 2a6978c349 | |||
| 1c5425c9eb | |||
| 2338a13749 | |||
| 6df6fe03fa | |||
| b0217ee04c | |||
| 2a54ce803e | |||
| 642bf9d113 | |||
| 31746e7baf | |||
| 48b1703725 | |||
| 72d6440de7 | |||
| eaed7ec86f | |||
| 02043d9109 | |||
| b9c0823abf | |||
| 61f30373d4 | |||
| 9889166afc | |||
| 35d5036aaa | |||
| 14f5b46ece | |||
| ac636b6649 | |||
| 52b8162d01 | |||
| 167b6974fe | |||
| 4bd86a6dfa | |||
| 5bbec118e4 | |||
| 473229f03a | |||
| af1fd5bd06 | |||
| c737a3adc9 | |||
| 8e2bf7795c | |||
| 4fd9e7f14f | |||
| 7859aca006 | |||
| 1403acd808 | |||
| e258de329c | |||
| 044bcf6f05 | |||
| 2c028f7c45 | |||
| 435200ec5a | |||
| 3d808d638a | |||
| bb0ee0b36c | |||
| b70ee742e8 | |||
| 4281111344 | |||
| 37e111d8a9 | |||
| 4202af96c8 | |||
| d0efd67d5e | |||
| aeb6c34d27 | |||
| a3f4fc7762 | |||
| 6517188b39 | |||
| c4acecd510 | |||
| c7d8bddf24 | |||
| 29c1d05540 | |||
| 55f04436c0 | |||
| 4ce2eaa919 | |||
| 489e064932 | |||
| 1369603ed9 | |||
| 79431d8b4b | |||
| 4591899df0 | |||
| dfa7d3f186 | |||
| a7c13ceacc | |||
| b2b3c80192 | |||
| badb678b69 | |||
| 5933b17b47 | |||
| 761912c3bd | |||
| 99f35b2588 | |||
| 51eb9f18f7 | |||
| 4b8ca31091 | |||
| 2706f1b02f | |||
| 6710476768 | |||
| de30095737 |
@@ -23,5 +23,7 @@ RUN set -x \
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
|
||||
USER node
|
||||
|
||||
EXPOSE 8080
|
||||
CMD [ "node", "./src/www" ]
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Trilium注意事项
|
||||
|
||||
[](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)以快速了解:
|
||||
|
||||

|
||||
|
||||
## 特性
|
||||
|
||||
* 笔记可以排列成任意深的树。单个笔记可以放在树中的多个位置(请参阅[克隆](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)
|
||||
* 支持编辑[使用源代码的笔记](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-revisions)
|
||||
* 笔记[属性](https://github.com/zadam/trilium/wiki/Attributes)可用于笔记组织,查询和高级[脚本编写](https://github.com/zadam/trilium/wiki/Scripts)
|
||||
* [同步](https://github.com/zadam/trilium/wiki/Synchronization)与自托管同步服务器
|
||||
* 具有按笔记粒度的强大的[笔记加密](https://github.com/zadam/trilium/wiki/Protected-notes)
|
||||
* [关系图](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/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内容
|
||||
|
||||
## 构建
|
||||
|
||||
Trilium是作为桌面应用程序(Linux和Windows)或服务器上托管的Web应用程序(Linux)提供的。Mac OS桌面版本可用,但[不受支持](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浏览器。
|
||||
|
||||
## 文档
|
||||
|
||||
[有关文档页面的完整列表,请参见Wiki。](https://github.com/zadam/trilium/wiki/)
|
||||
|
||||
您还可以阅读[个人知识库模式](https://github.com/zadam/trilium/wiki/Patterns-of-personal-knowledge-base),以获取有关如何使用Trilium的灵感。
|
||||
|
||||
## 贡献
|
||||
|
||||
使用基于浏览器的开发环境
|
||||
|
||||
[](https://gitpod.io/#https://github.com/zadam/trilium)
|
||||
|
||||
或在本地克隆并运行
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run start-server
|
||||
```
|
||||
|
||||
## 致谢
|
||||
|
||||
* [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)
|
||||
@@ -8,7 +8,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
|
||||
## Features
|
||||
|
||||
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://github.com/zadam/trilium/wiki/Cloning-notes))
|
||||
* Rich WYSIWYG note editing including e.g. tables and images with markdown [autoformat](https://github.com/zadam/trilium/wiki/Text-notes#autoformat)
|
||||
* Rich WYSIWYG note editing including e.g. tables, images and [math](https://github.com/zadam/trilium/wiki/Text-notes#math-support) with markdown [autoformat](https://github.com/zadam/trilium/wiki/Text-notes#autoformat)
|
||||
* Support for editing [notes with source code](https://github.com/zadam/trilium/wiki/Code-notes), including syntax highlighting
|
||||
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation), full text search and [note hoisting](https://github.com/zadam/trilium/wiki/Note-hoisting)
|
||||
* Seamless [note versioning](https://github.com/zadam/trilium/wiki/Note-revisions)
|
||||
@@ -54,4 +54,4 @@ npm run start-server
|
||||
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team
|
||||
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
|
||||
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
|
||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
|
||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
|
||||
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
DELETE FROM options WHERE name IN (
|
||||
'noteInfoWidget',
|
||||
'attributesWidget',
|
||||
'linkMapWidget',
|
||||
'noteRevisionsWidget',
|
||||
'whatLinksHereWidget',
|
||||
'codeNotesMimeTypes',
|
||||
'similarNotesWidget',
|
||||
'editedNotesWidget',
|
||||
'calendarWidget',
|
||||
'sidebarMinWidth',
|
||||
'sidebarWidthPercent',
|
||||
'showSidebarInNewTab',
|
||||
'hoistedNoteId'
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
const repository = require('../../src/services/repository');
|
||||
|
||||
module.exports = () => {
|
||||
for (const note of repository.getEntities("SELECT * FROM notes WHERE type = 'search' AND isProtected = 0 AND isDeleted = 0")) {
|
||||
try {
|
||||
let origContent = note.getJsonContent();
|
||||
|
||||
if (!origContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
note.addLabel('searchString', origContent.searchString);
|
||||
|
||||
note.setContent('');
|
||||
|
||||
note.mime = 'text/plain';
|
||||
note.save();
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`Changing note content for note ${note.noteId} failed with: ${e.message} ${e.stack}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
CREATE TABLE IF NOT EXISTS "mig_entity_changes" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entityName` TEXT NOT NULL,
|
||||
`entityId` TEXT NOT NULL,
|
||||
`hash` TEXT NOT NULL,
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`isErased` INT NOT NULL,
|
||||
`utcDateChanged` TEXT NOT NULL,
|
||||
`isSynced` INTEGER NOT NULL);
|
||||
|
||||
INSERT INTO mig_entity_changes (id, entityName, entityId, hash, sourceId, isSynced, utcDateChanged, isErased)
|
||||
SELECT id, entityName, entityId, '', sourceId, isSynced, utcChangedDate, 0 FROM entity_changes;
|
||||
|
||||
UPDATE mig_entity_changes SET isErased = (SELECT isErased FROM notes WHERE noteId = entityId) WHERE entityName = 'notes';
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM notes WHERE noteId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'notes';
|
||||
|
||||
UPDATE mig_entity_changes SET isErased = (SELECT isErased FROM notes WHERE noteId = entityId) WHERE entityName = 'note_contents';
|
||||
|
||||
UPDATE mig_entity_changes SET isErased = (
|
||||
SELECT isErased
|
||||
FROM attributes
|
||||
JOIN notes USING(noteId)
|
||||
WHERE attributeId = entityId
|
||||
) WHERE entityName = 'attributes';
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM attributes WHERE attributeId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'attributes';
|
||||
|
||||
UPDATE mig_entity_changes SET isErased = (
|
||||
SELECT isErased
|
||||
FROM branches
|
||||
JOIN notes USING(noteId)
|
||||
WHERE branchId = entityId
|
||||
) WHERE entityName = 'branches';
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM branches WHERE branchId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'branches';
|
||||
|
||||
UPDATE mig_entity_changes SET isErased = (
|
||||
SELECT isErased
|
||||
FROM note_revisions
|
||||
WHERE noteRevisionId = entityId
|
||||
) WHERE entityName = 'note_revisions';
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM note_revisions WHERE noteRevisionId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'note_revisions';
|
||||
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateCreated FROM api_tokens WHERE apiTokenId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'api_tokens';
|
||||
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM note_contents WHERE noteId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'note_contents';
|
||||
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM note_revision_contents WHERE noteRevisionId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'note_revision_contents';
|
||||
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateModified FROM options WHERE name = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'options';
|
||||
|
||||
UPDATE mig_entity_changes SET utcDateChanged = COALESCE((SELECT utcDateCreated FROM recent_notes WHERE noteId = entityId), '2020-12-14 14:07:05.165Z') WHERE entityName = 'options';
|
||||
|
||||
DROP TABLE entity_changes;
|
||||
ALTER TABLE mig_entity_changes RENAME TO entity_changes;
|
||||
|
||||
CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" (
|
||||
`entityName`,
|
||||
`entityId`
|
||||
);
|
||||
|
||||
DELETE FROM attributes WHERE noteId IN (SELECT noteId FROM notes WHERE isErased = 1);
|
||||
DELETE FROM branches WHERE noteId IN (SELECT noteId FROM notes WHERE isErased = 1);
|
||||
DELETE FROM note_contents WHERE noteId IN (SELECT noteId FROM notes WHERE isErased = 1);
|
||||
DELETE FROM note_revision_contents WHERE noteRevisionId IN (
|
||||
SELECT noteRevisionId FROM note_revisions WHERE isErased = 1
|
||||
);
|
||||
DELETE FROM note_revisions WHERE isErased = 1;
|
||||
DELETE FROM notes WHERE isErased = 1;
|
||||
|
||||
UPDATE entity_changes SET isErased = COALESCE((SELECT isErased FROM entity_changes AS sub WHERE sub.entityId = entity_changes.entityId AND sub.entityName = 'note_revisions'), 0) WHERE entityName = 'note_revision_contents';
|
||||
@@ -0,0 +1,198 @@
|
||||
CREATE TABLE IF NOT EXISTS "mig_entity_changes" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entityName` TEXT NOT NULL,
|
||||
`entityId` TEXT NOT NULL,
|
||||
`hash` TEXT NOT NULL,
|
||||
`isErased` INT NOT NULL,
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`isSynced` INTEGER NOT NULL,
|
||||
`utcDateChanged` TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO mig_entity_changes (id, entityName, entityId, hash, sourceId, isSynced, isErased, utcDateChanged)
|
||||
SELECT id, entityName, entityId, '', sourceId, isSynced, isErased, utcDateChanged FROM entity_changes;
|
||||
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM api_tokens WHERE apiTokenId = entityId), '') WHERE entityName = 'api_tokens';
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM attributes WHERE attributeId = entityId), '') WHERE entityName = 'attributes';
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM branches WHERE branchId = entityId), '') WHERE entityName = 'branches';
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM notes WHERE noteId = entityId), '') WHERE entityName = 'notes';
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM note_contents WHERE noteId = entityId), '') WHERE entityName = 'note_contents';
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM note_revisions WHERE noteRevisionId = entityId), '') WHERE entityName = 'note_revisions';
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM note_revision_contents WHERE noteRevisionId = entityId), '') WHERE entityName = 'note_revision_contents';
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM options WHERE name = entityId), '') WHERE entityName = 'options';
|
||||
UPDATE mig_entity_changes SET hash = COALESCE((SELECT hash FROM recent_notes WHERE noteId = entityId), '') WHERE entityName = 'recent_notes';
|
||||
|
||||
DROP TABLE entity_changes;
|
||||
ALTER TABLE mig_entity_changes RENAME TO entity_changes;
|
||||
|
||||
CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" (
|
||||
`entityName`,
|
||||
`entityId`
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_api_tokens"
|
||||
(
|
||||
apiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0);
|
||||
|
||||
INSERT INTO mig_api_tokens (apiTokenId, token, utcDateCreated, isDeleted)
|
||||
SELECT apiTokenId, token, utcDateCreated, isDeleted FROM api_tokens;
|
||||
|
||||
DROP TABLE api_tokens;
|
||||
ALTER TABLE mig_api_tokens RENAME TO api_tokens;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_attributes"
|
||||
(
|
||||
attributeId TEXT not null primary key,
|
||||
noteId TEXT not null,
|
||||
type TEXT not null,
|
||||
name TEXT not null,
|
||||
value TEXT default '' not null,
|
||||
position INT default 0 not null,
|
||||
utcDateCreated TEXT not null,
|
||||
utcDateModified TEXT not null,
|
||||
isDeleted INT not null,
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
isInheritable int DEFAULT 0 NULL);
|
||||
|
||||
INSERT INTO mig_attributes (attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, deleteId, isInheritable)
|
||||
SELECT attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, deleteId, isInheritable FROM attributes;
|
||||
|
||||
DROP TABLE attributes;
|
||||
ALTER TABLE mig_attributes RENAME TO attributes;
|
||||
|
||||
CREATE INDEX IDX_attributes_name_value
|
||||
on attributes (name, value);
|
||||
CREATE INDEX IDX_attributes_noteId_index
|
||||
on attributes (noteId);
|
||||
CREATE INDEX IDX_attributes_value_index
|
||||
on attributes (value);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_branches" (
|
||||
`branchId` TEXT NOT NULL,
|
||||
`noteId` TEXT NOT NULL,
|
||||
`parentNoteId` TEXT NOT NULL,
|
||||
`notePosition` INTEGER NOT NULL,
|
||||
`prefix` TEXT,
|
||||
`isExpanded` INTEGER NOT NULL DEFAULT 0,
|
||||
`isDeleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
PRIMARY KEY(`branchId`));
|
||||
|
||||
INSERT INTO mig_branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, deleteId, utcDateModified, utcDateCreated)
|
||||
SELECT branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, deleteId, utcDateModified, utcDateCreated FROM branches;
|
||||
|
||||
DROP TABLE branches;
|
||||
ALTER TABLE mig_branches RENAME TO branches;
|
||||
|
||||
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_notes" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT NOT NULL DEFAULT "note",
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`type` TEXT NOT NULL DEFAULT 'text',
|
||||
`mime` TEXT NOT NULL DEFAULT 'text/html',
|
||||
`isDeleted` INT NOT NULL DEFAULT 0,
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`utcDateCreated` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`));
|
||||
|
||||
INSERT INTO mig_notes (noteId, title, isProtected, type, mime, isDeleted, deleteId, dateCreated, dateModified, utcDateCreated, utcDateModified)
|
||||
SELECT noteId, title, isProtected, type, mime, isDeleted, deleteId, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes;
|
||||
|
||||
DROP TABLE notes;
|
||||
ALTER TABLE mig_notes RENAME TO notes;
|
||||
|
||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
|
||||
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
|
||||
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
|
||||
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
|
||||
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
|
||||
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
|
||||
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_note_contents" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`content` TEXT NULL DEFAULT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`)
|
||||
);
|
||||
|
||||
INSERT INTO mig_note_contents (noteId, content, dateModified, utcDateModified)
|
||||
SELECT noteId, content, dateModified, utcDateModified FROM note_contents;
|
||||
|
||||
DROP TABLE note_contents;
|
||||
ALTER TABLE mig_note_contents RENAME TO note_contents;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT NOT NULL,
|
||||
type TEXT DEFAULT '' NOT NULL,
|
||||
mime TEXT DEFAULT '' NOT NULL,
|
||||
`title` TEXT,
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`utcDateLastEdited` TEXT NOT NULL,
|
||||
`utcDateCreated` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
`dateLastEdited` TEXT NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL);
|
||||
|
||||
INSERT INTO mig_note_revisions (noteRevisionId, noteId, type, mime, title, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated)
|
||||
SELECT noteRevisionId, noteId, type, mime, title, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated FROM note_revisions;
|
||||
|
||||
DROP TABLE note_revisions;
|
||||
ALTER TABLE mig_note_revisions RENAME TO note_revisions;
|
||||
|
||||
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (`noteId`);
|
||||
CREATE INDEX `IDX_note_revisions_utcDateCreated` ON `note_revisions` (`utcDateCreated`);
|
||||
CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`);
|
||||
CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`);
|
||||
CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
||||
`content` TEXT,
|
||||
`utcDateModified` TEXT NOT NULL);
|
||||
|
||||
INSERT INTO mig_note_revision_contents (noteRevisionId, content, utcDateModified)
|
||||
SELECT noteRevisionId, content, utcDateModified FROM note_revision_contents;
|
||||
|
||||
DROP TABLE note_revision_contents;
|
||||
ALTER TABLE mig_note_revision_contents RENAME TO note_revision_contents;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_options"
|
||||
(
|
||||
name TEXT not null PRIMARY KEY,
|
||||
value TEXT,
|
||||
isSynced INTEGER default 0 not null,
|
||||
utcDateCreated TEXT not null,
|
||||
utcDateModified TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO mig_options (name, value, isSynced, utcDateCreated, utcDateModified)
|
||||
SELECT name, value, isSynced, utcDateCreated, utcDateModified FROM options;
|
||||
|
||||
DROP TABLE options;
|
||||
ALTER TABLE mig_options RENAME TO options;
|
||||
|
||||
CREATE TABLE mig_recent_notes
|
||||
(
|
||||
noteId TEXT not null primary key,
|
||||
notePath TEXT not null,
|
||||
utcDateCreated TEXT not null,
|
||||
isDeleted INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
INSERT INTO mig_recent_notes (noteId, notePath, utcDateCreated, isDeleted)
|
||||
SELECT noteId, notePath, utcDateCreated, isDeleted FROM recent_notes;
|
||||
|
||||
DROP TABLE recent_notes;
|
||||
ALTER TABLE mig_recent_notes RENAME TO recent_notes;
|
||||
@@ -0,0 +1,2 @@
|
||||
UPDATE options SET name = 'eraseEntitiesAfterTimeInSeconds' WHERE name = 'eraseNotesAfterTimeInSeconds';
|
||||
UPDATE entity_changes SET entityId = 'eraseEntitiesAfterTimeInSeconds' WHERE entityName = 'options' AND entityId = 'eraseNotesAfterTimeInSeconds';
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE IF NOT EXISTS "mig_attributes"
|
||||
(
|
||||
attributeId TEXT not null primary key,
|
||||
noteId TEXT not null,
|
||||
type TEXT not null,
|
||||
name TEXT not null,
|
||||
value TEXT default '' not null,
|
||||
position INT default 0 not null,
|
||||
utcDateModified TEXT not null,
|
||||
isDeleted INT not null,
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
isInheritable int DEFAULT 0 NULL);
|
||||
|
||||
INSERT INTO mig_attributes (attributeId, noteId, type, name, value, position, utcDateModified, isDeleted, deleteId, isInheritable)
|
||||
SELECT attributeId, noteId, type, name, value, position, utcDateModified, isDeleted, deleteId, isInheritable FROM attributes;
|
||||
|
||||
DROP TABLE attributes;
|
||||
ALTER TABLE mig_attributes RENAME TO attributes;
|
||||
|
||||
CREATE INDEX IDX_attributes_name_value
|
||||
on attributes (name, value);
|
||||
CREATE INDEX IDX_attributes_noteId_index
|
||||
on attributes (noteId);
|
||||
CREATE INDEX IDX_attributes_value_index
|
||||
on attributes (value);
|
||||
@@ -0,0 +1,5 @@
|
||||
DELETE FROM options WHERE name = 'keyboardShortcutsCreateNoteIntoInbox'; -- in case there is already one which shouldn't (except for in-dev documents)
|
||||
UPDATE options SET name = 'keyboardShortcutsCreateNoteIntoInbox' WHERE name = 'keyboardShortcutsCreateNoteIntoDayNote';
|
||||
|
||||
DELETE FROM options WHERE name = 'keyboardShortcutsShowAttributes';
|
||||
DELETE FROM entity_changes WHERE entityName = 'options' AND entityId = 'keyboardShortcutsShowAttributes';
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IDX_notes_isDeleted;
|
||||
@@ -0,0 +1 @@
|
||||
VACUUM
|
||||
@@ -0,0 +1,2 @@
|
||||
UPDATE options SET name = 'keyboardShortcutsQuickSearch' WHERE name = 'keyboardShortcutsSearchNotes';
|
||||
UPDATE entity_changes SET entityId = 'keyboardShortcutsQuickSearch' WHERE entityId = 'keyboardShortcutsSearchNotes' AND entityName = 'options';
|
||||
@@ -0,0 +1,5 @@
|
||||
UPDATE entity_changes SET isSynced = COALESCE((
|
||||
SELECT options.isSynced
|
||||
FROM options
|
||||
WHERE options.name = entity_changes.entityId
|
||||
), 0) WHERE entityName = 'options';
|
||||
@@ -0,0 +1,14 @@
|
||||
DELETE FROM entity_changes WHERE entityName = 'recent_notes';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mig_recent_notes"
|
||||
(
|
||||
noteId TEXT not null primary key,
|
||||
notePath TEXT not null,
|
||||
utcDateCreated TEXT not null
|
||||
);
|
||||
|
||||
INSERT INTO mig_recent_notes (noteId, notePath, utcDateCreated)
|
||||
SELECT noteId, notePath, utcDateCreated FROM recent_notes;
|
||||
|
||||
DROP TABLE recent_notes;
|
||||
ALTER TABLE mig_recent_notes RENAME TO recent_notes;
|
||||
+69
-83
@@ -1,31 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS "entity_changes" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entityName` TEXT NOT NULL,
|
||||
`entityId` TEXT NOT NULL,
|
||||
`hash` TEXT NOT NULL,
|
||||
`isErased` INT NOT NULL,
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`isSynced` INTEGER NOT NULL,
|
||||
`utcDateChanged` TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "api_tokens"
|
||||
(
|
||||
apiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0,
|
||||
hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "options"
|
||||
(
|
||||
name TEXT not null PRIMARY KEY,
|
||||
value TEXT,
|
||||
isSynced INTEGER default 0 not null,
|
||||
hash TEXT default "" not null,
|
||||
utcDateCreated TEXT not null,
|
||||
utcDateModified TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE recent_notes
|
||||
(
|
||||
noteId TEXT not null primary key,
|
||||
notePath TEXT not null,
|
||||
hash TEXT default "" not null,
|
||||
utcDateCreated TEXT not null,
|
||||
isDeleted INT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
||||
`content` TEXT,
|
||||
hash TEXT DEFAULT '' NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL);
|
||||
apiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`branchId` TEXT NOT NULL,
|
||||
`noteId` TEXT NOT NULL,
|
||||
@@ -37,10 +25,49 @@ CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
hash TEXT DEFAULT "" NOT NULL,
|
||||
PRIMARY KEY(`branchId`));
|
||||
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT NOT NULL DEFAULT "note",
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`type` TEXT NOT NULL DEFAULT 'text',
|
||||
`mime` TEXT NOT NULL DEFAULT 'text/html',
|
||||
`isDeleted` INT NOT NULL DEFAULT 0,
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`utcDateCreated` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`));
|
||||
CREATE TABLE IF NOT EXISTS "note_contents" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`content` TEXT NULL DEFAULT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT NOT NULL,
|
||||
type TEXT DEFAULT '' NOT NULL,
|
||||
mime TEXT DEFAULT '' NOT NULL,
|
||||
`title` TEXT,
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`utcDateLastEdited` TEXT NOT NULL,
|
||||
`utcDateCreated` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
`dateLastEdited` TEXT NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
||||
`content` TEXT,
|
||||
`utcDateModified` TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "options"
|
||||
(
|
||||
name TEXT not null PRIMARY KEY,
|
||||
value TEXT,
|
||||
isSynced INTEGER default 0 not null,
|
||||
utcDateCreated TEXT not null,
|
||||
utcDateModified TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "attributes"
|
||||
(
|
||||
attributeId TEXT not null primary key,
|
||||
@@ -49,77 +76,36 @@ CREATE TABLE IF NOT EXISTS "attributes"
|
||||
name TEXT not null,
|
||||
value TEXT default '' not null,
|
||||
position INT default 0 not null,
|
||||
utcDateCreated TEXT not null,
|
||||
utcDateModified TEXT not null,
|
||||
isDeleted INT not null,
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
hash TEXT default "" not null,
|
||||
isInheritable int DEFAULT 0 NULL);
|
||||
CREATE INDEX IDX_attributes_name_value
|
||||
on attributes (name, value);
|
||||
CREATE INDEX IDX_attributes_noteId_index
|
||||
on attributes (noteId);
|
||||
CREATE INDEX IDX_attributes_value_index
|
||||
on attributes (value);
|
||||
CREATE TABLE IF NOT EXISTS "entity_changes" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entityName` TEXT NOT NULL,
|
||||
`entityId` TEXT NOT NULL,
|
||||
`sourceId` TEXT NOT NULL,
|
||||
`isSynced` INTEGER default 0 not null,
|
||||
`utcChangedDate` TEXT NOT NULL);
|
||||
CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" (
|
||||
`entityName`,
|
||||
`entityId`
|
||||
);
|
||||
CREATE INDEX `IDX_entityChanges_utcChangedDate` ON "entity_changes" (
|
||||
`utcChangedDate`
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT NOT NULL DEFAULT "note",
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`type` TEXT NOT NULL DEFAULT 'text',
|
||||
`mime` TEXT NOT NULL DEFAULT 'text/html',
|
||||
`hash` TEXT DEFAULT "" NOT NULL,
|
||||
`isDeleted` INT NOT NULL DEFAULT 0,
|
||||
`deleteId` TEXT DEFAULT NULL,
|
||||
`isErased` INT NOT NULL DEFAULT 0,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`utcDateCreated` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`));
|
||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
|
||||
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (`noteId`,`parentNoteId`);
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
|
||||
CREATE INDEX `IDX_notes_type` ON `notes` (`type`);
|
||||
CREATE INDEX `IDX_notes_dateCreated` ON `notes` (`dateCreated`);
|
||||
CREATE INDEX `IDX_notes_dateModified` ON `notes` (`dateModified`);
|
||||
CREATE INDEX `IDX_notes_utcDateModified` ON `notes` (`utcDateModified`);
|
||||
CREATE INDEX `IDX_notes_utcDateCreated` ON `notes` (`utcDateCreated`);
|
||||
CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT,
|
||||
`isErased` INT NOT NULL DEFAULT 0,
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`utcDateLastEdited` TEXT NOT NULL,
|
||||
`utcDateCreated` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
`dateLastEdited` TEXT NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
type TEXT DEFAULT '' NOT NULL,
|
||||
mime TEXT DEFAULT '' NOT NULL,
|
||||
hash TEXT DEFAULT '' NOT NULL);
|
||||
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (`noteId`);
|
||||
CREATE INDEX `IDX_note_revisions_utcDateCreated` ON `note_revisions` (`utcDateCreated`);
|
||||
CREATE INDEX `IDX_note_revisions_utcDateLastEdited` ON `note_revisions` (`utcDateLastEdited`);
|
||||
CREATE INDEX `IDX_note_revisions_dateCreated` ON `note_revisions` (`dateCreated`);
|
||||
CREATE INDEX `IDX_note_revisions_dateLastEdited` ON `note_revisions` (`dateLastEdited`);
|
||||
CREATE TABLE IF NOT EXISTS "note_contents" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`content` TEXT NULL DEFAULT NULL,
|
||||
`hash` TEXT DEFAULT "" NOT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`noteId`)
|
||||
CREATE INDEX IDX_attributes_name_value
|
||||
on attributes (name, value);
|
||||
CREATE INDEX IDX_attributes_noteId_index
|
||||
on attributes (noteId);
|
||||
CREATE INDEX IDX_attributes_value_index
|
||||
on attributes (value);
|
||||
CREATE TABLE IF NOT EXISTS "recent_notes"
|
||||
(
|
||||
noteId TEXT not null primary key,
|
||||
notePath TEXT not null,
|
||||
utcDateCreated TEXT not null
|
||||
);
|
||||
|
||||
+4
-4
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* CKEditor 5 (v23.1.0) content styles.
|
||||
* Generated on Thu, 29 Oct 2020 12:17:48 GMT.
|
||||
* CKEditor 5 (v24.0.0) content styles.
|
||||
* Generated on Thu, 10 Dec 2020 08:15:26 GMT.
|
||||
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
--ck-todo-list-checkmark-size: 16px;
|
||||
}
|
||||
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized img {
|
||||
width: 100%;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized > figcaption {
|
||||
display: block;
|
||||
}
|
||||
/* ckeditor5-image/theme/imagestyle.css */
|
||||
.ck-content .image-style-side {
|
||||
float: right;
|
||||
@@ -69,20 +83,6 @@
|
||||
font-size: .75em;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized img {
|
||||
width: 100%;
|
||||
}
|
||||
/* ckeditor5-image/theme/imageresize.css */
|
||||
.ck-content .image.image_resized > figcaption {
|
||||
display: block;
|
||||
}
|
||||
/* ckeditor5-highlight/theme/highlight.css */
|
||||
.ck-content .marker-yellow {
|
||||
background-color: var(--ck-highlight-marker-yellow);
|
||||
@@ -215,6 +215,13 @@
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
/* ckeditor5-media-embed/theme/mediaembed.css */
|
||||
.ck-content .media {
|
||||
clear: both;
|
||||
margin: 1em 0;
|
||||
display: block;
|
||||
min-width: 15em;
|
||||
}
|
||||
/* ckeditor5-list/theme/todolist.css */
|
||||
.ck-content .todo-list {
|
||||
list-style: none;
|
||||
@@ -282,17 +289,11 @@
|
||||
.ck-content .todo-list .todo-list__label .todo-list__label__description {
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* ckeditor5-media-embed/theme/mediaembed.css */
|
||||
.ck-content .media {
|
||||
clear: both;
|
||||
margin: 1em 0;
|
||||
display: block;
|
||||
min-width: 15em;
|
||||
}
|
||||
/* ckeditor5-html-embed/theme/htmlembed.css */
|
||||
.ck-content .raw-html-embed {
|
||||
margin: 1em auto;
|
||||
min-width: 15em;
|
||||
font-style: normal;
|
||||
}
|
||||
/* ckeditor5-horizontal-line/theme/horizontalline.css */
|
||||
.ck-content hr {
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*! jQuery Fancytree Plugin - 2.37.0 - 2020-09-11T18:58:08Z
|
||||
/*! jQuery Fancytree Plugin - 2.38.0 - 2021-02-09T20:03:49Z
|
||||
* https://github.com/mar10/fancytree
|
||||
* Copyright (c) 2020 Martin Wendt; Licensed MIT
|
||||
* Copyright (c) 2021 Martin Wendt; Licensed MIT
|
||||
*/
|
||||
/*! jQuery UI - v1.12.1 - 2018-05-20
|
||||
* http://jqueryui.com
|
||||
@@ -1361,12 +1361,12 @@ var uniqueId = $.fn.extend( {
|
||||
* Tree view control with support for lazy loading and much more.
|
||||
* https://github.com/mar10/fancytree/
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
/** Core Fancytree module.
|
||||
@@ -1530,8 +1530,16 @@ var uniqueId = $.fn.extend( {
|
||||
// TODO: see qunit.js extractStacktrace()
|
||||
if (!cond) {
|
||||
msg = msg ? ": " + msg : "";
|
||||
msg = "Fancytree assertion failed" + msg;
|
||||
|
||||
// consoleApply("assert", [!!cond, msg]);
|
||||
$.error("Fancytree assertion failed" + msg);
|
||||
|
||||
// #1041: Raised exceptions may not be visible in the browser
|
||||
// console if inside promise chains, so we also print directly:
|
||||
$.ui.fancytree.error(msg);
|
||||
|
||||
// Throw exception:
|
||||
$.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5259,6 +5267,13 @@ var uniqueId = $.fn.extend( {
|
||||
// visit siblings
|
||||
siblings = parent.children;
|
||||
nextIdx = siblings.indexOf(node) + siblingOfs;
|
||||
_assert(
|
||||
nextIdx >= 0,
|
||||
"Could not find " +
|
||||
node +
|
||||
" in parent's children: " +
|
||||
parent
|
||||
);
|
||||
|
||||
for (i = nextIdx; i < siblings.length; i++) {
|
||||
node = siblings[i];
|
||||
@@ -6660,7 +6675,17 @@ var uniqueId = $.fn.extend( {
|
||||
if (isActive === flag) {
|
||||
// Nothing to do
|
||||
return _getResolvedPromise(node);
|
||||
} else if (
|
||||
}
|
||||
// #1042: don't scroll between mousedown/-up when clicking an embedded link
|
||||
if (
|
||||
scroll &&
|
||||
ctx.originalEvent &&
|
||||
$(ctx.originalEvent.target).is("a,:checkbox")
|
||||
) {
|
||||
node.info("Not scrolling while clicking an embedded link.");
|
||||
scroll = false;
|
||||
}
|
||||
if (
|
||||
flag &&
|
||||
!noEvents &&
|
||||
this._triggerNodeEvent(
|
||||
@@ -8070,7 +8095,7 @@ var uniqueId = $.fn.extend( {
|
||||
{
|
||||
/** Version number `"MAJOR.MINOR.PATCH"`
|
||||
* @type {string} */
|
||||
version: "2.37.0", // Set to semver by 'grunt release'
|
||||
version: "2.38.0", // Set to semver by 'grunt release'
|
||||
/** @type {string}
|
||||
* @description `"production" for release builds` */
|
||||
buildType: "production", // Set to 'production' by 'grunt build'
|
||||
@@ -8700,13 +8725,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Add a child counter bubble to tree nodes.
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
// To keep the global namespace clean, we wrap everything in a closure.
|
||||
@@ -8825,7 +8850,7 @@ var uniqueId = $.fn.extend( {
|
||||
// Every extension must be registered by a unique name.
|
||||
name: "childcounter",
|
||||
// Version information should be compliant with [semver](http://semver.org)
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
|
||||
// Extension specific options and their defaults.
|
||||
// This options will be available as `tree.options.childcounter.hideExpanded`
|
||||
@@ -8931,13 +8956,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Support faster lookup of nodes by key and shared ref-ids.
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -8958,13 +8983,8 @@ var uniqueId = $.fn.extend( {
|
||||
/*******************************************************************************
|
||||
* Private functions and variables
|
||||
*/
|
||||
function _assert(cond, msg) {
|
||||
// TODO: see qunit.js extractStacktrace()
|
||||
if (!cond) {
|
||||
msg = msg ? ": " + msg : "";
|
||||
$.error("Assertion failed" + msg);
|
||||
}
|
||||
}
|
||||
|
||||
var _assert = $.ui.fancytree.assert;
|
||||
|
||||
/* Return first occurrence of member from array. */
|
||||
function _removeArrayMember(arr, elem) {
|
||||
@@ -9295,7 +9315,7 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "clones",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers
|
||||
@@ -9452,13 +9472,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Drag-and-drop support (native HTML5).
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -10056,7 +10076,7 @@ var uniqueId = $.fn.extend( {
|
||||
// data store list of items representing dragged data can be
|
||||
// enumerated, but the data itself is unavailable and no new
|
||||
// data can be added.
|
||||
var nodeData = node.toDict();
|
||||
var nodeData = node.toDict(true, dndOpts.sourceCopyHook);
|
||||
nodeData.treeId = node.tree._id;
|
||||
json = JSON.stringify(nodeData);
|
||||
try {
|
||||
@@ -10476,7 +10496,7 @@ var uniqueId = $.fn.extend( {
|
||||
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "dnd5",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering
|
||||
@@ -10498,6 +10518,7 @@ var uniqueId = $.fn.extend( {
|
||||
scrollSensitivity: 20, // Active top/bottom margin in pixel
|
||||
scrollSpeed: 5, // Pixel per event
|
||||
setTextTypeJson: false, // Allow dragging of nodes to different IE windows
|
||||
sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38
|
||||
// Events (drag support)
|
||||
dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag
|
||||
dragDrag: $.noop, // Callback(sourceNode, data)
|
||||
@@ -10607,13 +10628,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Make node titles editable.
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -10905,7 +10926,7 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "edit",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
adjustWidthOfs: 4, // null: don't adjust input size to content
|
||||
@@ -11011,13 +11032,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Remove or highlight tree nodes, based on a filter.
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -11040,8 +11061,9 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
|
||||
var KeyNoData = "__not_found__",
|
||||
escapeHtml = $.ui.fancytree.escapeHtml;
|
||||
|
||||
escapeHtml = $.ui.fancytree.escapeHtml,
|
||||
exoticStartChar = "\uFFF7",
|
||||
exoticEndChar = "\uFFF8";
|
||||
function _escapeRegex(str) {
|
||||
return (str + "").replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
|
||||
}
|
||||
@@ -11055,6 +11077,47 @@ var uniqueId = $.fn.extend( {
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Marks the matching charecters of `text` either by `mark` or
|
||||
* by exotic*Chars (if `escapeTitles` is `true`) based on `regexMatchArray`
|
||||
* which is an array of matching groups.
|
||||
* @param {string} text
|
||||
* @param {RegExpMatchArray} regexMatchArray
|
||||
*/
|
||||
function _markFuzzyMatchedChars(text, regexMatchArray, escapeTitles) {
|
||||
// It is extremely infuriating that we can not use `let` or `const` or arrow functions.
|
||||
// Damn you IE!!!
|
||||
var matchingIndices = [];
|
||||
// get the indices of matched characters (Iterate through `RegExpMatchArray`)
|
||||
for (
|
||||
var _matchingArrIdx = 1;
|
||||
_matchingArrIdx < regexMatchArray.length;
|
||||
_matchingArrIdx++
|
||||
) {
|
||||
var _mIdx =
|
||||
// get matching char index by cumulatively adding
|
||||
// the matched group length
|
||||
regexMatchArray[_matchingArrIdx].length +
|
||||
(_matchingArrIdx === 1 ? 0 : 1) +
|
||||
(matchingIndices[matchingIndices.length - 1] || 0);
|
||||
matchingIndices.push(_mIdx);
|
||||
}
|
||||
// Map each `text` char to its position and store in `textPoses`.
|
||||
var textPoses = text.split("");
|
||||
if (escapeTitles) {
|
||||
// If escaping the title, then wrap the matchng char within exotic chars
|
||||
matchingIndices.forEach(function(v) {
|
||||
textPoses[v] = exoticStartChar + textPoses[v] + exoticEndChar;
|
||||
});
|
||||
} else {
|
||||
// Otherwise, Wrap the matching chars within `mark`.
|
||||
matchingIndices.forEach(function(v) {
|
||||
textPoses[v] = "<mark>" + textPoses[v] + "</mark>";
|
||||
});
|
||||
}
|
||||
// Join back the modified `textPoses` to create final highlight markup.
|
||||
return textPoses.join("");
|
||||
}
|
||||
$.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function(
|
||||
filter,
|
||||
branchMode,
|
||||
@@ -11064,6 +11127,8 @@ var uniqueId = $.fn.extend( {
|
||||
statusNode,
|
||||
re,
|
||||
reHighlight,
|
||||
reExoticStartChar,
|
||||
reExoticEndChar,
|
||||
temp,
|
||||
prevEnableUpdate,
|
||||
count = 0,
|
||||
@@ -11087,14 +11152,29 @@ var uniqueId = $.fn.extend( {
|
||||
// See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905
|
||||
// and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed
|
||||
// and http://www.dustindiaz.com/autocomplete-fuzzy-matching
|
||||
match = filter.split("").reduce(function(a, b) {
|
||||
return a + "[^" + b + "]*" + b;
|
||||
});
|
||||
match = filter
|
||||
.split("")
|
||||
// Escaping the `filter` will not work because,
|
||||
// it gets further split into individual characters. So,
|
||||
// escape each character after splitting
|
||||
.map(_escapeRegex)
|
||||
.reduce(function(a, b) {
|
||||
// create capture groups for parts that comes before
|
||||
// the character
|
||||
return a + "([^" + b + "]*)" + b;
|
||||
}, "");
|
||||
} else {
|
||||
match = _escapeRegex(filter); // make sure a '.' is treated literally
|
||||
}
|
||||
re = new RegExp(".*" + match + ".*", "i");
|
||||
re = new RegExp(match, "i");
|
||||
reHighlight = new RegExp(_escapeRegex(filter), "gi");
|
||||
if (escapeTitles) {
|
||||
reExoticStartChar = new RegExp(
|
||||
_escapeRegex(exoticStartChar),
|
||||
"g"
|
||||
);
|
||||
reExoticEndChar = new RegExp(_escapeRegex(exoticEndChar), "g");
|
||||
}
|
||||
filter = function(node) {
|
||||
if (!node.title) {
|
||||
return false;
|
||||
@@ -11102,31 +11182,46 @@ var uniqueId = $.fn.extend( {
|
||||
var text = escapeTitles
|
||||
? node.title
|
||||
: extractHtmlText(node.title),
|
||||
res = !!re.test(text);
|
||||
|
||||
// `.match` instead of `.test` to get the capture groups
|
||||
res = text.match(re);
|
||||
if (res && opts.highlight) {
|
||||
if (escapeTitles) {
|
||||
// #740: we must not apply the marks to escaped entity names, e.g. `"`
|
||||
// Use some exotic characters to mark matches:
|
||||
temp = text.replace(reHighlight, function(s) {
|
||||
return "\uFFF7" + s + "\uFFF8";
|
||||
});
|
||||
if (opts.fuzzy) {
|
||||
temp = _markFuzzyMatchedChars(
|
||||
text,
|
||||
res,
|
||||
escapeTitles
|
||||
);
|
||||
} else {
|
||||
// #740: we must not apply the marks to escaped entity names, e.g. `"`
|
||||
// Use some exotic characters to mark matches:
|
||||
temp = text.replace(reHighlight, function(s) {
|
||||
return exoticStartChar + s + exoticEndChar;
|
||||
});
|
||||
}
|
||||
// now we can escape the title...
|
||||
node.titleWithHighlight = escapeHtml(temp)
|
||||
// ... and finally insert the desired `<mark>` tags
|
||||
.replace(/\uFFF7/g, "<mark>")
|
||||
.replace(/\uFFF8/g, "</mark>");
|
||||
.replace(reExoticStartChar, "<mark>")
|
||||
.replace(reExoticEndChar, "</mark>");
|
||||
} else {
|
||||
node.titleWithHighlight = text.replace(
|
||||
reHighlight,
|
||||
function(s) {
|
||||
return "<mark>" + s + "</mark>";
|
||||
}
|
||||
);
|
||||
if (opts.fuzzy) {
|
||||
node.titleWithHighlight = _markFuzzyMatchedChars(
|
||||
text,
|
||||
res
|
||||
);
|
||||
} else {
|
||||
node.titleWithHighlight = text.replace(
|
||||
reHighlight,
|
||||
function(s) {
|
||||
return "<mark>" + s + "</mark>";
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// node.debug("filter", escapeTitles, text, node.titleWithHighlight);
|
||||
}
|
||||
return res;
|
||||
return !!res;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11264,6 +11359,26 @@ var uniqueId = $.fn.extend( {
|
||||
return this._applyFilterImpl(filter, true, opts);
|
||||
};
|
||||
|
||||
/**
|
||||
* [ext-filter] Re-apply current filter.
|
||||
*
|
||||
* @returns {integer} count
|
||||
* @alias Fancytree#updateFilter
|
||||
* @requires jquery.fancytree.filter.js
|
||||
* @since 2.38
|
||||
*/
|
||||
$.ui.fancytree._FancytreeClass.prototype.updateFilter = function() {
|
||||
if (
|
||||
this.enableFilter &&
|
||||
this.lastFilterArgs &&
|
||||
this.options.filter.autoApply
|
||||
) {
|
||||
this._applyFilterImpl.apply(this, this.lastFilterArgs);
|
||||
} else {
|
||||
this.warn("updateFilter(): no filter active.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* [ext-filter] Reset the filter.
|
||||
*
|
||||
@@ -11355,7 +11470,7 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "filter",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
autoApply: true, // Re-apply last filter if lazy data is loaded
|
||||
@@ -11470,13 +11585,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Use glyph-fonts, ligature-fonts, or SVG icons instead of icon sprites.
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -11678,7 +11793,7 @@ var uniqueId = $.fn.extend( {
|
||||
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "glyph",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
preset: null, // 'awesome3', 'awesome4', 'bootstrap3', 'material'
|
||||
@@ -11828,13 +11943,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Support keyboard navigation for trees with embedded input controls.
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -11951,7 +12066,7 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "gridnav",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
autofocusInput: false, // Focus first embedded input if node gets activated
|
||||
@@ -12053,13 +12168,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Allow multiple selection of nodes by mouse or keyboard.
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -12088,7 +12203,7 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "multi",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
allowNoSelect: false, //
|
||||
@@ -12185,13 +12300,13 @@ var uniqueId = $.fn.extend( {
|
||||
*
|
||||
* @depends: js-cookie or jquery-cookie
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -12402,7 +12517,7 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "persist",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
cookieDelimiter: "~",
|
||||
@@ -12690,13 +12805,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Render tree as table (aka 'tree grid', 'table tree').
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -12717,12 +12832,7 @@ var uniqueId = $.fn.extend( {
|
||||
/******************************************************************************
|
||||
* Private functions and variables
|
||||
*/
|
||||
function _assert(cond, msg) {
|
||||
msg = msg || "";
|
||||
if (!cond) {
|
||||
$.error("Assertion failed " + msg);
|
||||
}
|
||||
}
|
||||
var _assert = $.ui.fancytree.assert;
|
||||
|
||||
function insertFirstChild(referenceNode, newNode) {
|
||||
referenceNode.insertBefore(newNode, referenceNode.firstChild);
|
||||
@@ -12779,7 +12889,7 @@ var uniqueId = $.fn.extend( {
|
||||
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "table",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx)
|
||||
@@ -13240,13 +13350,13 @@ var uniqueId = $.fn.extend( {
|
||||
*
|
||||
* @see http://jqueryui.com/themeroller/
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -13269,7 +13379,7 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "themeroller",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
activeClass: "ui-state-active", // Class added to active node
|
||||
@@ -13360,13 +13470,13 @@ var uniqueId = $.fn.extend( {
|
||||
* Support for 100% wide selection bars.
|
||||
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
*
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
*/
|
||||
|
||||
(function(factory) {
|
||||
@@ -13496,7 +13606,7 @@ var uniqueId = $.fn.extend( {
|
||||
*/
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "wide",
|
||||
version: "2.37.0",
|
||||
version: "2.38.0",
|
||||
// Default options for this extension.
|
||||
options: {
|
||||
iconWidth: null, // Adjust this if @fancy-icon-width != "16px"
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,12 +9,12 @@
|
||||
*
|
||||
* This section is automatically generated from the `skin-common.less` template.
|
||||
*
|
||||
* Copyright (c) 2008-2020, Martin Wendt (https://wwWendt.de)
|
||||
* Copyright (c) 2008-2021, Martin Wendt (https://wwWendt.de)
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*
|
||||
* @version 2.37.0
|
||||
* @date 2020-09-11T18:58:08Z
|
||||
* @version 2.38.0
|
||||
* @date 2021-02-09T20:03:49Z
|
||||
******************************************************************************/
|
||||
/*------------------------------------------------------------------------------
|
||||
* Helpers
|
||||
|
||||
Generated
+581
-545
File diff suppressed because it is too large
Load Diff
+27
-26
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.45.9",
|
||||
"version": "0.46.0-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -24,45 +24,46 @@
|
||||
"test-all": "npm run test && npm run test-es6"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "0.2.4",
|
||||
"axios": "0.20.0",
|
||||
"better-sqlite3": "7.1.1",
|
||||
"async-mutex": "0.3.0",
|
||||
"axios": "0.21.1",
|
||||
"better-sqlite3": "7.1.2",
|
||||
"body-parser": "1.19.0",
|
||||
"cls-hooked": "4.2.2",
|
||||
"commonmark": "0.29.2",
|
||||
"commonmark": "0.29.3",
|
||||
"cookie-parser": "1.4.5",
|
||||
"csurf": "1.11.0",
|
||||
"dayjs": "1.9.3",
|
||||
"ejs": "3.1.5",
|
||||
"electron-debug": "3.1.0",
|
||||
"electron-dl": "3.0.2",
|
||||
"dayjs": "1.10.4",
|
||||
"ejs": "3.1.6",
|
||||
"electron-debug": "3.2.0",
|
||||
"electron-dl": "3.0.3",
|
||||
"electron-find": "1.0.6",
|
||||
"electron-window-state": "5.0.3",
|
||||
"express": "4.17.1",
|
||||
"express-session": "1.17.1",
|
||||
"fs-extra": "9.0.1",
|
||||
"helmet": "4.1.1",
|
||||
"fs-extra": "9.1.0",
|
||||
"helmet": "4.4.1",
|
||||
"html": "1.0.0",
|
||||
"html2plaintext": "2.1.2",
|
||||
"http-proxy-agent": "4.0.1",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"image-type": "4.1.0",
|
||||
"ini": "1.3.5",
|
||||
"ini": "2.0.0",
|
||||
"is-animated": "^2.0.1",
|
||||
"is-svg": "4.2.1",
|
||||
"jimp": "0.16.1",
|
||||
"jsdom": "^16.4.0",
|
||||
"mime-types": "2.1.27",
|
||||
"mime-types": "2.1.28",
|
||||
"multer": "1.4.2",
|
||||
"node-abi": "2.19.1",
|
||||
"open": "7.3.0",
|
||||
"node-abi": "2.19.3",
|
||||
"open": "7.4.1",
|
||||
"portscanner": "2.2.0",
|
||||
"rand-token": "1.0.1",
|
||||
"request": "^2.88.2",
|
||||
"rimraf": "3.0.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.1.1",
|
||||
"sanitize-html": "2.3.2",
|
||||
"sax": "1.2.4",
|
||||
"semver": "7.3.2",
|
||||
"semver": "7.3.4",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.5.0",
|
||||
"stream-throttle": "^0.1.3",
|
||||
@@ -71,23 +72,23 @@
|
||||
"turndown": "7.0.0",
|
||||
"turndown-plugin-gfm": "1.0.2",
|
||||
"unescape": "1.0.1",
|
||||
"ws": "7.3.1",
|
||||
"ws": "7.4.3",
|
||||
"yauzl": "2.10.0",
|
||||
"yazl": "2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.2",
|
||||
"electron": "9.3.5",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "9.4.3",
|
||||
"electron-builder": "22.9.1",
|
||||
"electron-packager": "15.1.0",
|
||||
"electron-rebuild": "2.3.2",
|
||||
"electron-packager": "15.2.0",
|
||||
"electron-rebuild": "2.3.5",
|
||||
"esm": "3.2.25",
|
||||
"jasmine": "3.6.2",
|
||||
"jasmine": "3.6.4",
|
||||
"jsdoc": "3.6.6",
|
||||
"lorem-ipsum": "2.0.3",
|
||||
"rcedit": "2.2.0",
|
||||
"webpack": "5.1.3",
|
||||
"webpack-cli": "4.1.0"
|
||||
"rcedit": "3.0.0",
|
||||
"webpack": "5.21.2",
|
||||
"webpack-cli": "4.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron-installer-debian": "2.0.1"
|
||||
|
||||
@@ -46,7 +46,8 @@ class NoteBuilder {
|
||||
branchId: id(),
|
||||
noteId: childNoteBuilder.note.noteId,
|
||||
parentNoteId: this.note.noteId,
|
||||
prefix
|
||||
prefix,
|
||||
notePosition: 10
|
||||
});
|
||||
|
||||
return this;
|
||||
|
||||
@@ -12,8 +12,8 @@ describe("Search", () => {
|
||||
beforeEach(() => {
|
||||
noteCache.reset();
|
||||
|
||||
rootNote = new NoteBuilder(new Note(noteCache, {noteId: 'root', title: 'root'}));
|
||||
new Branch(noteCache, {branchId: 'root', noteId: 'root', parentNoteId: 'none'});
|
||||
rootNote = new NoteBuilder(new Note(noteCache, {noteId: 'root', title: 'root', type: 'text'}));
|
||||
new Branch(noteCache, {branchId: 'root', noteId: 'root', parentNoteId: 'none', notePosition: 10});
|
||||
});
|
||||
|
||||
it("simple path match", () => {
|
||||
@@ -113,6 +113,15 @@ describe("Search", () => {
|
||||
let searchResults = searchService.findNotesWithQuery('#capital=Vienna', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
|
||||
// case sensitivity:
|
||||
searchResults = searchService.findNotesWithQuery('#CAPITAL=VIENNA', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
|
||||
searchResults = searchService.findNotesWithQuery('#caPItal=vienNa', searchContext);
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("label comparison with full syntax", () => {
|
||||
@@ -472,14 +481,17 @@ describe("Search", () => {
|
||||
expect(searchResults.length).toEqual(expectedResultCount);
|
||||
}
|
||||
|
||||
test("type", "text", 6);
|
||||
test("type", "text", 7);
|
||||
test("TYPE", "TEXT", 7);
|
||||
test("type", "code", 0);
|
||||
|
||||
test("mime", "text/html", 6);
|
||||
test("mime", "application/json", 0);
|
||||
|
||||
test("isProtected", "false", 7);
|
||||
test("isProtected", "FALSE", 7);
|
||||
test("isProtected", "true", 0);
|
||||
test("isProtected", "TRUE", 0);
|
||||
|
||||
test("dateCreated", "'2020-05-14 12:11:42.001+0200'", 1);
|
||||
test("dateCreated", "wrong", 0);
|
||||
|
||||
@@ -24,12 +24,12 @@ describe("Value extractor", () => {
|
||||
let valueExtractor = new ValueExtractor(["note", "labels", "capital"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(austria)).toEqual("vienna");
|
||||
expect(valueExtractor.extract(austria)).toEqual("Vienna");
|
||||
|
||||
valueExtractor = new ValueExtractor(["#capital"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(austria)).toEqual("vienna");
|
||||
expect(valueExtractor.extract(austria)).toEqual("Vienna");
|
||||
});
|
||||
|
||||
it("parent/child property extraction", async () => {
|
||||
@@ -59,12 +59,12 @@ describe("Value extractor", () => {
|
||||
let valueExtractor = new ValueExtractor(["note", "relations", "neighbor", "labels", "capital"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(austria.note)).toEqual("prague");
|
||||
expect(valueExtractor.extract(austria.note)).toEqual("Prague");
|
||||
|
||||
valueExtractor = new ValueExtractor(["~neighbor", "labels", "capital"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(austria.note)).toEqual("prague");
|
||||
expect(valueExtractor.extract(austria.note)).toEqual("Prague");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-1
@@ -10,7 +10,6 @@ const FileStore = require('session-file-store')(session);
|
||||
const sessionSecret = require('./services/session_secret');
|
||||
const dataDir = require('./services/data_dir');
|
||||
require('./services/handlers');
|
||||
require('./services/hoisted_note_loader');
|
||||
require('./services/note_cache/note_cache_loader');
|
||||
|
||||
const app = express();
|
||||
@@ -24,6 +23,7 @@ app.use(helmet({
|
||||
contentSecurityPolicy: false
|
||||
}));
|
||||
|
||||
app.use(bodyParser.text({limit: '500mb'}));
|
||||
app.use(bodyParser.json({limit: '500mb'}));
|
||||
app.use(bodyParser.urlencoded({extended: false}));
|
||||
app.use(cookieParser());
|
||||
@@ -39,6 +39,7 @@ const sessionParser = session({
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
|
||||
},
|
||||
name: 'trilium.sid',
|
||||
store: new FileStore({
|
||||
ttl: 30 * 24 * 3600,
|
||||
path: dataDir.TRILIUM_DATA_DIR + '/sessions'
|
||||
|
||||
@@ -18,7 +18,6 @@ const promotedAttributeDefinitionParser = require("../services/promoted_attribut
|
||||
* @property {boolean} isInheritable - immutable
|
||||
* @property {boolean} isDeleted
|
||||
* @property {string|null} deleteId - ID identifying delete transaction
|
||||
* @property {string} utcDateCreated
|
||||
* @property {string} utcDateModified
|
||||
*
|
||||
* @extends Entity
|
||||
@@ -26,7 +25,7 @@ const promotedAttributeDefinitionParser = require("../services/promoted_attribut
|
||||
class Attribute extends Entity {
|
||||
static get entityName() { return "attributes"; }
|
||||
static get primaryKeyName() { return "attributeId"; }
|
||||
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted", "utcDateCreated"]; }
|
||||
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
@@ -103,15 +102,9 @@ class Attribute extends Entity {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.utcDateCreated) {
|
||||
this.utcDateCreated = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
createClone(type, name, value, isInheritable) {
|
||||
@@ -123,7 +116,6 @@ class Attribute extends Entity {
|
||||
position: this.position,
|
||||
isInheritable: isInheritable,
|
||||
isDeleted: false,
|
||||
utcDateCreated: this.utcDateCreated,
|
||||
utcDateModified: this.utcDateModified
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,9 +57,7 @@ class Branch extends Entity {
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
createClone(parentNoteId, notePosition) {
|
||||
|
||||
@@ -15,18 +15,13 @@ class Entity {
|
||||
}
|
||||
}
|
||||
|
||||
if ('isDeleted' in this) {
|
||||
if ('isDeleted' in this && this.constructor.entityName !== 'recent_notes') {
|
||||
this.isDeleted = !!this.isDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
this.generateIdIfNecessary();
|
||||
|
||||
const origHash = this.hash;
|
||||
|
||||
this.hash = this.generateHash();
|
||||
this.isChanged = origHash !== this.hash;
|
||||
}
|
||||
|
||||
generateIdIfNecessary() {
|
||||
@@ -45,6 +40,10 @@ class Entity {
|
||||
return utils.hash(contentToHash).substr(0, 10);
|
||||
}
|
||||
|
||||
getUtcDateChanged() {
|
||||
return this.utcDateModified;
|
||||
}
|
||||
|
||||
get repository() {
|
||||
if (!repo) {
|
||||
repo = require('../services/repository');
|
||||
|
||||
+16
-12
@@ -21,7 +21,6 @@ const RELATION = 'relation';
|
||||
* @property {boolean} isProtected - true if note is protected
|
||||
* @property {boolean} isDeleted - true if note is deleted
|
||||
* @property {string|null} deleteId - ID identifying delete transaction
|
||||
* @property {boolean} isErased - true if note's content is erased after it has been deleted
|
||||
* @property {string} dateCreated - local date time (with offset)
|
||||
* @property {string} dateModified - local date time (with offset)
|
||||
* @property {string} utcDateCreated
|
||||
@@ -70,9 +69,9 @@ class Note extends Entity {
|
||||
/** @returns {*} */
|
||||
getContent(silentNotFoundError = false) {
|
||||
if (this.content === undefined) {
|
||||
const res = sql.getRow(`SELECT content, hash FROM note_contents WHERE noteId = ?`, [this.noteId]);
|
||||
const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]);
|
||||
|
||||
if (!res) {
|
||||
if (!row) {
|
||||
if (silentNotFoundError) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -81,7 +80,7 @@ class Note extends Entity {
|
||||
}
|
||||
}
|
||||
|
||||
this.content = res.content;
|
||||
this.content = row.content;
|
||||
|
||||
if (this.isProtected) {
|
||||
if (this.isContentAvailable) {
|
||||
@@ -143,8 +142,7 @@ class Note extends Entity {
|
||||
noteId: this.noteId,
|
||||
content: content,
|
||||
dateModified: dateUtils.localNowDateTime(),
|
||||
utcDateModified: dateUtils.utcNowDateTime(),
|
||||
hash: utils.hash(this.noteId + "|" + content.toString())
|
||||
utcDateModified: dateUtils.utcNowDateTime()
|
||||
};
|
||||
|
||||
if (this.isProtected) {
|
||||
@@ -158,7 +156,15 @@ class Note extends Entity {
|
||||
|
||||
sql.upsert("note_contents", "noteId", pojo);
|
||||
|
||||
entityChangesService.addNoteContentEntityChange(this.noteId);
|
||||
const hash = utils.hash(this.noteId + "|" + content.toString());
|
||||
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: 'note_contents',
|
||||
entityId: this.noteId,
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: this.getUtcDateChanged()
|
||||
}, null);
|
||||
}
|
||||
|
||||
setJsonContent(content) {
|
||||
@@ -806,7 +812,7 @@ class Note extends Entity {
|
||||
* @returns {boolean} - true if note has children
|
||||
*/
|
||||
hasChildren() {
|
||||
return (this.getChildNotes()).length > 0;
|
||||
return this.getChildNotes().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -904,10 +910,8 @@ class Note extends Entity {
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.dateModified = dateUtils.localNowDateTime();
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
this.dateModified = dateUtils.localNowDateTime();
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
// cannot be static!
|
||||
|
||||
@@ -15,7 +15,6 @@ const entityChangesService = require('../services/entity_changes.js');
|
||||
* @property {string} type
|
||||
* @property {string} mime
|
||||
* @property {string} title
|
||||
* @property {boolean} isErased
|
||||
* @property {boolean} isProtected
|
||||
* @property {string} dateLastEdited
|
||||
* @property {string} dateCreated
|
||||
@@ -28,12 +27,11 @@ const entityChangesService = require('../services/entity_changes.js');
|
||||
class NoteRevision extends Entity {
|
||||
static get entityName() { return "note_revisions"; }
|
||||
static get primaryKeyName() { return "noteRevisionId"; }
|
||||
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isErased", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
|
||||
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
|
||||
|
||||
constructor(row) {
|
||||
super(row);
|
||||
|
||||
this.isErased = !!this.isErased;
|
||||
this.isProtected = !!this.isProtected;
|
||||
|
||||
if (this.isProtected) {
|
||||
@@ -67,7 +65,7 @@ class NoteRevision extends Entity {
|
||||
/** @returns {*} */
|
||||
getContent(silentNotFoundError = false) {
|
||||
if (this.content === undefined) {
|
||||
const res = sql.getRow(`SELECT content, hash FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
|
||||
const res = sql.getRow(`SELECT content FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
|
||||
|
||||
if (!res) {
|
||||
if (silentNotFoundError) {
|
||||
@@ -79,7 +77,6 @@ class NoteRevision extends Entity {
|
||||
}
|
||||
|
||||
this.content = res.content;
|
||||
|
||||
if (this.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
this.content = protectedSessionService.decrypt(this.content);
|
||||
@@ -106,8 +103,7 @@ class NoteRevision extends Entity {
|
||||
const pojo = {
|
||||
noteRevisionId: this.noteRevisionId,
|
||||
content: content,
|
||||
utcDateModified: dateUtils.utcNowDateTime(),
|
||||
hash: utils.hash(this.noteRevisionId + "|" + content)
|
||||
utcDateModified: dateUtils.utcNowDateTime()
|
||||
};
|
||||
|
||||
if (this.isProtected) {
|
||||
@@ -121,15 +117,21 @@ class NoteRevision extends Entity {
|
||||
|
||||
sql.upsert("note_revision_contents", "noteRevisionId", pojo);
|
||||
|
||||
entityChangesService.addNoteRevisionContentEntityChange(this.noteRevisionId);
|
||||
const hash = utils.hash(this.noteRevisionId + "|" + content);
|
||||
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: 'note_revision_contents',
|
||||
entityId: this.noteRevisionId,
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: this.getUtcDateChanged()
|
||||
}, null);
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
// cannot be static!
|
||||
|
||||
@@ -32,10 +32,8 @@ class Option extends Entity {
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.isChanged) {
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Option;
|
||||
module.exports = Option;
|
||||
|
||||
@@ -8,21 +8,15 @@ const dateUtils = require('../services/date_utils');
|
||||
*
|
||||
* @property {string} noteId
|
||||
* @property {string} notePath
|
||||
* @property {boolean} isDeleted
|
||||
* @property {string} utcDateModified
|
||||
* @property {string} utcDateCreated
|
||||
*
|
||||
* @extends Entity
|
||||
*/
|
||||
class RecentNote extends Entity {
|
||||
static get entityName() { return "recent_notes"; }
|
||||
static get primaryKeyName() { return "noteId"; }
|
||||
static get hashedProperties() { return ["noteId", "notePath", "utcDateCreated", "isDeleted"]; }
|
||||
|
||||
beforeSaving() {
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.utcDateCreated) {
|
||||
this.utcDateCreated = dateUtils.utcNowDateTime();
|
||||
}
|
||||
@@ -31,4 +25,4 @@ class RecentNote extends Entity {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RecentNote;
|
||||
module.exports = RecentNote;
|
||||
|
||||
@@ -7,17 +7,18 @@ const $form = $("#add-link-form");
|
||||
const $autoComplete = $("#add-link-note-autocomplete");
|
||||
const $linkTitle = $("#link-title");
|
||||
const $addLinkTitleSettings = $("#add-link-title-settings");
|
||||
const $addLinkTitleRadios = $(".add-link-title-radios");
|
||||
const $addLinkTitleFormGroup = $("#add-link-title-form-group");
|
||||
|
||||
/** @var TextTypeWidget */
|
||||
let textTypeWidget;
|
||||
|
||||
export async function showDialog(widget) {
|
||||
export async function showDialog(widget, text = '') {
|
||||
textTypeWidget = widget;
|
||||
|
||||
$addLinkTitleSettings.toggle(!textTypeWidget.hasSelection());
|
||||
|
||||
$addLinkTitleSettings.find('input[type=radio]').on('change', updateTitleFormGroupVisibility);
|
||||
$addLinkTitleSettings.find('input[type=radio]').on('change', updateTitleSettingsVisibility);
|
||||
|
||||
// with selection hyper link is implied
|
||||
if (textTypeWidget.hasSelection()) {
|
||||
@@ -27,11 +28,11 @@ export async function showDialog(widget) {
|
||||
$addLinkTitleSettings.find("input[value='reference-link']").prop("checked", true);
|
||||
}
|
||||
|
||||
updateTitleFormGroupVisibility();
|
||||
updateTitleSettingsVisibility();
|
||||
|
||||
utils.openDialog($dialog);
|
||||
|
||||
$autoComplete.val('').trigger('focus');
|
||||
$autoComplete.val('');
|
||||
$linkTitle.val('');
|
||||
|
||||
async function setDefaultLinkTitle(noteId) {
|
||||
@@ -40,13 +41,15 @@ export async function showDialog(widget) {
|
||||
$linkTitle.val(noteTitle);
|
||||
}
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($autoComplete);
|
||||
noteAutocompleteService.initNoteAutocomplete($autoComplete, { allowExternalLinks: true });
|
||||
|
||||
$autoComplete.on('autocomplete:noteselected', function(event, suggestion, dataset) {
|
||||
$autoComplete.on('autocomplete:noteselected', (event, suggestion, dataset) => {
|
||||
if (!suggestion.notePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateTitleSettingsVisibility();
|
||||
|
||||
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
|
||||
|
||||
if (noteId) {
|
||||
@@ -54,39 +57,71 @@ export async function showDialog(widget) {
|
||||
}
|
||||
});
|
||||
|
||||
$autoComplete.on('autocomplete:cursorchanged', function(event, suggestion, dataset) {
|
||||
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
|
||||
$autoComplete.on('autocomplete:externallinkselected', (event, suggestion, dataset) => {
|
||||
if (!suggestion.externalLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
updateTitleSettingsVisibility();
|
||||
|
||||
$linkTitle.val(suggestion.externalLink);
|
||||
});
|
||||
|
||||
$autoComplete.on('autocomplete:cursorchanged', function(event, suggestion, dataset) {
|
||||
if (suggestion.externalLink) {
|
||||
$linkTitle.val(suggestion.externalLink)
|
||||
}
|
||||
else {
|
||||
const noteId = treeService.getNoteIdFromNotePath(suggestion.notePath);
|
||||
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
noteAutocompleteService.showRecentNotes($autoComplete);
|
||||
if (text && text.trim()) {
|
||||
noteAutocompleteService.setText($autoComplete, text);
|
||||
}
|
||||
else {
|
||||
noteAutocompleteService.showRecentNotes($autoComplete);
|
||||
}
|
||||
|
||||
$autoComplete
|
||||
.trigger('focus')
|
||||
.trigger('select'); // to be able to quickly remove entered text
|
||||
}
|
||||
|
||||
function getLinkType() {
|
||||
if ($autoComplete.getSelectedExternalLink()) {
|
||||
return 'external-link';
|
||||
}
|
||||
|
||||
return $addLinkTitleSettings.find('input[type=radio]:checked').val();
|
||||
}
|
||||
|
||||
function updateTitleFormGroupVisibility() {
|
||||
const visible = getLinkType() === 'hyper-link';
|
||||
function updateTitleSettingsVisibility() {
|
||||
const linkType = getLinkType();
|
||||
|
||||
$addLinkTitleFormGroup.toggle(visible);
|
||||
$addLinkTitleFormGroup.toggle(linkType !== 'reference-link');
|
||||
$addLinkTitleRadios.toggle(linkType !== 'external-link')
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
const notePath = $autoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
if ($autoComplete.getSelectedNotePath()) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
const linkTitle = getLinkType() === 'reference-link' ? null : $linkTitle.val();
|
||||
|
||||
textTypeWidget.addLink(notePath, linkTitle);
|
||||
textTypeWidget.addLink($autoComplete.getSelectedNotePath(), linkTitle);
|
||||
}
|
||||
else if ($autoComplete.getSelectedExternalLink()) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
textTypeWidget.addLink($autoComplete.getSelectedExternalLink(), $linkTitle.val());
|
||||
}
|
||||
else {
|
||||
logError("No path to add link.");
|
||||
logError("No link to add.");
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -13,6 +13,7 @@ const $textImportedAsTextCheckbox = $("#text-imported-as-text-checkbox");
|
||||
const $codeImportedAsCodeCheckbox = $("#code-imported-as-code-checkbox");
|
||||
const $explodeArchivesCheckbox = $("#explode-archives-checkbox");
|
||||
const $replaceUnderscoresWithSpacesCheckbox = $("#replace-underscores-with-spaces-checkbox");
|
||||
const $csrf = $("#import-csrf");
|
||||
|
||||
let parentNoteId = null;
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ export async function showDialog() {
|
||||
utils.openDialog($dialog);
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true })
|
||||
// clear any event listener added in previous invocation of this function
|
||||
.off('autocomplete:noteselected')
|
||||
.on('autocomplete:noteselected', function(event, suggestion, dataset) {
|
||||
if (!suggestion.notePath) {
|
||||
return false;
|
||||
@@ -45,10 +47,9 @@ function showInFullText(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const searchText = $autoComplete.val();
|
||||
const searchString = $autoComplete.val();
|
||||
|
||||
appContext.triggerCommand('showSearch', {searchText});
|
||||
appContext.triggerCommand('searchForResults', {searchText});
|
||||
appContext.triggerCommand('searchNotes', {searchString});
|
||||
|
||||
$dialog.modal('hide');
|
||||
}
|
||||
|
||||
@@ -42,14 +42,14 @@ const TPL = `
|
||||
<div>
|
||||
<h4>Note erasure timeout</h4>
|
||||
|
||||
<p>Deleted notes are at first only marked as deleted and it is possible to recover them
|
||||
<p>Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them
|
||||
from Recent Notes dialog. After a period of time, deleted notes are "erased" which means
|
||||
their content is not recoverable anymore. This setting allows you to configure the length
|
||||
of the period between deleting and erasing the note.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="erase-notes-after-time-in-seconds">Erase notes after X seconds</label>
|
||||
<input class="form-control" id="erase-notes-after-time-in-seconds" type="number" min="0">
|
||||
<label for="erase-entities-after-time-in-seconds">Erase notes after X seconds</label>
|
||||
<input class="form-control" id="erase-entities-after-time-in-seconds" type="number" min="0">
|
||||
</div>
|
||||
|
||||
<p>You can also trigger erasing manually:</p>
|
||||
@@ -111,12 +111,12 @@ export default class ProtectedSessionOptions {
|
||||
this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', '));
|
||||
}
|
||||
|
||||
this.$eraseNotesAfterTimeInSeconds = $("#erase-notes-after-time-in-seconds");
|
||||
this.$eraseEntitiesAfterTimeInSeconds = $("#erase-entities-after-time-in-seconds");
|
||||
|
||||
this.$eraseNotesAfterTimeInSeconds.on('change', () => {
|
||||
const eraseNotesAfterTimeInSeconds = this.$eraseNotesAfterTimeInSeconds.val();
|
||||
this.$eraseEntitiesAfterTimeInSeconds.on('change', () => {
|
||||
const eraseEntitiesAfterTimeInSeconds = this.$eraseEntitiesAfterTimeInSeconds.val();
|
||||
|
||||
server.put('options', { 'eraseNotesAfterTimeInSeconds': eraseNotesAfterTimeInSeconds }).then(() => {
|
||||
server.put('options', { 'eraseEntitiesAfterTimeInSeconds': eraseEntitiesAfterTimeInSeconds }).then(() => {
|
||||
toastService.showMessage("Options change have been saved.");
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ export default class ProtectedSessionOptions {
|
||||
this.$spellCheckEnabled.prop("checked", options['spellCheckEnabled'] === 'true');
|
||||
this.$spellCheckLanguageCode.val(options['spellCheckLanguageCode']);
|
||||
|
||||
this.$eraseNotesAfterTimeInSeconds.val(options['eraseNotesAfterTimeInSeconds']);
|
||||
this.$eraseEntitiesAfterTimeInSeconds.val(options['eraseEntitiesAfterTimeInSeconds']);
|
||||
this.$protectedSessionTimeout.val(options['protectedSessionTimeout']);
|
||||
this.$noteRevisionsTimeInterval.val(options['noteRevisionSnapshotTimeInterval']);
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ class Attribute {
|
||||
this.position = row.position;
|
||||
/** @param {boolean} isInheritable */
|
||||
this.isInheritable = !!row.isInheritable;
|
||||
/** @param {boolean} */
|
||||
this.isDeleted = !!row.isDeleted;
|
||||
}
|
||||
|
||||
/** @returns {NoteShort} */
|
||||
@@ -86,6 +88,17 @@ class Attribute {
|
||||
getDefinition() {
|
||||
return promotedAttributeDefinitionParser.parse(this.value);
|
||||
}
|
||||
|
||||
isDefinitionFor(attr) {
|
||||
return this.type === 'label' && this.name === `${attr.type}:${attr.name}`;
|
||||
}
|
||||
|
||||
get dto() {
|
||||
const dto = Object.assign({}, this);
|
||||
delete dto.treeCache;
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
||||
export default Attribute;
|
||||
|
||||
@@ -20,9 +20,9 @@ class Branch {
|
||||
/** @param {boolean} */
|
||||
this.isExpanded = !!row.isExpanded;
|
||||
/** @param {boolean} */
|
||||
this.isDeleted = !!row.isDeleted;
|
||||
/** @param {boolean} */
|
||||
this.fromSearchNote = !!row.fromSearchNote;
|
||||
/** @param {boolean} */
|
||||
this.isDeleted = !!row.isDeleted;
|
||||
}
|
||||
|
||||
/** @returns {NoteShort} */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import server from '../services/server.js';
|
||||
import Attribute from './attribute.js';
|
||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||
import ws from "../services/ws.js";
|
||||
import options from "../services/options.js";
|
||||
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
@@ -76,7 +77,7 @@ class NoteShort {
|
||||
}
|
||||
|
||||
addChild(childNoteId, branchId, sort = true) {
|
||||
if (!this.children.includes(childNoteId)) {
|
||||
if (!(childNoteId in this.childToBranch)) {
|
||||
this.children.push(childNoteId);
|
||||
}
|
||||
|
||||
@@ -266,17 +267,21 @@ class NoteShort {
|
||||
return this.getAttributes(LABEL, name);
|
||||
}
|
||||
|
||||
getIcon(isFolder = false) {
|
||||
const iconCassLabels = this.getLabels('iconClass');
|
||||
getIcon() {
|
||||
const iconClassLabels = this.getLabels('iconClass');
|
||||
const workspaceIconClass = this.getWorkspaceIconClass();
|
||||
|
||||
if (iconCassLabels.length > 0) {
|
||||
return iconCassLabels.map(l => l.value).join(' ');
|
||||
if (iconClassLabels.length > 0) {
|
||||
return iconClassLabels.map(l => l.value).join(' ');
|
||||
}
|
||||
else if (workspaceIconClass) {
|
||||
return workspaceIconClass;
|
||||
}
|
||||
else if (this.noteId === 'root') {
|
||||
return "bx bx-chevrons-right";
|
||||
}
|
||||
else if (this.type === 'text') {
|
||||
if (isFolder) {
|
||||
if (this.isFolder()) {
|
||||
return "bx bx-folder";
|
||||
}
|
||||
else {
|
||||
@@ -291,6 +296,34 @@ class NoteShort {
|
||||
}
|
||||
}
|
||||
|
||||
isFolder() {
|
||||
return this.type === 'search'
|
||||
|| this.getFilteredChildBranches().length > 0;
|
||||
}
|
||||
|
||||
getFilteredChildBranches() {
|
||||
let childBranches = this.getChildBranches();
|
||||
|
||||
if (!childBranches) {
|
||||
ws.logError(`No children for ${parentNote}. This shouldn't happen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.is("hideIncludedImages_main")) {
|
||||
const imageLinks = this.getRelations('imageLink');
|
||||
|
||||
// image is already visible in the parent note so no need to display it separately in the book
|
||||
childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
|
||||
}
|
||||
|
||||
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
|
||||
// which would seriously slow down everything.
|
||||
// we check this flag only once user chooses to expand the parent. This has the negative consequence that
|
||||
// note may appear as folder but not contain any children when all of them are archived
|
||||
|
||||
return childBranches;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - relation name to filter
|
||||
* @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
|
||||
@@ -475,19 +508,42 @@ class NoteShort {
|
||||
return relations.map(rel => this.treeCache.notes[rel.value]);
|
||||
}
|
||||
|
||||
hasAncestor(ancestorNote) {
|
||||
getPromotedDefinitionAttributes() {
|
||||
if (this.hasLabel('hidePromotedAttributes')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.getAttributes()
|
||||
.filter(attr => attr.isDefinition())
|
||||
.filter(attr => {
|
||||
const def = attr.getDefinition();
|
||||
|
||||
return def && def.isPromoted;
|
||||
});
|
||||
}
|
||||
|
||||
hasAncestor(ancestorNote, visitedNoteIds) {
|
||||
if (this.noteId === ancestorNote.noteId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!visitedNoteIds) {
|
||||
visitedNoteIds = new Set();
|
||||
} else if (visitedNoteIds.has(this.noteId)) {
|
||||
// to avoid infinite cycle when template is descendent of the instance
|
||||
return false;
|
||||
}
|
||||
|
||||
visitedNoteIds.add(this.noteId);
|
||||
|
||||
for (const templateNote of this.getTemplateNotes()) {
|
||||
if (templateNote.hasAncestor(ancestorNote)) {
|
||||
if (templateNote.hasAncestor(ancestorNote, visitedNoteIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const parentNote of this.getParentNotes()) {
|
||||
if (parentNote.hasAncestor(ancestorNote)) {
|
||||
if (parentNote.hasAncestor(ancestorNote, visitedNoteIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -548,6 +604,16 @@ class NoteShort {
|
||||
const labels = this.getLabels('cssClass');
|
||||
return labels.map(l => l.value).join(' ');
|
||||
}
|
||||
|
||||
getWorkspaceIconClass() {
|
||||
const labels = this.getLabels('workspaceIconClass');
|
||||
return labels.length > 0 ? labels[0].value : "";
|
||||
}
|
||||
|
||||
getWorkspaceTabBackgroundColor() {
|
||||
const labels = this.getLabels('workspaceTabBackgroundColor');
|
||||
return labels.length > 0 ? labels[0].value : "";
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteShort;
|
||||
|
||||
@@ -5,11 +5,23 @@ import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import TabCachingWidget from "../widgets/tab_caching_widget.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import RunScriptButtonsWidget from "../widgets/run_script_buttons.js";
|
||||
import NoteTypeWidget from "../widgets/note_type.js";
|
||||
import NoteActionsWidget from "../widgets/note_actions.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import AttributeListWidget from "../widgets/attribute_list.js";
|
||||
import OwnedAttributeListWidget from "../widgets/type_property_widgets/owned_attribute_list.js";
|
||||
import CollapsibleSectionContainer from "../widgets/collapsible_section_container.js";
|
||||
import SearchDefinitionWidget from "../widgets/type_property_widgets/search_definition.js";
|
||||
import PromotedAttributesWidget from "../widgets/type_property_widgets/promoted_attributes.js";
|
||||
import InheritedAttributesWidget from "../widgets/type_property_widgets/inherited_attribute_list.js";
|
||||
import Container from "../widgets/container.js";
|
||||
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import SqlResultWidget from "../widgets/sql_result.js";
|
||||
import FilePropertiesWidget from "../widgets/type_property_widgets/file_properties.js";
|
||||
import ImagePropertiesWidget from "../widgets/type_property_widgets/image_properties.js";
|
||||
import NotePropertiesWidget from "../widgets/type_property_widgets/note_properties.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.js";
|
||||
import NotePathsWidget from "../widgets/note_paths.js";
|
||||
|
||||
export default class DesktopExtraWindowLayout {
|
||||
constructor(customWidgets) {
|
||||
@@ -32,15 +44,33 @@ export default class DesktopExtraWindowLayout {
|
||||
.filling()
|
||||
.child(new FlexContainer('column').id('center-pane').filling()
|
||||
.child(new FlexContainer('row').class('title-row')
|
||||
.css('align-items: center;')
|
||||
.cssBlock('.title-row > * { margin: 5px; }')
|
||||
.overflowing()
|
||||
.cssBlock('.title-row > * { margin: 5px 5px 0 5px; }')
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget())
|
||||
.child(new RunScriptButtonsWidget().hideInZenMode())
|
||||
.child(new NotePathsWidget().hideInZenMode())
|
||||
.child(new NoteTypeWidget().hideInZenMode())
|
||||
.child(new NoteActionsWidget().hideInZenMode())
|
||||
)
|
||||
.child(new TabCachingWidget(() => new AttributeListWidget()))
|
||||
.child(new TabCachingWidget(() => new NoteDetailWidget()))
|
||||
.child(
|
||||
new TabCachingWidget(() => new CollapsibleSectionContainer()
|
||||
.child(new SearchDefinitionWidget())
|
||||
.child(new NotePropertiesWidget())
|
||||
.child(new FilePropertiesWidget())
|
||||
.child(new ImagePropertiesWidget())
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new OwnedAttributeListWidget())
|
||||
.child(new InheritedAttributesWidget())
|
||||
)
|
||||
)
|
||||
.child(new Container()
|
||||
.css('height: 100%; overflow: auto;')
|
||||
.child(new TabCachingWidget(() => new SqlTableSchemasWidget()))
|
||||
.child(new TabCachingWidget(() => new NoteDetailWidget()))
|
||||
.child(new TabCachingWidget(() => new NoteListWidget()))
|
||||
.child(new TabCachingWidget(() => new SqlResultWidget()))
|
||||
)
|
||||
.child(...this.customWidgets.get('center-pane'))
|
||||
)
|
||||
);
|
||||
|
||||
@@ -4,15 +4,11 @@ import TabRowWidget from "../widgets/tab_row.js";
|
||||
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
|
||||
import StandardTopWidget from "../widgets/standard_top_widget.js";
|
||||
import SidePaneContainer from "../widgets/side_pane_container.js";
|
||||
import GlobalButtonsWidget from "../widgets/global_buttons.js";
|
||||
import SearchBoxWidget from "../widgets/search_box.js";
|
||||
import SearchResultsWidget from "../widgets/search_results.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import TabCachingWidget from "../widgets/tab_caching_widget.js";
|
||||
import NotePathsWidget from "../widgets/note_paths.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import AttributeListWidget from "../widgets/attribute_list.js";
|
||||
import RunScriptButtonsWidget from "../widgets/run_script_buttons.js";
|
||||
import OwnedAttributeListWidget from "../widgets/type_property_widgets/owned_attribute_list.js";
|
||||
import NoteTypeWidget from "../widgets/note_type.js";
|
||||
import NoteActionsWidget from "../widgets/note_actions.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
@@ -24,6 +20,18 @@ import SimilarNotesWidget from "../widgets/similar_notes.js";
|
||||
import WhatLinksHereWidget from "../widgets/collapsible_widgets/what_links_here.js";
|
||||
import SidePaneToggles from "../widgets/side_pane_toggles.js";
|
||||
import EditedNotesWidget from "../widgets/collapsible_widgets/edited_notes.js";
|
||||
import CollapsibleSectionContainer from "../widgets/collapsible_section_container.js";
|
||||
import PromotedAttributesWidget from "../widgets/type_property_widgets/promoted_attributes.js";
|
||||
import InheritedAttributesWidget from "../widgets/type_property_widgets/inherited_attribute_list.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import SearchDefinitionWidget from "../widgets/type_property_widgets/search_definition.js";
|
||||
import Container from "../widgets/container.js";
|
||||
import SqlResultWidget from "../widgets/sql_result.js";
|
||||
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
||||
import FilePropertiesWidget from "../widgets/type_property_widgets/file_properties.js";
|
||||
import ImagePropertiesWidget from "../widgets/type_property_widgets/image_properties.js";
|
||||
import NotePropertiesWidget from "../widgets/type_property_widgets/note_properties.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.js";
|
||||
|
||||
const RIGHT_PANE_CSS = `
|
||||
<style>
|
||||
@@ -40,7 +48,7 @@ const RIGHT_PANE_CSS = `
|
||||
|
||||
#right-pane .card-header {
|
||||
background: inherit;
|
||||
padding: 6px 10px 3px 10px;
|
||||
padding: 6px 10px 3px 0;
|
||||
width: 99%; /* to give minimal right margin */
|
||||
background-color: var(--button-background-color);
|
||||
border-color: var(--button-border-color);
|
||||
@@ -85,15 +93,19 @@ const RIGHT_PANE_CSS = `
|
||||
#right-pane .widget-toggle-button {
|
||||
cursor: pointer;
|
||||
color: var(--main-text-color) !important;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
#right-pane .widget-toggle-button:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#right-pane .widget-toggle-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: large;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#right-pane .body-wrapper {
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -137,24 +149,38 @@ export default class DesktopMainWindowLayout {
|
||||
.filling()
|
||||
.child(new SidePaneContainer('left')
|
||||
.hideInZenMode()
|
||||
.child(new GlobalButtonsWidget())
|
||||
.child(new SearchBoxWidget())
|
||||
.child(new SearchResultsWidget())
|
||||
.child(new TabCachingWidget(() => new NotePathsWidget()))
|
||||
.child(appContext.mainTreeWidget)
|
||||
.child(...this.customWidgets.get('left-pane'))
|
||||
)
|
||||
.child(new FlexContainer('column').id('center-pane')
|
||||
.child(new FlexContainer('row').class('title-row')
|
||||
.cssBlock('.title-row > * { margin: 5px 5px 0 5px; }')
|
||||
.css('align-items: center;')
|
||||
.cssBlock('.title-row > * { margin: 5px; }')
|
||||
.overflowing()
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget())
|
||||
.child(new RunScriptButtonsWidget().hideInZenMode())
|
||||
.child(new NotePathsWidget().hideInZenMode())
|
||||
.child(new NoteTypeWidget().hideInZenMode())
|
||||
.child(new NoteActionsWidget().hideInZenMode())
|
||||
)
|
||||
.child(new TabCachingWidget(() => new AttributeListWidget()))
|
||||
.child(new TabCachingWidget(() => new NoteDetailWidget()))
|
||||
.child(
|
||||
new TabCachingWidget(() => new CollapsibleSectionContainer()
|
||||
.child(new SearchDefinitionWidget())
|
||||
.child(new NotePropertiesWidget())
|
||||
.child(new FilePropertiesWidget())
|
||||
.child(new ImagePropertiesWidget())
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new OwnedAttributeListWidget())
|
||||
.child(new InheritedAttributesWidget())
|
||||
)
|
||||
)
|
||||
.child(new Container()
|
||||
.css('height: 100%; overflow: auto;')
|
||||
.child(new TabCachingWidget(() => new SqlTableSchemasWidget()))
|
||||
.child(new TabCachingWidget(() => new NoteDetailWidget()))
|
||||
.child(new TabCachingWidget(() => new NoteListWidget()))
|
||||
.child(new TabCachingWidget(() => new SqlResultWidget()))
|
||||
)
|
||||
.child(new TabCachingWidget(() => new SimilarNotesWidget()))
|
||||
.child(...this.customWidgets.get('center-pane'))
|
||||
)
|
||||
|
||||
@@ -40,44 +40,49 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
|
||||
}
|
||||
|
||||
async function initLabelValueAutocomplete({ $el, open, nameCallback }) {
|
||||
if (!$el.hasClass("aa-input")) {
|
||||
const attributeName = nameCallback();
|
||||
|
||||
if (attributeName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeValues = (await server.get('attributes/values/' + encodeURIComponent(attributeName)))
|
||||
.map(attribute => ({ value: attribute }));
|
||||
|
||||
if (attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$el.autocomplete({
|
||||
appendTo: document.querySelector('body'),
|
||||
hint: false,
|
||||
openOnFocus: true,
|
||||
minLength: 0,
|
||||
tabAutocomplete: false
|
||||
}, [{
|
||||
displayKey: 'value',
|
||||
source: function (term, cb) {
|
||||
term = term.toLowerCase();
|
||||
|
||||
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
|
||||
|
||||
cb(filtered);
|
||||
}
|
||||
}]);
|
||||
|
||||
$el.on('autocomplete:opened', () => {
|
||||
if ($el.attr("readonly")) {
|
||||
$el.autocomplete('close');
|
||||
}
|
||||
})
|
||||
if ($el.hasClass("aa-input")) {
|
||||
// we reinit everytime because autocomplete seems to have a bug where it retains state from last
|
||||
// open even though the value was resetted
|
||||
$el.autocomplete('destroy');
|
||||
}
|
||||
|
||||
const attributeName = nameCallback();
|
||||
|
||||
if (attributeName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeValues = (await server.get('attributes/values/' + encodeURIComponent(attributeName)))
|
||||
.map(attribute => ({ value: attribute }));
|
||||
|
||||
if (attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$el.autocomplete({
|
||||
appendTo: document.querySelector('body'),
|
||||
hint: false,
|
||||
openOnFocus: false, // handled manually
|
||||
minLength: 0,
|
||||
tabAutocomplete: false
|
||||
}, [{
|
||||
displayKey: 'value',
|
||||
cache: false,
|
||||
source: async function (term, cb) {
|
||||
term = term.toLowerCase();
|
||||
|
||||
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
|
||||
|
||||
cb(filtered);
|
||||
}
|
||||
}]);
|
||||
|
||||
$el.on('autocomplete:opened', () => {
|
||||
if ($el.attr("readonly")) {
|
||||
$el.autocomplete('close');
|
||||
}
|
||||
});
|
||||
|
||||
if (open) {
|
||||
$el.autocomplete("open");
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ async function createNoteLink(noteId) {
|
||||
}
|
||||
|
||||
async function renderAttributes(attributes, renderIsInheritable) {
|
||||
const $container = $("<span>");
|
||||
const $container = $('<span class="rendered-note-attributes">');
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attribute = attributes[i];
|
||||
@@ -79,7 +79,31 @@ async function renderAttributes(attributes, renderIsInheritable) {
|
||||
return $container;
|
||||
}
|
||||
|
||||
async function renderNormalAttributes(note) {
|
||||
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
|
||||
let attrs = note.getAttributes();
|
||||
|
||||
if (promotedDefinitionAttributes.length > 0) {
|
||||
attrs = attrs.filter(attr => !!promotedDefinitionAttributes.find(promAttr => promAttr.isDefinitionFor(attr)));
|
||||
}
|
||||
else {
|
||||
attrs = attrs.filter(
|
||||
attr => !attr.isDefinition()
|
||||
&& !attr.isAutoLink
|
||||
&& attr.noteId === note.noteId
|
||||
);
|
||||
}
|
||||
|
||||
const $renderedAttributes = await renderAttributes(attrs, false);
|
||||
|
||||
return {
|
||||
count: attrs.length,
|
||||
$renderedAttributes
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
renderAttribute,
|
||||
renderAttributes
|
||||
renderAttributes,
|
||||
renderNormalAttributes
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import server from './server.js';
|
||||
|
||||
async function addLabel(noteId, name, value = "") {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
type: 'label',
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
async function setLabel(noteId, name, value = "") {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: 'label',
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
async function removeAttributeById(noteId, attributeId) {
|
||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||
}
|
||||
|
||||
export default {
|
||||
addLabel,
|
||||
setLabel,
|
||||
removeAttributeById
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
import treeCache from "./tree_cache.js";
|
||||
import server from "./server.js";
|
||||
import ws from "./ws.js";
|
||||
|
||||
/** @return {NoteShort} */
|
||||
async function getInboxNote() {
|
||||
const note = await server.get('date-notes/inbox/' + dayjs().format("YYYY-MM-DD"), "date-note");
|
||||
|
||||
return await treeCache.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/** @return {NoteShort} */
|
||||
async function getTodayNote() {
|
||||
@@ -34,10 +42,30 @@ async function createSqlConsole() {
|
||||
return await treeCache.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/** @return {NoteShort} */
|
||||
async function createSearchNote(opts = {}) {
|
||||
const note = await server.post('search-note');
|
||||
|
||||
const attrsToUpdate = [
|
||||
opts.ancestorNoteId ? { type: 'relation', name: 'ancestor', value: opts.ancestorNoteId } : undefined,
|
||||
{ type: 'label', name: 'searchString', value: opts.searchString }
|
||||
].filter(attr => !!attr);
|
||||
|
||||
if (attrsToUpdate.length > 0) {
|
||||
await server.put(`notes/${note.noteId}/attributes`, attrsToUpdate);
|
||||
}
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
return await treeCache.getNote(note.noteId);
|
||||
}
|
||||
|
||||
export default {
|
||||
getInboxNote,
|
||||
getTodayNote,
|
||||
getDateNote,
|
||||
getMonthNote,
|
||||
getYearNote,
|
||||
createSqlConsole
|
||||
}
|
||||
createSqlConsole,
|
||||
createSearchNote
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Component from "../widgets/component.js";
|
||||
import appContext from "./app_context.js";
|
||||
import dateNoteService from "../services/date_notes.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import treeService from "../services/tree.js";
|
||||
|
||||
export default class DialogCommandExecutor extends Component {
|
||||
jumpToNoteCommand() {
|
||||
@@ -64,7 +64,21 @@ export default class DialogCommandExecutor extends Component {
|
||||
|
||||
const tabContext = await appContext.tabManager.openTabWithNote(sqlConsoleNote.noteId, true);
|
||||
|
||||
appContext.triggerCommand('focusOnDetail', {tabId: tabContext.tabId});
|
||||
appContext.triggerEvent('focusOnDetail', {tabId: tabContext.tabId});
|
||||
}
|
||||
|
||||
async searchNotesCommand({searchString, ancestorNoteId}) {
|
||||
const searchNote = await dateNoteService.createSearchNote({searchString, ancestorNoteId});
|
||||
|
||||
const tabContext = await appContext.tabManager.openTabWithNote(searchNote.noteId, true);
|
||||
|
||||
appContext.triggerCommand('focusOnSearchDefinition', {tabId: tabContext.tabId});
|
||||
}
|
||||
|
||||
async searchInSubtreeCommand({notePath}) {
|
||||
const noteId = treeService.getNoteIdFromNotePath(notePath);
|
||||
|
||||
this.searchNotesCommand({ancestorNoteId: noteId});
|
||||
}
|
||||
|
||||
showBackendLogCommand() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import utils from "./utils.js";
|
||||
import dateNoteService from "./date_notes.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import protectedSessionHolder from './protected_session_holder.js';
|
||||
import server from "./server.js";
|
||||
import appContext from "./app_context.js";
|
||||
import Component from "../widgets/component.js";
|
||||
@@ -62,14 +62,14 @@ export default class Entrypoints extends Component {
|
||||
findInPage.openFindWindow();
|
||||
}
|
||||
|
||||
async createNoteIntoDayNoteCommand() {
|
||||
const todayNote = await dateNoteService.getTodayNote();
|
||||
async createNoteIntoInboxCommand() {
|
||||
const inboxNote = await dateNoteService.getInboxNote();
|
||||
|
||||
const {note} = await server.post(`notes/${todayNote.noteId}/children?target=into`, {
|
||||
const {note} = await server.post(`notes/${inboxNote.noteId}/children?target=into`, {
|
||||
title: 'new note',
|
||||
content: '',
|
||||
type: 'text',
|
||||
isProtected: todayNote.isProtected
|
||||
isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
@@ -80,19 +80,30 @@ export default class Entrypoints extends Component {
|
||||
}
|
||||
|
||||
async toggleNoteHoistingCommand() {
|
||||
const note = appContext.tabManager.getActiveTabNote();
|
||||
const tabContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
if (note.noteId === hoistedNoteId) {
|
||||
hoistedNoteService.unhoist();
|
||||
if (tabContext.note.noteId === tabContext.hoistedNoteId) {
|
||||
await tabContext.unhoist();
|
||||
}
|
||||
else if (note.type !== 'search') {
|
||||
hoistedNoteService.setHoistedNoteId(note.noteId);
|
||||
else if (tabContext.note.type !== 'search') {
|
||||
await tabContext.setHoistedNoteId(tabContext.note.noteId);
|
||||
}
|
||||
}
|
||||
|
||||
async hoistNoteCommand({noteId}) {
|
||||
const tabContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
if (tabContext.hoistedNoteId !== noteId) {
|
||||
await tabContext.setHoistedNoteId(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
async unhoistCommand() {
|
||||
hoistedNoteService.unhoist();
|
||||
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
if (activeTabContext) {
|
||||
activeTabContext.unhoist();
|
||||
}
|
||||
}
|
||||
|
||||
copyWithoutFormattingCommand() {
|
||||
@@ -165,21 +176,6 @@ export default class Entrypoints extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async searchForResultsCommand({searchText}) {
|
||||
const response = await server.get('search/' + encodeURIComponent(searchText) + '?includeNoteContent=true&excludeArchived=true&fuzzyAttributeSearch=false');
|
||||
|
||||
if (!response.success) {
|
||||
toastService.showError("Search failed: " + response.message, 10000);
|
||||
// even in this case we'll show the results
|
||||
}
|
||||
|
||||
this.triggerEvent('searchResults', {results: response.results});
|
||||
|
||||
// have at least some feedback which is good especially in situations
|
||||
// when the result list does not change with a query
|
||||
toastService.showMessage("Search finished successfully.");
|
||||
}
|
||||
|
||||
async switchToDesktopVersionCommand() {
|
||||
utils.setCookie('trilium-device', 'desktop');
|
||||
|
||||
@@ -188,11 +184,15 @@ export default class Entrypoints extends Component {
|
||||
|
||||
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
|
||||
|
||||
async openInWindowCommand({notePath}) {
|
||||
async openInWindowCommand({notePath, hoistedNoteId}) {
|
||||
if (!hoistedNoteId) {
|
||||
hoistedNoteId = 'root';
|
||||
}
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const {ipcRenderer} = utils.dynamicRequire('electron');
|
||||
|
||||
ipcRenderer.send('create-extra-window', {notePath});
|
||||
ipcRenderer.send('create-extra-window', {notePath, hoistedNoteId});
|
||||
}
|
||||
else {
|
||||
const url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?extra=1#' + notePath;
|
||||
@@ -202,11 +202,12 @@ export default class Entrypoints extends Component {
|
||||
}
|
||||
|
||||
async openNewWindowCommand() {
|
||||
this.openInWindowCommand({notePath: ''});
|
||||
this.openInWindowCommand({notePath: '', hoistedNoteId: 'root'});
|
||||
}
|
||||
|
||||
async runActiveNoteCommand() {
|
||||
const note = appContext.tabManager.getActiveTabNote();
|
||||
const tabContext = appContext.tabManager.getActiveTabContext();
|
||||
const note = tabContext.note;
|
||||
|
||||
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
|
||||
if (!note || note.type !== 'code') {
|
||||
@@ -215,10 +216,12 @@ export default class Entrypoints extends Component {
|
||||
|
||||
if (note.mime.endsWith("env=frontend")) {
|
||||
await bundleService.getAndExecuteBundle(note.noteId);
|
||||
}
|
||||
|
||||
if (note.mime.endsWith("env=backend")) {
|
||||
} else if (note.mime.endsWith("env=backend")) {
|
||||
await server.post('script/run/' + note.noteId);
|
||||
} else if (note.mime === 'text/x-sqlite;schema=trilium') {
|
||||
const result = await server.post("sql/execute/" + note.noteId);
|
||||
|
||||
this.triggerEvent('sqlQueryResults', {tabId: tabContext.tabId, results: result.results});
|
||||
}
|
||||
|
||||
toastService.showMessage("Note executed");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import treeService from './tree.js';
|
||||
import server from './server.js';
|
||||
import utils from './utils.js';
|
||||
import toastService from './toast.js';
|
||||
@@ -9,7 +8,6 @@ import protectedSessionService from './protected_session.js';
|
||||
import dateNotesService from './date_notes.js';
|
||||
import CollapsibleWidget from '../widgets/collapsible_widget.js';
|
||||
import ws from "./ws.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import appContext from "./app_context.js";
|
||||
import TabAwareWidget from "../widgets/tab_aware_widget.js";
|
||||
import TabCachingWidget from "../widgets/tab_caching_widget.js";
|
||||
@@ -89,7 +87,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
this.addButtonToToolbar = opts => {
|
||||
const buttonId = "toolbar-button-" + opts.title.replace(/\s/g, "-");
|
||||
|
||||
const button = $('<button>')
|
||||
const button = $('<button class="noborder">')
|
||||
.addClass("btn btn-sm")
|
||||
.on('click', opts.action);
|
||||
|
||||
@@ -379,13 +377,19 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
this.getYearNote = dateNotesService.getYearNote;
|
||||
|
||||
/**
|
||||
* Hoist note. See https://github.com/zadam/trilium/wiki/Note-hoisting
|
||||
* Hoist note in the current tab. See https://github.com/zadam/trilium/wiki/Note-hoisting
|
||||
*
|
||||
* @method
|
||||
* @param {string} noteId - set hoisted note. 'root' will effectively unhoist
|
||||
* @return {Promise}
|
||||
*/
|
||||
this.setHoistedNoteId = hoistedNoteService.setHoistedNoteId;
|
||||
this.setHoistedNoteId = (noteId) => {
|
||||
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
if (activeTabContext) {
|
||||
activeTabContext.setHoistedNoteId(noteId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @method
|
||||
|
||||
@@ -58,7 +58,7 @@ function setupGlobs() {
|
||||
'Line: ' + lineNo,
|
||||
'Column: ' + columnNo,
|
||||
'Error object: ' + JSON.stringify(error),
|
||||
'Stack: ' + error && error.stack
|
||||
'Stack: ' + (error && error.stack)
|
||||
].join(', ');
|
||||
}
|
||||
|
||||
@@ -73,11 +73,7 @@ function setupGlobs() {
|
||||
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`);
|
||||
}
|
||||
|
||||
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
|
||||
|
||||
$(document).on("click", "*[data-help-page]", e => {
|
||||
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
|
||||
});
|
||||
utils.initHelpButtons($(window));
|
||||
|
||||
$("body").on("click", "a.external", function () {
|
||||
window.open($(this).attr("href"), '_blank');
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import options from './options.js';
|
||||
import appContext from "./app_context.js";
|
||||
import treeService from "./tree.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
|
||||
function getHoistedNoteId() {
|
||||
return options.get('hoistedNoteId');
|
||||
}
|
||||
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
async function setHoistedNoteId(noteId) {
|
||||
if (getHoistedNoteId() === noteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await options.save('hoistedNoteId', noteId);
|
||||
|
||||
await treeCache.loadInitialTree();
|
||||
|
||||
// FIXME - just use option load event
|
||||
appContext.triggerEvent('hoistedNoteChanged', {noteId});
|
||||
return activeTabContext ? activeTabContext.hoistedNoteId : 'root';
|
||||
}
|
||||
|
||||
async function unhoist() {
|
||||
await setHoistedNoteId('root');
|
||||
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
if (activeTabContext) {
|
||||
await activeTabContext.unhoist();
|
||||
}
|
||||
}
|
||||
|
||||
function isTopLevelNode(node) {
|
||||
@@ -34,7 +25,7 @@ function isRootNode(node) {
|
||||
|| node.data.noteId === getHoistedNoteId();
|
||||
}
|
||||
|
||||
async function checkNoteAccess(notePath) {
|
||||
async function checkNoteAccess(notePath, tabContext) {
|
||||
// notePath argument can contain only noteId which is not good when hoisted since
|
||||
// then we need to check the whole note path
|
||||
const resolvedNotePath = await treeService.resolveNotePath(notePath);
|
||||
@@ -44,7 +35,7 @@ async function checkNoteAccess(notePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hoistedNoteId = getHoistedNoteId();
|
||||
const hoistedNoteId = tabContext.hoistedNoteId;
|
||||
|
||||
if (hoistedNoteId !== 'root' && !resolvedNotePath.includes(hoistedNoteId)) {
|
||||
const confirmDialog = await import('../dialogs/confirm.js');
|
||||
@@ -62,7 +53,6 @@ async function checkNoteAccess(notePath) {
|
||||
|
||||
export default {
|
||||
getHoistedNoteId,
|
||||
setHoistedNoteId,
|
||||
unhoist,
|
||||
isTopLevelNode,
|
||||
isRootNode,
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function uploadFiles(parentNoteId, files, options) {
|
||||
|
||||
({noteId} = await $.ajax({
|
||||
url: baseApiUrl + 'notes/' + parentNoteId + '/import',
|
||||
headers: server.getHeaders(),
|
||||
headers: await server.getHeaders(),
|
||||
data: formData,
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
@@ -71,4 +71,4 @@ ws.subscribeToMessages(async message => {
|
||||
|
||||
export default {
|
||||
uploadFiles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ function goToLink(e) {
|
||||
|
||||
if (notePath) {
|
||||
if ((e.which === 1 && e.ctrlKey) || e.which === 2) {
|
||||
appContext.tabManager.openTabWithNote(notePath);
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
||||
}
|
||||
else if (e.which === 1) {
|
||||
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||
@@ -90,7 +90,7 @@ function goToLink(e) {
|
||||
|| $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices
|
||||
) {
|
||||
const address = $link.attr('href');
|
||||
console.log("address", address);
|
||||
|
||||
if (address) {
|
||||
if (address.toLowerCase().startsWith('http')) {
|
||||
window.open(address, '_blank');
|
||||
@@ -127,10 +127,10 @@ function linkContextMenu(e) {
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => {
|
||||
if (command === 'openNoteInNewTab') {
|
||||
appContext.tabManager.openTabWithNote(notePath);
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
||||
}
|
||||
else if (command === 'openNoteInNewWindow') {
|
||||
appContext.triggerCommand('openInWindow', {notePath});
|
||||
appContext.triggerCommand('openInWindow', {notePath, hoistedNoteId: 'root'});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -144,11 +144,8 @@ async function loadReferenceLinkTitle(noteId, $el) {
|
||||
if (!note) {
|
||||
title = '[missing]';
|
||||
}
|
||||
else if (!note.isDeleted) {
|
||||
title = note.title;
|
||||
}
|
||||
else {
|
||||
title = note.isErased ? '[erased]' : `${note.title} (deleted)`;
|
||||
title = note.isDeleted ? `${note.title} (deleted)` : note.title;
|
||||
}
|
||||
|
||||
$el.text(title);
|
||||
|
||||
@@ -54,6 +54,7 @@ export default class LoadResults {
|
||||
this.attributes.push({attributeId, sourceId});
|
||||
}
|
||||
|
||||
/** @return {Attribute[]} */
|
||||
getAttributes(sourceId = 'none') {
|
||||
return this.attributes
|
||||
.filter(row => row.sourceId !== sourceId)
|
||||
|
||||
@@ -3,10 +3,13 @@ import appContext from "./app_context.js";
|
||||
import utils from './utils.js';
|
||||
import noteCreateService from './note_create.js';
|
||||
import treeService from './tree.js';
|
||||
import treeCache from "./tree_cache.js";
|
||||
|
||||
// this key needs to have this value so it's hit by the tooltip
|
||||
const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
||||
|
||||
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
|
||||
|
||||
async function autocompleteSourceForCKEditor(queryText) {
|
||||
return await new Promise((res, rej) => {
|
||||
autocompleteSource(queryText, rows => {
|
||||
@@ -21,18 +24,20 @@ async function autocompleteSourceForCKEditor(queryText) {
|
||||
highlightedNotePathTitle: row.highlightedNotePathTitle
|
||||
}
|
||||
}));
|
||||
}, {
|
||||
allowCreatingNotes: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function autocompleteSource(term, cb) {
|
||||
async function autocompleteSource(term, cb, options = {}) {
|
||||
const activeNoteId = appContext.tabManager.getActiveTabNoteId();
|
||||
|
||||
let results = await server.get('autocomplete'
|
||||
+ '?query=' + encodeURIComponent(term)
|
||||
+ '&activeNoteId=' + activeNoteId);
|
||||
|
||||
if (term.trim().length >= 1) {
|
||||
if (term.trim().length >= 1 && options.allowCreatingNotes) {
|
||||
results = [
|
||||
{
|
||||
action: 'create-note',
|
||||
@@ -43,6 +48,16 @@ async function autocompleteSource(term, cb) {
|
||||
].concat(results);
|
||||
}
|
||||
|
||||
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
|
||||
results = [
|
||||
{
|
||||
action: 'external-link',
|
||||
externalLink: term,
|
||||
highlightedNotePathTitle: `Insert external link to "${term}"`
|
||||
}
|
||||
].concat(results);
|
||||
}
|
||||
|
||||
cb(results);
|
||||
}
|
||||
|
||||
@@ -55,6 +70,17 @@ function clearText($el) {
|
||||
$el.autocomplete("val", "").trigger('change');
|
||||
}
|
||||
|
||||
function setText($el, text) {
|
||||
if (utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$el.setSelectedNotePath("");
|
||||
$el
|
||||
.autocomplete("val", text.trim())
|
||||
.autocomplete("open");
|
||||
}
|
||||
|
||||
function showRecentNotes($el) {
|
||||
if (utils.isMobile()) {
|
||||
return;
|
||||
@@ -119,7 +145,7 @@ function initNoteAutocomplete($el, options) {
|
||||
tabAutocomplete: false
|
||||
}, [
|
||||
{
|
||||
source: autocompleteSource,
|
||||
source: (term, cb) => autocompleteSource(term, cb, options),
|
||||
displayKey: 'notePathTitle',
|
||||
templates: {
|
||||
suggestion: suggestion => suggestion.highlightedNotePathTitle
|
||||
@@ -130,6 +156,19 @@ function initNoteAutocomplete($el, options) {
|
||||
]);
|
||||
|
||||
$el.on('autocomplete:selected', async (event, suggestion) => {
|
||||
if (suggestion.action === 'external-link') {
|
||||
$el.setSelectedNotePath(null);
|
||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||
|
||||
$el.autocomplete("val", suggestion.externalLink);
|
||||
|
||||
$el.autocomplete("close");
|
||||
|
||||
$el.trigger('autocomplete:externallinkselected', [suggestion]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (suggestion.action === 'create-note') {
|
||||
const {note} = await noteCreateService.createNote(suggestion.parentNoteId, {
|
||||
title: suggestion.noteTitle,
|
||||
@@ -140,6 +179,7 @@ function initNoteAutocomplete($el, options) {
|
||||
}
|
||||
|
||||
$el.setSelectedNotePath(suggestion.notePath);
|
||||
$el.setSelectedExternalLink(null);
|
||||
|
||||
$el.autocomplete("val", suggestion.noteTitle);
|
||||
|
||||
@@ -193,6 +233,31 @@ function init() {
|
||||
.toggleClass("disabled", !notePath.trim())
|
||||
.attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed
|
||||
};
|
||||
|
||||
$.fn.getSelectedExternalLink = function () {
|
||||
if (!$(this).val().trim()) {
|
||||
return "";
|
||||
} else {
|
||||
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.setSelectedExternalLink = function (externalLink) {
|
||||
$(this).attr(SELECTED_EXTERNAL_LINK_KEY, externalLink);
|
||||
|
||||
$(this)
|
||||
.closest(".input-group")
|
||||
.find(".go-to-selected-note-button")
|
||||
.toggleClass("disabled", true);
|
||||
}
|
||||
|
||||
$.fn.setNote = async function (noteId) {
|
||||
const note = noteId ? await treeCache.getNote(noteId, true) : null;
|
||||
|
||||
$(this)
|
||||
.val(note ? note.title : "")
|
||||
.setSelectedNotePath(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -200,5 +265,6 @@ export default {
|
||||
autocompleteSourceForCKEditor,
|
||||
initNoteAutocomplete,
|
||||
showRecentNotes,
|
||||
setText,
|
||||
init
|
||||
}
|
||||
|
||||
@@ -4,34 +4,42 @@ import protectedSessionService from "./protected_session.js";
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
import libraryLoader from "./library_loader.js";
|
||||
import openService from "./open.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
|
||||
async function getRenderedContent(note, options = {}) {
|
||||
options = Object.assign({
|
||||
trim: false,
|
||||
tooltip: false
|
||||
}, options);
|
||||
|
||||
async function getRenderedContent(note) {
|
||||
const type = getRenderingType(note);
|
||||
|
||||
let $rendered;
|
||||
const $renderedContent = $('<div class="rendered-note-content">');
|
||||
|
||||
if (type === 'text') {
|
||||
const fullNote = await server.get('notes/' + note.noteId);
|
||||
const noteComplement = await treeCache.getNoteComplement(note.noteId);
|
||||
|
||||
$rendered = $('<div class="ck-content">').html(fullNote.content);
|
||||
$renderedContent.append($('<div class="ck-content">').html(trim(noteComplement.content, options.trim)));
|
||||
|
||||
if ($rendered.find('span.math-tex').length > 0) {
|
||||
if ($renderedContent.find('span.math-tex').length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.KATEX);
|
||||
|
||||
renderMathInElement($rendered[0], {});
|
||||
renderMathInElement($renderedContent[0], {});
|
||||
}
|
||||
}
|
||||
else if (type === 'code') {
|
||||
const fullNote = await server.get('notes/' + note.noteId);
|
||||
|
||||
$rendered = $("<pre>").text(fullNote.content);
|
||||
$renderedContent.append($("<pre>").text(trim(fullNote.content, options.trim)));
|
||||
}
|
||||
else if (type === 'image') {
|
||||
$rendered = $("<img>")
|
||||
.attr("src", `api/images/${note.noteId}/${note.title}`)
|
||||
.css("max-width", "100%");
|
||||
$renderedContent.append(
|
||||
$("<img>")
|
||||
.attr("src", `api/images/${note.noteId}/${note.title}`)
|
||||
.css("max-width", "100%")
|
||||
);
|
||||
}
|
||||
else if (type === 'file' || type === 'pdf') {
|
||||
else if (!options.tooltip && (type === 'file' || type === 'pdf')) {
|
||||
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
|
||||
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
|
||||
|
||||
@@ -41,48 +49,62 @@ async function getRenderedContent(note) {
|
||||
// open doesn't work for protected notes since it works through browser which isn't in protected session
|
||||
$openButton.toggle(!note.isProtected);
|
||||
|
||||
$rendered = $('<div style="display: flex; flex-direction: column; height: 100%;">');
|
||||
const $content = $('<div style="display: flex; flex-direction: column; height: 100%;">');
|
||||
|
||||
if (type === 'pdf') {
|
||||
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
|
||||
$pdfPreview.attr("src", openService.getUrlForDownload("api/notes/" + note.noteId + "/open"));
|
||||
|
||||
$rendered.append($pdfPreview);
|
||||
$content.append($pdfPreview);
|
||||
}
|
||||
|
||||
$rendered.append(
|
||||
$("<div>")
|
||||
$content.append(
|
||||
$('<div style="display: flex; justify-content: space-evenly; margin-top: 5px;">')
|
||||
.append($downloadButton)
|
||||
.append(' ')
|
||||
.append($openButton)
|
||||
);
|
||||
|
||||
$renderedContent.append($content);
|
||||
}
|
||||
else if (type === 'render') {
|
||||
$rendered = $('<div>');
|
||||
const $content = $('<div>');
|
||||
|
||||
await renderService.render(note, $rendered, this.ctx);
|
||||
await renderService.render(note, $content, this.ctx);
|
||||
|
||||
$renderedContent.append($content);
|
||||
}
|
||||
else if (type === 'protected-session') {
|
||||
else if (!options.tooltip && type === 'protected-session') {
|
||||
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
|
||||
.on('click', protectedSessionService.enterProtectedSession);
|
||||
|
||||
$rendered = $("<div>")
|
||||
.append("<div>This note is protected and to access it you need to enter password.</div>")
|
||||
.append("<br/>")
|
||||
.append($button);
|
||||
$renderedContent.append(
|
||||
$("<div>")
|
||||
.append("<div>This note is protected and to access it you need to enter password.</div>")
|
||||
.append("<br/>")
|
||||
.append($button)
|
||||
);
|
||||
}
|
||||
else {
|
||||
$rendered = $("<em>Content of this note cannot be displayed in the book format</em>");
|
||||
$renderedContent.append($("<em>Content of this note cannot be displayed in the book format</em>"));
|
||||
}
|
||||
|
||||
$rendered.addClass(note.getCssClass());
|
||||
$renderedContent.addClass(note.getCssClass());
|
||||
|
||||
return {
|
||||
renderedContent: $rendered,
|
||||
$renderedContent,
|
||||
type
|
||||
};
|
||||
}
|
||||
|
||||
function trim(text, doTrim) {
|
||||
if (!doTrim) {
|
||||
return text;
|
||||
}
|
||||
else {
|
||||
return text.substr(0, Math.min(text.length, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
function getRenderingType(note) {
|
||||
let type = note.type;
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ async function createNote(parentNoteId, options = {}) {
|
||||
await activeTabContext.setNote(note.noteId);
|
||||
|
||||
if (options.focus === 'title') {
|
||||
appContext.triggerCommand('focusAndSelectTitle');
|
||||
appContext.triggerEvent('focusAndSelectTitle');
|
||||
}
|
||||
else if (options.focus === 'content') {
|
||||
appContext.triggerCommand('focusOnDetail', {tabId: this.tabId});
|
||||
appContext.triggerEvent('focusOnDetail', {tabId: activeTabContext.tabId});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
import linkService from "./link.js";
|
||||
import noteContentRenderer from "./note_content_renderer.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
import attributeService from "./attributes.js";
|
||||
import attributeRenderer from "./attribute_renderer.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-list">
|
||||
<style>
|
||||
.note-list {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-list-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
flex-basis: 300px;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-expander {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.note-book-card {
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 10px 15px 15px 8px;
|
||||
margin: 5px 5px 5px 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.note-book-card:not(.expanded) .note-book-content {
|
||||
display: none !important;
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
.note-book-card.expanded .note-book-content {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.note-book-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.note-book-title .rendered-note-attributes {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.note-book-title .rendered-note-attributes:before {
|
||||
content: "\\00a0\\00a0";
|
||||
}
|
||||
|
||||
.note-book-card .note-book-card {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.note-book-content.type-image, .note-book-content.type-file, .note-book-content.type-protected-session {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.note-book-content.type-image img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.note-book-card.type-image .note-book-content img, .note-book-card.type-text .note-book-content img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.note-book-title {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.note-list-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.note-expander {
|
||||
font-size: x-large;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
padding-right: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-list-pager {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="btn-group floating-button" style="right: 20px; top: 10px;">
|
||||
<button type="button"
|
||||
class="collapse-all-button btn icon-button bx bx-layer-minus"
|
||||
title="Collapse all notes"></button>
|
||||
|
||||
|
||||
|
||||
<button type="button"
|
||||
class="expand-children-button btn icon-button bx bx-move-vertical"
|
||||
title="Expand all children"></button>
|
||||
|
||||
|
||||
|
||||
<button type="button"
|
||||
class="list-view-button btn icon-button bx bx-menu"
|
||||
title="List view"></button>
|
||||
|
||||
|
||||
|
||||
<button type="button"
|
||||
class="grid-view-button btn icon-button bx bx-grid-alt"
|
||||
title="Grid view"></button>
|
||||
</div>
|
||||
|
||||
<div class="note-list-wrapper">
|
||||
<div class="note-list-pager"></div>
|
||||
|
||||
<div class="note-list-container"></div>
|
||||
|
||||
<div class="note-list-pager"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
class NoteListRenderer {
|
||||
/*
|
||||
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
||||
*/
|
||||
constructor($parent, parentNote, noteIds) {
|
||||
this.$noteList = $(TPL);
|
||||
|
||||
// note list must be added to the DOM immediatelly, otherwise some functionality scripting (canvas) won't work
|
||||
$parent.empty();
|
||||
|
||||
this.parentNote = parentNote;
|
||||
const includedNoteIds = this.getIncludedNoteIds();
|
||||
|
||||
this.noteIds = noteIds.filter(noteId => !includedNoteIds.has(noteId));
|
||||
|
||||
if (this.noteIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent.append(this.$noteList);
|
||||
|
||||
this.page = 1;
|
||||
this.pageSize = parseInt(parentNote.getLabelValue('pageSize'));
|
||||
|
||||
if (!this.pageSize || this.pageSize < 1) {
|
||||
this.pageSize = 20;
|
||||
}
|
||||
|
||||
this.viewType = parentNote.getLabelValue('viewType');
|
||||
|
||||
if (!['list', 'grid'].includes(this.viewType)) {
|
||||
// when not explicitly set decide based on note type
|
||||
this.viewType = parentNote.type === 'search' ? 'list' : 'grid';
|
||||
}
|
||||
|
||||
this.$noteList.addClass(this.viewType + '-view');
|
||||
|
||||
this.$noteList.find('.list-view-button').on('click', () => this.toggleViewType('list'));
|
||||
this.$noteList.find('.grid-view-button').on('click', () => this.toggleViewType('grid'));
|
||||
|
||||
this.$noteList.find('.expand-children-button').on('click', async () => {
|
||||
if (!this.parentNote.hasLabel('expanded')) {
|
||||
await attributeService.addLabel(this.parentNote.noteId, 'expanded');
|
||||
}
|
||||
|
||||
await this.renderList();
|
||||
});
|
||||
|
||||
this.$noteList.find('.collapse-all-button').on('click', async () => {
|
||||
// owned is important - we shouldn't remove inherited expanded labels
|
||||
for (const expandedAttr of this.parentNote.getOwnedLabels('expanded')) {
|
||||
await attributeService.removeAttributeById(this.parentNote.noteId, expandedAttr.attributeId);
|
||||
}
|
||||
|
||||
await this.renderList();
|
||||
});
|
||||
}
|
||||
|
||||
/** @return {Set<string>} list of noteIds included (images, included notes) into a parent note and which
|
||||
* don't have to be shown in the note list. */
|
||||
getIncludedNoteIds() {
|
||||
const includedLinks = this.parentNote
|
||||
? this.parentNote.getRelations().filter(rel => rel.name === 'imageLink' || rel.name === 'includeNoteLink')
|
||||
: [];
|
||||
|
||||
return new Set(includedLinks.map(rel => rel.value));
|
||||
}
|
||||
|
||||
async toggleViewType(type) {
|
||||
if (type !== 'list' && type !== 'grid') {
|
||||
throw new Error(`Invalid view type ${type}`);
|
||||
}
|
||||
|
||||
this.viewType = type;
|
||||
|
||||
this.$noteList
|
||||
.removeClass('grid-view')
|
||||
.removeClass('list-view')
|
||||
.addClass(this.viewType + '-view');
|
||||
|
||||
await attributeService.setLabel(this.parentNote.noteId, 'viewType', type);
|
||||
|
||||
await this.renderList();
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
if (this.noteIds.length === 0) {
|
||||
this.$noteList.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.$noteList.show();
|
||||
|
||||
const $container = this.$noteList.find('.note-list-container').empty();
|
||||
|
||||
const startIdx = (this.page - 1) * this.pageSize;
|
||||
const endIdx = startIdx + this.pageSize;
|
||||
|
||||
const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length));
|
||||
const pageNotes = await treeCache.getNotes(pageNoteIds);
|
||||
|
||||
for (const note of pageNotes) {
|
||||
const $card = await this.renderNote(note, this.parentNote.hasLabel('expanded'));
|
||||
|
||||
$container.append($card);
|
||||
}
|
||||
|
||||
this.renderPager();
|
||||
|
||||
return this.$noteList;
|
||||
}
|
||||
|
||||
renderPager() {
|
||||
const $pager = this.$noteList.find('.note-list-pager').empty();
|
||||
const pageCount = Math.ceil(this.noteIds.length / this.pageSize);
|
||||
|
||||
$pager.toggle(pageCount > 1);
|
||||
|
||||
let lastPrinted;
|
||||
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) {
|
||||
lastPrinted = true;
|
||||
|
||||
$pager.append(
|
||||
i === this.page
|
||||
? $('<span>').text(i).css('text-decoration', 'underline').css('font-weight', "bold")
|
||||
: $('<a href="javascript:">')
|
||||
.text(i)
|
||||
.on('click', () => {
|
||||
this.page = i;
|
||||
this.renderList();
|
||||
}),
|
||||
" "
|
||||
);
|
||||
}
|
||||
else if (lastPrinted) {
|
||||
$pager.append("... ");
|
||||
|
||||
lastPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async renderNote(note, expand = false) {
|
||||
const $expander = $('<span class="note-expander bx bx-chevron-right"></span>');
|
||||
|
||||
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
|
||||
|
||||
const $card = $('<div class="note-book-card">')
|
||||
.attr('data-note-id', note.noteId)
|
||||
.append(
|
||||
$('<h5 class="note-book-title">')
|
||||
.append($expander)
|
||||
.append(await linkService.createNoteLink(note.noteId, {showTooltip: false}))
|
||||
.append($renderedAttributes)
|
||||
);
|
||||
|
||||
$expander.on('click', () => this.toggleContent($card, note, !$card.hasClass("expanded")));
|
||||
|
||||
await this.toggleContent($card, note, expand);
|
||||
|
||||
return $card;
|
||||
}
|
||||
|
||||
async toggleContent($card, note, expand) {
|
||||
if (this.viewType === 'list' && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $expander = $card.find('> .note-book-title .note-expander');
|
||||
|
||||
if (expand || this.viewType === 'grid') {
|
||||
$card.addClass("expanded");
|
||||
$expander.addClass("bx-chevron-down").removeClass("bx-chevron-right");
|
||||
}
|
||||
else {
|
||||
$card.removeClass("expanded");
|
||||
$expander.addClass("bx-chevron-right").removeClass("bx-chevron-down");
|
||||
}
|
||||
|
||||
if ((expand || this.viewType === 'grid') && $card.find('.note-book-content').length === 0) {
|
||||
$card.append(await this.renderNoteContent(note));
|
||||
}
|
||||
}
|
||||
|
||||
async renderNoteContent(note) {
|
||||
const $content = $('<div class="note-book-content">');
|
||||
|
||||
try {
|
||||
const {$renderedContent, type} = await noteContentRenderer.getRenderedContent(note, {
|
||||
trim: this.viewType === 'grid' // for grid only short content is needed
|
||||
});
|
||||
|
||||
$content.append($renderedContent);
|
||||
$content.addClass("type-" + type);
|
||||
} catch (e) {
|
||||
console.log(`Caught error while rendering note ${note.noteId} of type ${note.type}: ${e.message}, stack: ${e.stack}`);
|
||||
|
||||
$content.append("rendering error");
|
||||
}
|
||||
|
||||
if (this.viewType === 'list') {
|
||||
const imageLinks = note.getRelations('imageLink');
|
||||
|
||||
const childNotes = (await note.getChildNotes())
|
||||
.filter(childNote => !imageLinks.find(rel => rel.value === childNote.noteId));
|
||||
|
||||
for (const childNote of childNotes) {
|
||||
$content.append(await this.renderNote(childNote));
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteListRenderer;
|
||||
@@ -3,7 +3,7 @@ import linkService from "./link.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
import utils from "./utils.js";
|
||||
import attributeRenderer from "./attribute_renderer.js";
|
||||
import libraryLoader from "./library_loader.js";
|
||||
import noteContentRenderer from "./note_content_renderer.js";
|
||||
|
||||
function setupGlobalTooltip() {
|
||||
$(document).on("mouseenter", "a", mouseEnterHandler);
|
||||
@@ -45,9 +45,7 @@ async function mouseEnterHandler() {
|
||||
const noteId = treeService.getNoteIdFromNotePath(notePath);
|
||||
|
||||
const note = await treeCache.getNote(noteId);
|
||||
const noteComplement = await treeCache.getNoteComplement(noteId);
|
||||
|
||||
const content = await renderTooltip(note, noteComplement);
|
||||
const content = await renderTooltip(note);
|
||||
|
||||
if (utils.isHtmlEmpty(content)) {
|
||||
return;
|
||||
@@ -79,7 +77,7 @@ function mouseLeaveHandler() {
|
||||
$(this).tooltip('dispose');
|
||||
}
|
||||
|
||||
async function renderTooltip(note, noteComplement) {
|
||||
async function renderTooltip(note) {
|
||||
if (note.isDeleted) {
|
||||
return '<div>Note has been deleted.</div>';
|
||||
}
|
||||
@@ -90,39 +88,18 @@ async function renderTooltip(note, noteComplement) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = $("<h5>").text(await treeService.getNotePathTitle(someNotePath)).prop('outerHTML');
|
||||
let content = '<h5 class="note-tooltip-title">' + (await treeService.getNoteTitleWithPathAsSuffix(someNotePath)).prop('outerHTML') + '</h5>';
|
||||
|
||||
const attributes = note.getAttributes()
|
||||
.filter(attr => !attr.isDefinition());
|
||||
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
|
||||
|
||||
if (attributes.length > 0) {
|
||||
content += '<div class="tooltip-attributes"><strong>Attributes: </strong> '
|
||||
+ (await attributeRenderer.renderAttributes(attributes)).html()
|
||||
+ '</div>'
|
||||
}
|
||||
const {$renderedContent} = await noteContentRenderer.getRenderedContent(note, {
|
||||
tooltip: true,
|
||||
trim: true
|
||||
});
|
||||
|
||||
if (note.type === 'text' && !utils.isHtmlEmpty(noteComplement.content)) {
|
||||
const $content = $('<div class="ck-content">').append(noteComplement.content);
|
||||
|
||||
if ($content.find('span.math-tex').length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.KATEX);
|
||||
|
||||
renderMathInElement($content[0], {});
|
||||
}
|
||||
|
||||
content += $content[0].outerHTML;
|
||||
}
|
||||
else if (note.type === 'code' && noteComplement.content && noteComplement.content.trim()) {
|
||||
content += $("<pre>")
|
||||
.text(noteComplement.content)
|
||||
.prop('outerHTML');
|
||||
}
|
||||
else if (note.type === 'image') {
|
||||
content += $("<img>")
|
||||
.prop("src", `api/images/${note.noteId}/${note.title}`)
|
||||
.prop('outerHTML');
|
||||
}
|
||||
// other types of notes don't have tooltip preview
|
||||
content = content
|
||||
+ '<div class="note-tooltip-attributes">' + $renderedAttributes[0].outerHTML + '</div>'
|
||||
+ $renderedContent[0].outerHTML;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ import utils from './utils.js';
|
||||
|
||||
const REQUEST_LOGGING_ENABLED = false;
|
||||
|
||||
function getHeaders(headers) {
|
||||
async function getHeaders(headers) {
|
||||
const appContext = (await import('./app_context.js')).default;
|
||||
const activeTabContext = appContext.tabManager ? appContext.tabManager.getActiveTabContext() : null;
|
||||
|
||||
// headers need to be lowercase because node.js automatically converts them to lower case
|
||||
// so hypothetical protectedSessionId becomes protectedsessionid on the backend
|
||||
// also avoiding using underscores instead of dashes since nginx filters them out by default
|
||||
const allHeaders = {
|
||||
'trilium-source-id': glob.sourceId,
|
||||
'trilium-local-now-datetime': utils.localNowDateTime(),
|
||||
'trilium-hoisted-note-id': activeTabContext ? activeTabContext.hoistedNoteId : null,
|
||||
'x-csrf-token': glob.csrfToken
|
||||
};
|
||||
|
||||
@@ -44,6 +47,7 @@ async function remove(url, sourceId) {
|
||||
|
||||
let i = 1;
|
||||
const reqResolves = {};
|
||||
const reqRejects = {};
|
||||
|
||||
let maxKnownEntityChangeId = 0;
|
||||
|
||||
@@ -52,12 +56,15 @@ async function call(method, url, data, headers = {}) {
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
headers = await getHeaders(headers);
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const ipc = utils.dynamicRequire('electron').ipcRenderer;
|
||||
const requestId = i++;
|
||||
|
||||
resp = await new Promise((resolve, reject) => {
|
||||
reqResolves[requestId] = resolve;
|
||||
reqRejects[requestId] = reject;
|
||||
|
||||
if (REQUEST_LOGGING_ENABLED) {
|
||||
console.log(utils.now(), "Request #" + requestId + " to " + method + " " + url);
|
||||
@@ -65,7 +72,7 @@ async function call(method, url, data, headers = {}) {
|
||||
|
||||
ipc.send('server-request', {
|
||||
requestId: requestId,
|
||||
headers: getHeaders(headers),
|
||||
headers: headers,
|
||||
method: method,
|
||||
url: "/" + baseApiUrl + url,
|
||||
data: data
|
||||
@@ -91,12 +98,20 @@ async function call(method, url, data, headers = {}) {
|
||||
return resp.body;
|
||||
}
|
||||
|
||||
async function reportError(method, url, status, error) {
|
||||
const message = "Error when calling " + method + " " + url + ": " + status + " - " + error;
|
||||
|
||||
const toastService = (await import("./toast.js")).default;
|
||||
toastService.showError(message);
|
||||
toastService.throwError(message);
|
||||
}
|
||||
|
||||
function ajax(url, method, data, headers) {
|
||||
return new Promise((res, rej) => {
|
||||
const options = {
|
||||
url: baseApiUrl + url,
|
||||
type: method,
|
||||
headers: getHeaders(headers),
|
||||
headers: headers,
|
||||
timeout: 60000,
|
||||
success: (body, textStatus, jqXhr) => {
|
||||
const respHeaders = {};
|
||||
@@ -112,11 +127,8 @@ function ajax(url, method, data, headers) {
|
||||
headers: respHeaders
|
||||
});
|
||||
},
|
||||
error: async (jqXhr, textStatus, error) => {
|
||||
const message = "Error when calling " + method + " " + url + ": " + textStatus + " - " + error;
|
||||
const toastService = (await import("./toast.js")).default;
|
||||
toastService.showError(message);
|
||||
toastService.throwError(message);
|
||||
error: async (jqXhr, status, error) => {
|
||||
await reportError(method, url, status, error);
|
||||
|
||||
rej(error);
|
||||
}
|
||||
@@ -138,17 +150,25 @@ function ajax(url, method, data, headers) {
|
||||
if (utils.isElectron()) {
|
||||
const ipc = utils.dynamicRequire('electron').ipcRenderer;
|
||||
|
||||
ipc.on('server-response', (event, arg) => {
|
||||
ipc.on('server-response', async (event, arg) => {
|
||||
if (REQUEST_LOGGING_ENABLED) {
|
||||
console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
|
||||
}
|
||||
|
||||
reqResolves[arg.requestId]({
|
||||
body: arg.body,
|
||||
headers: arg.headers
|
||||
});
|
||||
if (arg.statusCode >= 200 && arg.statusCode < 300) {
|
||||
reqResolves[arg.requestId]({
|
||||
body: arg.body,
|
||||
headers: arg.headers
|
||||
});
|
||||
}
|
||||
else {
|
||||
await reportError(arg.method, arg.url, arg.statusCode, arg.body);
|
||||
|
||||
reqRejects[arg.requestId]();
|
||||
}
|
||||
|
||||
delete reqResolves[arg.requestId];
|
||||
delete reqRejects[arg.requestId];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,15 @@ function setupSplit(left, right) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftPaneWidth = options.getInt('leftPaneWidth');
|
||||
const rightPaneWidth = options.getInt('rightPaneWidth');
|
||||
let leftPaneWidth = options.getInt('leftPaneWidth');
|
||||
if (!leftPaneWidth || leftPaneWidth < 5) {
|
||||
leftPaneWidth = 5;
|
||||
}
|
||||
|
||||
let rightPaneWidth = options.getInt('rightPaneWidth');
|
||||
if (!rightPaneWidth || rightPaneWidth < 5) {
|
||||
rightPaneWidth = 5;
|
||||
}
|
||||
|
||||
if (left && right) {
|
||||
instance = Split(['#left-pane', '#center-pane', '#right-pane'], {
|
||||
@@ -49,4 +56,4 @@ function setupSplit(left, right) {
|
||||
|
||||
export default {
|
||||
setupSplit
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,10 +11,11 @@ class TabContext extends Component {
|
||||
/**
|
||||
* @param {string|null} tabId
|
||||
*/
|
||||
constructor(tabId = null) {
|
||||
constructor(tabId = null, hoistedNoteId = 'root') {
|
||||
super();
|
||||
|
||||
this.tabId = tabId || utils.randomString(4);
|
||||
this.hoistedNoteId = hoistedNoteId;
|
||||
}
|
||||
|
||||
setEmpty() {
|
||||
@@ -25,31 +26,10 @@ class TabContext extends Component {
|
||||
}
|
||||
|
||||
async setNote(inputNotePath, triggerSwitchEvent = true) {
|
||||
const noteId = treeService.getNoteIdFromNotePath(inputNotePath);
|
||||
let resolvedNotePath;
|
||||
const resolvedNotePath = await this.getResolvedNotePath(inputNotePath);
|
||||
|
||||
if ((await treeCache.getNote(noteId)).isDeleted) {
|
||||
// no point in trying to resolve canonical notePath
|
||||
resolvedNotePath = inputNotePath;
|
||||
}
|
||||
else {
|
||||
resolvedNotePath = await treeService.resolveNotePath(inputNotePath);
|
||||
|
||||
if (!resolvedNotePath) {
|
||||
logError(`Cannot resolve note path ${inputNotePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolvedNotePath === this.notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await hoistedNoteService.checkNoteAccess(resolvedNotePath) === false) {
|
||||
return; // note is outside of hoisted subtree and user chose not to unhoist
|
||||
}
|
||||
|
||||
// if user choise to unhoist, cache was reloaded, but might not contain this note (since it's on unexpanded path)
|
||||
await treeCache.getNote(noteId);
|
||||
if (!resolvedNotePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.triggerEvent('beforeNoteSwitch', {tabContext: this});
|
||||
@@ -57,21 +37,12 @@ class TabContext extends Component {
|
||||
utils.closeActiveDialog();
|
||||
|
||||
this.notePath = resolvedNotePath;
|
||||
this.noteId = noteId;
|
||||
this.noteId = treeService.getNoteIdFromNotePath(resolvedNotePath);
|
||||
|
||||
this.autoBookDisabled = false;
|
||||
this.textPreviewDisabled = false;
|
||||
this.codePreviewDisabled = false;
|
||||
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (resolvedNotePath && resolvedNotePath === this.notePath) {
|
||||
await server.post('recent-notes', {
|
||||
noteId: this.note.noteId,
|
||||
notePath: this.notePath
|
||||
});
|
||||
}
|
||||
}, 5000);
|
||||
this.saveToRecentNotes(resolvedNotePath);
|
||||
|
||||
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
||||
|
||||
@@ -88,6 +59,47 @@ class TabContext extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
saveToRecentNotes(resolvedNotePath) {
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (resolvedNotePath && resolvedNotePath === this.notePath) {
|
||||
await server.post('recent-notes', {
|
||||
noteId: this.note.noteId,
|
||||
notePath: this.notePath
|
||||
});
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async getResolvedNotePath(inputNotePath) {
|
||||
const noteId = treeService.getNoteIdFromNotePath(inputNotePath);
|
||||
|
||||
if ((await treeCache.getNote(noteId)).isDeleted) {
|
||||
// no point in trying to resolve canonical notePath
|
||||
return inputNotePath;
|
||||
}
|
||||
|
||||
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath);
|
||||
|
||||
if (!resolvedNotePath) {
|
||||
logError(`Cannot resolve note path ${inputNotePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolvedNotePath === this.notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await hoistedNoteService.checkNoteAccess(resolvedNotePath, this) === false) {
|
||||
return; // note is outside of hoisted subtree and user chose not to unhoist
|
||||
}
|
||||
|
||||
// if user choise to unhoist, cache was reloaded, but might not contain this note (since it's on unexpanded path)
|
||||
await treeCache.getNote(noteId);
|
||||
|
||||
return resolvedNotePath;
|
||||
}
|
||||
|
||||
/** @property {NoteShort} */
|
||||
get note() {
|
||||
if (this.noteId && !(this.noteId in treeCache.notes)) {
|
||||
@@ -123,10 +135,28 @@ class TabContext extends Component {
|
||||
return {
|
||||
tabId: this.tabId,
|
||||
notePath: this.notePath,
|
||||
hoistedNoteId: this.hoistedNoteId,
|
||||
active: this.isActive()
|
||||
}
|
||||
}
|
||||
|
||||
async unhoist() {
|
||||
await this.setHoistedNoteId('root');
|
||||
}
|
||||
|
||||
async setHoistedNoteId(noteIdToHoist) {
|
||||
if (this.notePathArray && !this.notePathArray.includes(noteIdToHoist)) {
|
||||
await this.setNote(noteIdToHoist);
|
||||
}
|
||||
|
||||
this.hoistedNoteId = noteIdToHoist;
|
||||
|
||||
await this.triggerEvent('hoistedNoteChanged', {
|
||||
noteId: noteIdToHoist,
|
||||
tabId: this.tabId
|
||||
});
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.isNoteReloaded(this.noteId)) {
|
||||
const note = await treeCache.getNote(this.noteId);
|
||||
|
||||
@@ -58,7 +58,8 @@ export default class TabManager extends Component {
|
||||
else {
|
||||
tabsToOpen.push({
|
||||
notePath: notePath,
|
||||
active: true
|
||||
active: true,
|
||||
hoistedNoteId: glob.extraHoistedNoteId || 'root'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -83,7 +84,8 @@ export default class TabManager extends Component {
|
||||
if (filteredTabs.length === 0) {
|
||||
filteredTabs.push({
|
||||
notePath: this.isMainWindow ? 'root' : '',
|
||||
active: true
|
||||
active: true,
|
||||
extraHoistedNoteId: glob.extraHoistedNoteId || 'root'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,7 +95,7 @@ export default class TabManager extends Component {
|
||||
|
||||
await this.tabsUpdate.allowUpdateWithoutChange(async () => {
|
||||
for (const tab of filteredTabs) {
|
||||
await this.openTabWithNote(tab.notePath, tab.active, tab.tabId);
|
||||
await this.openTabWithNote(tab.notePath, tab.active, tab.tabId, tab.hoistedNoteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -184,8 +186,8 @@ export default class TabManager extends Component {
|
||||
await tabContext.setEmpty();
|
||||
}
|
||||
|
||||
async openEmptyTab(tabId) {
|
||||
const tabContext = new TabContext(tabId);
|
||||
async openEmptyTab(tabId, hoistedNoteId) {
|
||||
const tabContext = new TabContext(tabId, hoistedNoteId);
|
||||
this.child(tabContext);
|
||||
|
||||
await this.triggerEvent('newTabOpened', {tabContext});
|
||||
@@ -193,8 +195,26 @@ export default class TabManager extends Component {
|
||||
return tabContext;
|
||||
}
|
||||
|
||||
async openTabWithNote(notePath, activate, tabId = null) {
|
||||
const tabContext = await this.openEmptyTab(tabId);
|
||||
/**
|
||||
* If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab.
|
||||
*/
|
||||
async openTabWithNoteWithHoisting(notePath) {
|
||||
const tabContext = this.getActiveTabContext();
|
||||
let hoistedNoteId = 'root';
|
||||
|
||||
if (tabContext) {
|
||||
const resolvedNotePath = await treeService.resolveNotePath(notePath);
|
||||
|
||||
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
|
||||
hoistedNoteId = tabContext.hoistedNoteId;
|
||||
}
|
||||
}
|
||||
|
||||
return this.openTabWithNote(notePath, false, null, hoistedNoteId);
|
||||
}
|
||||
|
||||
async openTabWithNote(notePath, activate, tabId, hoistedNoteId) {
|
||||
const tabContext = await this.openEmptyTab(tabId, hoistedNoteId);
|
||||
|
||||
if (notePath) {
|
||||
await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event
|
||||
@@ -330,22 +350,14 @@ export default class TabManager extends Component {
|
||||
}
|
||||
|
||||
moveTabToNewWindowCommand({tabId}) {
|
||||
const notePath = this.getTabContextById(tabId).notePath;
|
||||
const {notePath, hoistedNoteId} = this.getTabContextById(tabId);
|
||||
|
||||
this.removeTab(tabId);
|
||||
|
||||
this.triggerCommand('openInWindow', {notePath});
|
||||
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
|
||||
}
|
||||
|
||||
async hoistedNoteChangedEvent({hoistedNoteId}) {
|
||||
if (hoistedNoteId === 'root') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const tc of this.tabContexts.splice()) {
|
||||
if (tc.notePath && !tc.notePath.split("/").includes(hoistedNoteId)) {
|
||||
await this.removeTab(tc.tabId);
|
||||
}
|
||||
}
|
||||
hoistedNoteChangedEvent() {
|
||||
this.tabsUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
|
||||
path.push('root');
|
||||
}
|
||||
|
||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
const effectivePath = [];
|
||||
let childNoteId = null;
|
||||
let i = 0;
|
||||
@@ -91,10 +89,6 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
|
||||
|
||||
effectivePath.push(parentNoteId);
|
||||
childNoteId = parentNoteId;
|
||||
|
||||
if (parentNoteId === hoistedNoteId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return effectivePath.reverse();
|
||||
@@ -110,7 +104,7 @@ function getSomeNotePath(note) {
|
||||
while (cur.noteId !== 'root') {
|
||||
path.push(cur.noteId);
|
||||
|
||||
const parents = cur.getParentNotes();
|
||||
const parents = cur.getParentNotes().filter(note => note.type !== 'search');
|
||||
|
||||
if (!parents.length) {
|
||||
logError(`Can't find parents for note ${cur.noteId}`);
|
||||
@@ -242,10 +236,8 @@ async function getNoteTitle(noteId, parentNoteId = null) {
|
||||
return title;
|
||||
}
|
||||
|
||||
async function getNotePathTitle(notePath) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
const titlePath = [];
|
||||
async function getNotePathTitleComponents(notePath) {
|
||||
const titleComponents = [];
|
||||
|
||||
if (notePath.startsWith('root/')) {
|
||||
notePath = notePath.substr(5);
|
||||
@@ -253,20 +245,51 @@ async function getNotePathTitle(notePath) {
|
||||
|
||||
// special case when we want just root's title
|
||||
if (notePath === 'root') {
|
||||
return await getNoteTitle(notePath);
|
||||
titleComponents.push(await getNoteTitle(notePath));
|
||||
} else {
|
||||
let parentNoteId = 'root';
|
||||
|
||||
for (const noteId of notePath.split('/')) {
|
||||
titleComponents.push(await getNoteTitle(noteId, parentNoteId));
|
||||
|
||||
parentNoteId = noteId;
|
||||
}
|
||||
}
|
||||
|
||||
let parentNoteId = 'root';
|
||||
return titleComponents;
|
||||
}
|
||||
|
||||
for (const noteId of notePath.split('/')) {
|
||||
titlePath.push(await getNoteTitle(noteId, parentNoteId));
|
||||
async function getNotePathTitle(notePath) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
parentNoteId = noteId;
|
||||
}
|
||||
const titlePath = await getNotePathTitleComponents(notePath);
|
||||
|
||||
return titlePath.join(' / ');
|
||||
}
|
||||
|
||||
async function getNoteTitleWithPathAsSuffix(notePath) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
const titleComponents = await getNotePathTitleComponents(notePath);
|
||||
|
||||
if (!titleComponents || titleComponents.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const title = titleComponents[titleComponents.length - 1];
|
||||
const path = titleComponents.slice(0, titleComponents.length - 1);
|
||||
|
||||
const $titleWithPath = $('<span class="note-title-with-path">')
|
||||
.append($('<span class="note-title">').text(title));
|
||||
|
||||
if (path.length > 0) {
|
||||
$titleWithPath
|
||||
.append($('<span class="note-path">').text(' (' + path.join(' / ') + ')'));
|
||||
}
|
||||
|
||||
return $titleWithPath;
|
||||
}
|
||||
|
||||
function getHashValueFromAddress() {
|
||||
const str = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
|
||||
|
||||
@@ -295,6 +318,7 @@ export default {
|
||||
getBranchIdFromNotePath,
|
||||
getNoteTitle,
|
||||
getNotePathTitle,
|
||||
getNoteTitleWithPathAsSuffix,
|
||||
getHashValueFromAddress,
|
||||
parseNotePath
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import Branch from "../entities/branch.js";
|
||||
import NoteShort from "../entities/note_short.js";
|
||||
import Attribute from "../entities/attribute.js";
|
||||
import server from "./server.js";
|
||||
import appContext from "./app_context.js";
|
||||
import NoteComplement from "../entities/note_complement.js";
|
||||
|
||||
/**
|
||||
@@ -20,9 +21,6 @@ class TreeCache {
|
||||
async loadInitialTree() {
|
||||
const resp = await server.get('tree');
|
||||
|
||||
// FIXME: we need to do this to cover for ascendants of template notes which are not loaded
|
||||
await this.loadParents(resp, false);
|
||||
|
||||
// clear the cache only directly before adding new content which is important for e.g. switching to protected session
|
||||
|
||||
/** @type {Object.<string, NoteShort>} */
|
||||
@@ -43,45 +41,11 @@ class TreeCache {
|
||||
async loadSubTree(subTreeNoteId) {
|
||||
const resp = await server.get('tree?subTreeNoteId=' + subTreeNoteId);
|
||||
|
||||
await this.loadParents(resp, true);
|
||||
|
||||
this.addResp(resp);
|
||||
|
||||
return this.notes[subTreeNoteId];
|
||||
}
|
||||
|
||||
async loadParents(resp, additiveLoad) {
|
||||
const noteIds = new Set(resp.notes.map(note => note.noteId));
|
||||
const missingNoteIds = [];
|
||||
const existingNotes = additiveLoad ? this.notes : {};
|
||||
|
||||
for (const branch of resp.branches) {
|
||||
if (!(branch.parentNoteId in existingNotes) && !noteIds.has(branch.parentNoteId) && branch.parentNoteId !== 'none') {
|
||||
missingNoteIds.push(branch.parentNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr of resp.attributes) {
|
||||
if (attr.type === 'relation' && attr.name === 'template' && !(attr.value in existingNotes) && !noteIds.has(attr.value)) {
|
||||
missingNoteIds.push(attr.value);
|
||||
}
|
||||
|
||||
if (!(attr.noteId in existingNotes) && !noteIds.has(attr.noteId)) {
|
||||
missingNoteIds.push(attr.noteId);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingNoteIds.length > 0) {
|
||||
const newResp = await server.post('tree/load', { noteIds: missingNoteIds });
|
||||
|
||||
resp.notes = resp.notes.concat(newResp.notes);
|
||||
resp.branches = resp.branches.concat(newResp.branches);
|
||||
resp.attributes = resp.attributes.concat(newResp.attributes);
|
||||
|
||||
await this.loadParents(resp, additiveLoad);
|
||||
}
|
||||
}
|
||||
|
||||
addResp(resp) {
|
||||
const noteRows = resp.notes;
|
||||
const branchRows = resp.branches;
|
||||
@@ -111,7 +75,7 @@ class TreeCache {
|
||||
}
|
||||
|
||||
note.children = [];
|
||||
note.childToBranch = [];
|
||||
note.childToBranch = {};
|
||||
}
|
||||
|
||||
// we want to remove all "real" branches (represented in the database) since those will be created
|
||||
@@ -198,7 +162,6 @@ class TreeCache {
|
||||
|
||||
const resp = await server.post('tree/load', { noteIds });
|
||||
|
||||
await this.loadParents(resp, true);
|
||||
this.addResp(resp);
|
||||
|
||||
for (const note of resp.notes) {
|
||||
@@ -209,19 +172,17 @@ class TreeCache {
|
||||
throw new Error(`Search note ${note.noteId} failed: ${searchResultNoteIds}`);
|
||||
}
|
||||
|
||||
// force to load all the notes at once instead of one by one
|
||||
await this.getNotes(searchResultNoteIds);
|
||||
|
||||
// reset all the virtual branches from old search results
|
||||
if (note.noteId in treeCache.notes) {
|
||||
treeCache.notes[note.noteId].children = [];
|
||||
treeCache.notes[note.noteId].childToBranch = {};
|
||||
}
|
||||
|
||||
const branches = resp.branches.filter(b => b.noteId === note.noteId || b.parentNoteId === note.noteId);
|
||||
|
||||
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
|
||||
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
|
||||
branchId: "virt" + resultNoteId + '-' + note.noteId,
|
||||
branchId: "virt-" + note.noteId + '-' + resultNoteId,
|
||||
noteId: resultNoteId,
|
||||
parentNoteId: note.noteId,
|
||||
notePosition: (index + 1) * 10,
|
||||
@@ -236,6 +197,8 @@ class TreeCache {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
appContext.triggerEvent('notesReloaded', {noteIds});
|
||||
}
|
||||
|
||||
/** @return {NoteShort[]} */
|
||||
@@ -263,8 +226,7 @@ class TreeCache {
|
||||
console.trace(`Can't find note "${noteId}"`);
|
||||
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
}).filter(note => !!note);
|
||||
@@ -292,6 +254,10 @@ class TreeCache {
|
||||
}
|
||||
|
||||
getNoteFromCache(noteId) {
|
||||
if (!noteId) {
|
||||
throw new Error("Empty noteId");
|
||||
}
|
||||
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import treeService from './tree.js';
|
||||
import treeCache from "./tree_cache.js";
|
||||
import hoistedNoteService from './hoisted_note.js';
|
||||
import clipboard from './clipboard.js';
|
||||
import noteCreateService from "./note_create.js";
|
||||
import contextMenu from "./context_menu.js";
|
||||
@@ -40,7 +39,7 @@ class TreeContextMenu {
|
||||
const note = await treeCache.getNote(this.node.data.noteId);
|
||||
const branch = treeCache.getBranch(this.node.data.branchId);
|
||||
const isNotRoot = note.noteId !== 'root';
|
||||
const isHoisted = note.noteId === hoistedNoteService.getHoistedNoteId();
|
||||
const isHoisted = note.noteId === appContext.tabManager.getActiveTabContext().hoistedNoteId;
|
||||
const parentNote = isNotRoot ? await treeCache.getNote(branch.parentNoteId) : null;
|
||||
|
||||
// some actions don't support multi-note so they are disabled when notes are selected
|
||||
@@ -69,7 +68,7 @@ class TreeContextMenu {
|
||||
{ title: 'Search in subtree <kbd data-command="searchInSubtree"></kbd>', command: "searchInSubtree", uiIcon: "search",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
||||
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="ToggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "arrow-from-bottom" },
|
||||
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "door-open" },
|
||||
{ title: 'Edit branch prefix <kbd data-command="editBranchPrefix"></kbd>', command: "editBranchPrefix", uiIcon: "empty",
|
||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes},
|
||||
{ title: "Advanced", uiIcon: "empty", enabled: true, items: [
|
||||
@@ -110,7 +109,7 @@ class TreeContextMenu {
|
||||
const notePath = treeService.getNotePath(this.node);
|
||||
|
||||
if (command === 'openInTab') {
|
||||
appContext.tabManager.openTabWithNote(notePath);
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
||||
}
|
||||
else if (command === "insertNoteAfter") {
|
||||
const parentNoteId = this.node.data.parentNoteId;
|
||||
@@ -130,6 +129,8 @@ class TreeContextMenu {
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.log("Triggering", command, notePath);
|
||||
|
||||
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,23 @@ function timeLimit(promise, limitMs) {
|
||||
});
|
||||
}
|
||||
|
||||
function initHelpDropdown($el) {
|
||||
// stop inside clicks from closing the menu
|
||||
const $dropdownMenu = $el.find('.help-dropdown .dropdown-menu');
|
||||
$dropdownMenu.on('click', e => e.stopPropagation());
|
||||
|
||||
// previous propagation stop will also block help buttons from being opened so we need to re-init for this element
|
||||
initHelpButtons($dropdownMenu);
|
||||
}
|
||||
|
||||
const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"
|
||||
|
||||
function initHelpButtons($el) {
|
||||
$el.on("click", "*[data-help-page]", e => {
|
||||
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadApp,
|
||||
parseDate,
|
||||
@@ -355,5 +372,7 @@ export default {
|
||||
copySelectionToClipboard,
|
||||
isCKEditorInitialized,
|
||||
dynamicRequire,
|
||||
timeLimit
|
||||
timeLimit,
|
||||
initHelpDropdown,
|
||||
initHelpButtons
|
||||
};
|
||||
|
||||
@@ -40,8 +40,8 @@ let consumeQueuePromise = null;
|
||||
// to make sure each change event is processed only once. Not clear if this is still necessary
|
||||
const processedEntityChangeIds = new Set();
|
||||
|
||||
function logRows(syncRows) {
|
||||
const filteredRows = syncRows.filter(row =>
|
||||
function logRows(entityChanges) {
|
||||
const filteredRows = entityChanges.filter(row =>
|
||||
!processedEntityChangeIds.has(row.id)
|
||||
&& row.entityName !== 'recent_notes'
|
||||
&& (row.entityName !== 'options' || row.entityId !== 'openTabs'));
|
||||
@@ -59,16 +59,16 @@ async function handleMessage(event) {
|
||||
}
|
||||
|
||||
if (message.type === 'sync') {
|
||||
let syncRows = message.data;
|
||||
let entityChanges = message.data;
|
||||
lastPingTs = Date.now();
|
||||
|
||||
if (syncRows.length > 0) {
|
||||
logRows(syncRows);
|
||||
if (entityChanges.length > 0) {
|
||||
logRows(entityChanges);
|
||||
|
||||
syncDataQueue.push(...syncRows);
|
||||
syncDataQueue.push(...entityChanges);
|
||||
|
||||
// we set lastAcceptedEntityChangeId even before sync processing and send ping so that backend can start sending more updates
|
||||
lastAcceptedEntityChangeId = Math.max(lastAcceptedEntityChangeId, syncRows[syncRows.length - 1].id);
|
||||
lastAcceptedEntityChangeId = Math.max(lastAcceptedEntityChangeId, entityChanges[entityChanges.length - 1].id);
|
||||
sendPing();
|
||||
|
||||
// first wait for all the preceding consumers to finish
|
||||
@@ -141,13 +141,13 @@ async function runSafely(syncHandler, syncData) {
|
||||
|
||||
async function consumeSyncData() {
|
||||
if (syncDataQueue.length > 0) {
|
||||
const allSyncRows = syncDataQueue;
|
||||
const allEntityChanges = syncDataQueue;
|
||||
syncDataQueue = [];
|
||||
|
||||
const nonProcessedSyncRows = allSyncRows.filter(sync => !processedEntityChangeIds.has(sync.id));
|
||||
const nonProcessedEntityChanges = allEntityChanges.filter(ec => !processedEntityChangeIds.has(ec.id));
|
||||
|
||||
try {
|
||||
await utils.timeLimit(processSyncRows(nonProcessedSyncRows), 30000);
|
||||
await utils.timeLimit(processEntityChanges(nonProcessedEntityChanges), 30000);
|
||||
}
|
||||
catch (e) {
|
||||
logError(`Encountered error ${e.message}: ${e.stack}, reloading frontend.`);
|
||||
@@ -158,17 +158,17 @@ async function consumeSyncData() {
|
||||
utils.reloadApp();
|
||||
}
|
||||
else {
|
||||
console.log("nonProcessedSyncRows causing the timeout", nonProcessedSyncRows);
|
||||
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
|
||||
|
||||
alert(`Encountered error "${e.message}", check out the console.`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const syncRow of nonProcessedSyncRows) {
|
||||
processedEntityChangeIds.add(syncRow.id);
|
||||
for (const entityChange of nonProcessedEntityChanges) {
|
||||
processedEntityChangeIds.add(entityChange.id);
|
||||
}
|
||||
|
||||
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, allSyncRows[allSyncRows.length - 1].id);
|
||||
lastProcessedEntityChangeId = Math.max(lastProcessedEntityChangeId, allEntityChanges[allEntityChanges.length - 1].id);
|
||||
}
|
||||
|
||||
checkEntityChangeIdListeners();
|
||||
@@ -229,10 +229,10 @@ subscribeToMessages(message => {
|
||||
}
|
||||
});
|
||||
|
||||
async function processSyncRows(syncRows) {
|
||||
async function processEntityChanges(entityChanges) {
|
||||
const missingNoteIds = [];
|
||||
|
||||
for (const {entityName, entity} of syncRows) {
|
||||
for (const {entityName, entity} of entityChanges) {
|
||||
if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) {
|
||||
missingNoteIds.push(entity.parentNoteId);
|
||||
}
|
||||
@@ -251,33 +251,33 @@ async function processSyncRows(syncRows) {
|
||||
|
||||
const loadResults = new LoadResults(treeCache);
|
||||
|
||||
for (const sync of syncRows.filter(sync => sync.entityName === 'notes')) {
|
||||
const note = treeCache.notes[sync.entityId];
|
||||
for (const ec of entityChanges.filter(ec => ec.entityName === 'notes')) {
|
||||
const note = treeCache.notes[ec.entityId];
|
||||
|
||||
if (note) {
|
||||
note.update(sync.entity);
|
||||
loadResults.addNote(sync.entityId, sync.sourceId);
|
||||
note.update(ec.entity);
|
||||
loadResults.addNote(ec.entityId, ec.sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sync of syncRows.filter(sync => sync.entityName === 'branches')) {
|
||||
let branch = treeCache.branches[sync.entityId];
|
||||
const childNote = treeCache.notes[sync.entity.noteId];
|
||||
const parentNote = treeCache.notes[sync.entity.parentNoteId];
|
||||
for (const ec of entityChanges.filter(ec => ec.entityName === 'branches')) {
|
||||
let branch = treeCache.branches[ec.entityId];
|
||||
const childNote = treeCache.notes[ec.entity.noteId];
|
||||
const parentNote = treeCache.notes[ec.entity.parentNoteId];
|
||||
|
||||
if (branch) {
|
||||
branch.update(sync.entity);
|
||||
loadResults.addBranch(sync.entityId, sync.sourceId);
|
||||
branch.update(ec.entity);
|
||||
loadResults.addBranch(ec.entityId, ec.sourceId);
|
||||
|
||||
if (sync.entity.isDeleted) {
|
||||
if (ec.entity.isDeleted) {
|
||||
if (childNote) {
|
||||
childNote.parents = childNote.parents.filter(parentNoteId => parentNoteId !== sync.entity.parentNoteId);
|
||||
delete childNote.parentToBranch[sync.entity.parentNoteId];
|
||||
childNote.parents = childNote.parents.filter(parentNoteId => parentNoteId !== ec.entity.parentNoteId);
|
||||
delete childNote.parentToBranch[ec.entity.parentNoteId];
|
||||
}
|
||||
|
||||
if (parentNote) {
|
||||
parentNote.children = parentNote.children.filter(childNoteId => childNoteId !== sync.entity.noteId);
|
||||
delete parentNote.childToBranch[sync.entity.noteId];
|
||||
parentNote.children = parentNote.children.filter(childNoteId => childNoteId !== ec.entity.noteId);
|
||||
delete parentNote.childToBranch[ec.entity.noteId];
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -290,12 +290,12 @@ async function processSyncRows(syncRows) {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!sync.entity.isDeleted) {
|
||||
else if (!ec.entity.isDeleted) {
|
||||
if (childNote || parentNote) {
|
||||
branch = new Branch(treeCache, sync.entity);
|
||||
branch = new Branch(treeCache, ec.entity);
|
||||
treeCache.branches[branch.branchId] = branch;
|
||||
|
||||
loadResults.addBranch(sync.entityId, sync.sourceId);
|
||||
loadResults.addBranch(ec.entityId, ec.sourceId);
|
||||
|
||||
if (childNote) {
|
||||
childNote.addParent(branch.parentNoteId, branch.branchId);
|
||||
@@ -308,14 +308,14 @@ async function processSyncRows(syncRows) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const sync of syncRows.filter(sync => sync.entityName === 'note_reordering')) {
|
||||
for (const ec of entityChanges.filter(ec => ec.entityName === 'note_reordering')) {
|
||||
const parentNoteIdsToSort = new Set();
|
||||
|
||||
for (const branchId in sync.positions) {
|
||||
for (const branchId in ec.positions) {
|
||||
const branch = treeCache.branches[branchId];
|
||||
|
||||
if (branch) {
|
||||
branch.notePosition = sync.positions[branchId];
|
||||
branch.notePosition = ec.positions[branchId];
|
||||
|
||||
parentNoteIdsToSort.add(branch.parentNoteId);
|
||||
}
|
||||
@@ -329,20 +329,20 @@ async function processSyncRows(syncRows) {
|
||||
}
|
||||
}
|
||||
|
||||
loadResults.addNoteReordering(sync.entityId, sync.sourceId);
|
||||
loadResults.addNoteReordering(ec.entityId, ec.sourceId);
|
||||
}
|
||||
|
||||
// missing reloading the relation target note
|
||||
for (const sync of syncRows.filter(sync => sync.entityName === 'attributes')) {
|
||||
let attribute = treeCache.attributes[sync.entityId];
|
||||
const sourceNote = treeCache.notes[sync.entity.noteId];
|
||||
const targetNote = sync.entity.type === 'relation' && treeCache.notes[sync.entity.value];
|
||||
for (const ec of entityChanges.filter(ec => ec.entityName === 'attributes')) {
|
||||
let attribute = treeCache.attributes[ec.entityId];
|
||||
const sourceNote = treeCache.notes[ec.entity.noteId];
|
||||
const targetNote = ec.entity.type === 'relation' && treeCache.notes[ec.entity.value];
|
||||
|
||||
if (attribute) {
|
||||
attribute.update(sync.entity);
|
||||
loadResults.addAttribute(sync.entityId, sync.sourceId);
|
||||
attribute.update(ec.entity);
|
||||
loadResults.addAttribute(ec.entityId, ec.sourceId);
|
||||
|
||||
if (sync.entity.isDeleted) {
|
||||
if (ec.entity.isDeleted) {
|
||||
if (sourceNote) {
|
||||
sourceNote.attributes = sourceNote.attributes.filter(attributeId => attributeId !== attribute.attributeId);
|
||||
}
|
||||
@@ -352,13 +352,13 @@ async function processSyncRows(syncRows) {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!sync.entity.isDeleted) {
|
||||
else if (!ec.entity.isDeleted) {
|
||||
if (sourceNote || targetNote) {
|
||||
attribute = new Attribute(treeCache, sync.entity);
|
||||
attribute = new Attribute(treeCache, ec.entity);
|
||||
|
||||
treeCache.attributes[attribute.attributeId] = attribute;
|
||||
|
||||
loadResults.addAttribute(sync.entityId, sync.sourceId);
|
||||
loadResults.addAttribute(ec.entityId, ec.sourceId);
|
||||
|
||||
if (sourceNote && !sourceNote.attributes.includes(attribute.attributeId)) {
|
||||
sourceNote.attributes.push(attribute.attributeId);
|
||||
@@ -371,24 +371,24 @@ async function processSyncRows(syncRows) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const sync of syncRows.filter(sync => sync.entityName === 'note_contents')) {
|
||||
delete treeCache.noteComplementPromises[sync.entityId];
|
||||
for (const ec of entityChanges.filter(ec => ec.entityName === 'note_contents')) {
|
||||
delete treeCache.noteComplementPromises[ec.entityId];
|
||||
|
||||
loadResults.addNoteContent(sync.entityId, sync.sourceId);
|
||||
loadResults.addNoteContent(ec.entityId, ec.sourceId);
|
||||
}
|
||||
|
||||
for (const sync of syncRows.filter(sync => sync.entityName === 'note_revisions')) {
|
||||
loadResults.addNoteRevision(sync.entityId, sync.noteId, sync.sourceId);
|
||||
for (const ec of entityChanges.filter(ec => ec.entityName === 'note_revisions')) {
|
||||
loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.sourceId);
|
||||
}
|
||||
|
||||
for (const sync of syncRows.filter(sync => sync.entityName === 'options')) {
|
||||
if (sync.entity.name === 'openTabs') {
|
||||
for (const ec of entityChanges.filter(ec => ec.entityName === 'options')) {
|
||||
if (ec.entity.name === 'openTabs') {
|
||||
continue; // only noise
|
||||
}
|
||||
|
||||
options.set(sync.entity.name, sync.entity.value);
|
||||
options.set(ec.entity.name, ec.entity.value);
|
||||
|
||||
loadResults.addOption(sync.entity.name);
|
||||
loadResults.addOption(ec.entity.name);
|
||||
}
|
||||
|
||||
if (!loadResults.isEmpty()) {
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import TabAwareWidget from "./tab_aware_widget.js";
|
||||
import AttributeDetailWidget from "./attribute_detail.js";
|
||||
import attributeRenderer from "../services/attribute_renderer.js";
|
||||
import AttributeEditorWidget from "./attribute_editor.js";
|
||||
import options from '../services/options.js';
|
||||
import PromotedAttributesWidget from "./promoted_attributes.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="attribute-list">
|
||||
<style>
|
||||
.attribute-list {
|
||||
margin-left: 7px;
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inherited-attributes-wrapper {
|
||||
color: var(--muted-text-color);
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
.attribute-list-editor p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.attribute-list.error .attribute-list-editor {
|
||||
border-color: red !important;
|
||||
}
|
||||
|
||||
.attr-expander {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: var(--muted-text-color);
|
||||
font-size: 90%;
|
||||
margin: 3px 0 3px 0;
|
||||
}
|
||||
|
||||
.attribute-list hr {
|
||||
height: 1px;
|
||||
border-color: var(--main-border-color);
|
||||
position: relative;
|
||||
top: 4px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.attr-expander-text {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attr-expander:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.attr-expander:hover hr {
|
||||
border-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.attr-expander:hover .attr-expander-text {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="attr-expander attr-promoted-expander">
|
||||
<hr class="w-100">
|
||||
|
||||
<div class="attr-expander-text">Promoted attributes</div>
|
||||
|
||||
<hr class="w-100">
|
||||
</div>
|
||||
|
||||
<div class="all-attr-wrapper">
|
||||
<div class="promoted-attributes-placeholder"></div>
|
||||
|
||||
<div class="attr-expander attr-owned-and-inherited-expander">
|
||||
<hr class="w-100">
|
||||
|
||||
<div class="attr-expander-text"></div>
|
||||
|
||||
<hr class="w-100">
|
||||
</div>
|
||||
|
||||
<div class="owned-and-inherited-wrapper">
|
||||
<div class="attr-editor-placeholder"></div>
|
||||
|
||||
<hr class="w-100 attr-inherited-empty-expander" style="margin-bottom: 10px;">
|
||||
|
||||
<div class="attr-expander attr-inherited-expander">
|
||||
<hr class="w-100">
|
||||
|
||||
<div class="attr-expander-text"></div>
|
||||
|
||||
<hr class="w-100">
|
||||
</div>
|
||||
|
||||
<div class="inherited-attributes-wrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class AttributeListWidget extends TabAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.promotedAttributesWidget = new PromotedAttributesWidget().setParent(this);
|
||||
this.attributeDetailWidget = new AttributeDetailWidget().setParent(this);
|
||||
this.attributeEditorWidget = new AttributeEditorWidget(this.attributeDetailWidget).setParent(this);
|
||||
|
||||
this.child(this.promotedAttributesWidget, this.attributeEditorWidget, this.attributeDetailWidget);
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$promotedExpander = this.$widget.find('.attr-promoted-expander');
|
||||
this.$allAttrWrapper = this.$widget.find('.all-attr-wrapper');
|
||||
|
||||
this.$promotedExpander.on('click', async () => {
|
||||
const collapse = this.$allAttrWrapper.is(":visible");
|
||||
|
||||
await options.save('promotedAttributesExpanded', !collapse);
|
||||
|
||||
this.triggerEvent(`promotedAttributesCollapsedStateChanged`, {collapse});
|
||||
});
|
||||
|
||||
this.$ownedAndInheritedWrapper = this.$widget.find('.owned-and-inherited-wrapper');
|
||||
|
||||
this.$ownedExpander = this.$widget.find('.attr-owned-and-inherited-expander');
|
||||
this.$ownedExpander.on('click', async () => {
|
||||
const collapse = this.$ownedAndInheritedWrapper.is(":visible");
|
||||
|
||||
await options.save('attributeListExpanded', !collapse);
|
||||
|
||||
this.triggerEvent(`attributeListCollapsedStateChanged`, {collapse});
|
||||
});
|
||||
|
||||
this.$ownedExpanderText = this.$ownedExpander.find('.attr-expander-text');
|
||||
|
||||
this.$inheritedAttributesWrapper = this.$widget.find('.inherited-attributes-wrapper');
|
||||
|
||||
this.$inheritedExpander = this.$widget.find('.attr-inherited-expander');
|
||||
this.$inheritedExpander.on('click', () => {
|
||||
if (this.$inheritedAttributesWrapper.is(":visible")) {
|
||||
this.$inheritedAttributesWrapper.slideUp(200);
|
||||
}
|
||||
else {
|
||||
this.$inheritedAttributesWrapper.slideDown(200);
|
||||
}
|
||||
});
|
||||
|
||||
this.$inheritedExpanderText = this.$inheritedExpander.find('.attr-expander-text');
|
||||
|
||||
this.$inheritedEmptyExpander = this.$widget.find('.attr-inherited-empty-expander');
|
||||
|
||||
this.$widget.find('.promoted-attributes-placeholder').replaceWith(this.promotedAttributesWidget.render());
|
||||
this.$widget.find('.attr-editor-placeholder').replaceWith(this.attributeEditorWidget.render());
|
||||
this.$widget.append(this.attributeDetailWidget.render());
|
||||
}
|
||||
|
||||
async refreshWithNote(note, updateOnly = false) {
|
||||
if (!updateOnly) {
|
||||
const hasPromotedAttrs = this.promotedAttributesWidget.getPromotedDefinitionAttributes().length > 0;
|
||||
|
||||
if (hasPromotedAttrs) {
|
||||
this.$promotedExpander.show();
|
||||
this.$allAttrWrapper.toggle(options.is('promotedAttributesExpanded'));
|
||||
this.$ownedAndInheritedWrapper.hide();
|
||||
this.$inheritedAttributesWrapper.hide();
|
||||
} else {
|
||||
this.$promotedExpander.hide();
|
||||
this.$allAttrWrapper.show();
|
||||
this.$ownedAndInheritedWrapper.toggle(options.is('attributeListExpanded'));
|
||||
}
|
||||
}
|
||||
|
||||
const ownedAttributes = note.getOwnedAttributes().filter(attr => !attr.isAutoLink);
|
||||
|
||||
this.$ownedExpanderText.text(ownedAttributes.length + ' owned ' + this.attrPlural(ownedAttributes.length));
|
||||
|
||||
const inheritedAttributes = note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
||||
|
||||
if (inheritedAttributes.length === 0) {
|
||||
this.$inheritedExpander.hide();
|
||||
this.$inheritedEmptyExpander.show();
|
||||
}
|
||||
else {
|
||||
this.$inheritedExpander.show();
|
||||
this.$inheritedEmptyExpander.hide();
|
||||
}
|
||||
|
||||
this.$inheritedExpanderText.text(inheritedAttributes.length + ' inherited ' + this.attrPlural(inheritedAttributes.length));
|
||||
|
||||
this.$inheritedAttributesWrapper.empty();
|
||||
|
||||
await this.renderInheritedAttributes(inheritedAttributes, this.$inheritedAttributesWrapper);
|
||||
}
|
||||
|
||||
attrPlural(number) {
|
||||
return 'attribute' + (number === 1 ? '' : 's');
|
||||
}
|
||||
|
||||
async renderInheritedAttributes(attributes, $container) {
|
||||
for (const attribute of attributes) {
|
||||
const $attr = (await attributeRenderer.renderAttribute(attribute, false))
|
||||
.on('click', e => this.attributeDetailWidget.showAttributeDetail({
|
||||
attribute: {
|
||||
noteId: attribute.noteId,
|
||||
type: attribute.type,
|
||||
name: attribute.name,
|
||||
value: attribute.value,
|
||||
isInheritable: attribute.isInheritable
|
||||
},
|
||||
isOwned: false,
|
||||
x: e.pageX,
|
||||
y: e.pageY
|
||||
}));
|
||||
|
||||
$container
|
||||
.append($attr)
|
||||
.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
async saveAttributesCommand() {
|
||||
await this.attributeEditorWidget.save();
|
||||
}
|
||||
|
||||
async reloadAttributesCommand() {
|
||||
await this.attributeEditorWidget.refresh();
|
||||
}
|
||||
|
||||
async updateAttributeListCommand({attributes}) {
|
||||
await this.attributeEditorWidget.updateAttributeList(attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered
|
||||
* separately but should behave uniformly for the user.
|
||||
*/
|
||||
attributeListCollapsedStateChangedEvent({collapse}) {
|
||||
if (collapse) {
|
||||
this.$ownedAndInheritedWrapper.slideUp(200);
|
||||
} else {
|
||||
this.$ownedAndInheritedWrapper.slideDown(200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This event is used to synchronize collapsed state of all the tab-cached widgets since they are all rendered
|
||||
* separately but should behave uniformly for the user.
|
||||
*/
|
||||
promotedAttributesCollapsedStateChangedEvent({collapse}) {
|
||||
if (collapse) {
|
||||
this.$allAttrWrapper.slideUp(200);
|
||||
} else {
|
||||
this.$allAttrWrapper.slideDown(200);
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
|
||||
this.refreshWithNote(this.note, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -1,13 +1,13 @@
|
||||
import server from "../services/server.js";
|
||||
import treeCache from "../services/tree_cache.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import linkService from "../services/link.js";
|
||||
import attributeAutocompleteService from "../services/attribute_autocomplete.js";
|
||||
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||
import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js';
|
||||
import TabAwareWidget from "./tab_aware_widget.js";
|
||||
import SpacedUpdate from "../services/spaced_update.js";
|
||||
import utils from "../services/utils.js";
|
||||
import server from "../../services/server.js";
|
||||
import treeCache from "../../services/tree_cache.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import promotedAttributeDefinitionParser from '../../services/promoted_attribute_definition_parser.js';
|
||||
import TabAwareWidget from "../tab_aware_widget.js";
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="attr-detail">
|
||||
@@ -248,6 +248,8 @@ export default class AttributeDetailWidget extends TabAwareWidget {
|
||||
this.$rowValue = this.$widget.find('.attr-row-value');
|
||||
this.$inputValue = this.$widget.find('.attr-input-value');
|
||||
this.$inputValue.on('keyup', () => this.userEditedAttribute());
|
||||
this.$inputValue.on('change', () => this.userEditedAttribute());
|
||||
this.$inputValue.on('autocomplete:closed', () => this.userEditedAttribute());
|
||||
this.$inputValue.on('focus', () => {
|
||||
attributeAutocompleteService.initLabelValueAutocomplete({
|
||||
$el: this.$inputValue,
|
||||
+15
-18
@@ -1,13 +1,13 @@
|
||||
import TabAwareWidget from "./tab_aware_widget.js";
|
||||
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||
import server from "../services/server.js";
|
||||
import contextMenuService from "../services/context_menu.js";
|
||||
import attributesParser from "../services/attribute_parser.js";
|
||||
import libraryLoader from "../services/library_loader.js";
|
||||
import treeCache from "../services/tree_cache.js";
|
||||
import attributeRenderer from "../services/attribute_renderer.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import TabAwareWidget from "../tab_aware_widget.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import server from "../../services/server.js";
|
||||
import contextMenuService from "../../services/context_menu.js";
|
||||
import attributesParser from "../../services/attribute_parser.js";
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import treeCache from "../../services/tree_cache.js";
|
||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
||||
import noteCreateService from "../../services/note_create.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
|
||||
const HELP_TEXT = `
|
||||
<p>To add label, just type e.g. <code>#rock</code> or if you want to add also value then e.g. <code>#year = 2020</code></p>
|
||||
@@ -17,7 +17,7 @@ const HELP_TEXT = `
|
||||
<p>Alternatively you can add label and relation using the <code>+</code> button on the right side.</p>`;
|
||||
|
||||
const TPL = `
|
||||
<div style="position: relative">
|
||||
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
|
||||
<style>
|
||||
.attribute-list-editor {
|
||||
border: 0 !important;
|
||||
@@ -34,7 +34,7 @@ const TPL = `
|
||||
.save-attributes-button {
|
||||
color: var(--muted-text-color);
|
||||
position: absolute;
|
||||
bottom: 3px;
|
||||
bottom: 14px;
|
||||
right: 25px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
@@ -44,7 +44,7 @@ const TPL = `
|
||||
.add-new-attribute-button {
|
||||
color: var(--muted-text-color);
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
bottom: 13px;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
@@ -446,11 +446,8 @@ export default class AttributeEditorWidget extends TabAwareWidget {
|
||||
if (!note) {
|
||||
title = '[missing]';
|
||||
}
|
||||
else if (!note.isDeleted) {
|
||||
title = note.title;
|
||||
}
|
||||
else {
|
||||
title = note.isErased ? '[erased]' : `${note.title} (deleted)`;
|
||||
title = note.isDeleted ? `${note.title} (deleted)` : note.title;
|
||||
}
|
||||
|
||||
$el.text(title);
|
||||
@@ -488,7 +485,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.triggerCommand('focusOnDetail', {tabId: this.tabContext.tabId});
|
||||
this.triggerEvent('focusOnDetail', {tabId: this.tabContext.tabId});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class BasicWidget extends Component {
|
||||
}
|
||||
|
||||
overflowing() {
|
||||
this.css('contain', 'none');
|
||||
this.css('contain', 'none !important');
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import TabAwareWidget from "./tab_aware_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="section-container">
|
||||
<style>
|
||||
.section-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.section-title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 7px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
color: var(--muted-text-color);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.section-title.active {
|
||||
color: var(--main-text-color);
|
||||
border-bottom: 1px solid var(--main-text-color);
|
||||
}
|
||||
|
||||
.section-title:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-title:hover {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.section-title-empty {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
display: none;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.section-body.active {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="section-title-container"></div>
|
||||
<div class="section-body-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class CollapsibleSectionContainer extends TabAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.children = [];
|
||||
|
||||
this.positionCounter = 10;
|
||||
}
|
||||
|
||||
child(...components) {
|
||||
if (!components) {
|
||||
return this;
|
||||
}
|
||||
|
||||
super.child(...components);
|
||||
|
||||
for (const component of components) {
|
||||
if (!component.position) {
|
||||
component.position = this.positionCounter;
|
||||
this.positionCounter += 10;
|
||||
}
|
||||
}
|
||||
|
||||
this.children.sort((a, b) => a.position - b.position < 0 ? -1 : 1);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$titleContainer = this.$widget.find('.section-title-container');
|
||||
this.$bodyContainer = this.$widget.find('.section-body-container');
|
||||
|
||||
for (const widget of this.children) {
|
||||
this.$bodyContainer.append(
|
||||
$('<div class="section-body">')
|
||||
.attr('data-section-component-id', widget.componentId)
|
||||
.append(widget.render())
|
||||
);
|
||||
}
|
||||
|
||||
this.$titleContainer.on('click', '.section-title-real', e => {
|
||||
const $sectionTitle = $(e.target).closest('.section-title-real');
|
||||
|
||||
const activate = !$sectionTitle.hasClass("active");
|
||||
|
||||
this.$titleContainer.find('.section-title-real').removeClass("active");
|
||||
this.$bodyContainer.find('.section-body').removeClass("active");
|
||||
|
||||
if (activate) {
|
||||
const sectionComponentId = $sectionTitle.attr('data-section-component-id');
|
||||
|
||||
this.lastActiveComponentId = sectionComponentId;
|
||||
|
||||
this.$titleContainer.find(`.section-title-real[data-section-component-id="${sectionComponentId}"]`).addClass("active");
|
||||
this.$bodyContainer.find(`.section-body[data-section-component-id="${sectionComponentId}"]`).addClass("active");
|
||||
}
|
||||
else {
|
||||
this.lastActiveComponentId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
let $sectionToActivate, $lastActiveSection;
|
||||
|
||||
this.$titleContainer.empty().append('<div class="section-title section-title-empty">');
|
||||
|
||||
for (const widget of this.children) {
|
||||
const ret = widget.renderTitle(note);
|
||||
|
||||
if (!ret.show) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $sectionTitle = $('<div class="section-title section-title-real">')
|
||||
.attr('data-section-component-id', widget.componentId)
|
||||
.append(ret.$title);
|
||||
|
||||
this.$titleContainer.append($sectionTitle);
|
||||
this.$titleContainer.append('<div class="section-title section-title-empty">');
|
||||
|
||||
if (ret.activate && !$sectionToActivate) {
|
||||
$sectionToActivate = $sectionTitle;
|
||||
}
|
||||
|
||||
if (this.lastActiveComponentId === widget.componentId) {
|
||||
$lastActiveSection = $sectionTitle;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$sectionToActivate) {
|
||||
$sectionToActivate = $lastActiveSection;
|
||||
}
|
||||
|
||||
if ($sectionToActivate) {
|
||||
$sectionToActivate.trigger('click');
|
||||
}
|
||||
else {
|
||||
this.$bodyContainer.find('.section-body').removeClass("active");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,24 @@ import options from "../services/options.js";
|
||||
|
||||
const WIDGET_TPL = `
|
||||
<div class="card widget">
|
||||
<div class="card-header">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span class="widget-title">
|
||||
Collapsible Group Item
|
||||
</span>
|
||||
<a class="widget-toggle-button no-arrow"
|
||||
title="Minimize/maximize widget"
|
||||
data-toggle="collapse" data-target="#[to be set]">
|
||||
|
||||
<span class="widget-toggle-icon bx"></span>
|
||||
|
||||
<span class="widget-title">
|
||||
Collapsible Group Item
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<span class="widget-header-actions"></span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a class="widget-help external no-arrow bx bx-info-circle"></a>
|
||||
|
||||
<a class="widget-toggle-button no-arrow bx bx-minus"
|
||||
title="Minimize/maximize widget"
|
||||
data-toggle="collapse" data-target="#[to be set]"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,13 +48,14 @@ export default class CollapsibleWidget extends TabAwareWidget {
|
||||
this.widgetName = this.widgetTitle.replace(/[^[a-zA-Z0-9]/g, "_");
|
||||
|
||||
this.$toggleButton = this.$widget.find('.widget-toggle-button');
|
||||
this.$toggleIcon = this.$widget.find('.widget-toggle-icon');
|
||||
|
||||
const collapsed = options.is(this.widgetName + 'Collapsed');
|
||||
if (!collapsed) {
|
||||
this.$bodyWrapper.collapse("show");
|
||||
}
|
||||
|
||||
this.updateToggleButton(collapsed);
|
||||
this.updateToggleIcon(collapsed);
|
||||
|
||||
// using immediate variants of the event so that the previous collapse is not caught
|
||||
this.$bodyWrapper.on('hide.bs.collapse', () => this.toggleCollapsed(true));
|
||||
@@ -85,23 +89,23 @@ export default class CollapsibleWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
toggleCollapsed(collapse) {
|
||||
this.updateToggleButton(collapse);
|
||||
this.updateToggleIcon(collapse);
|
||||
|
||||
options.save(this.widgetName + 'Collapsed', collapse.toString());
|
||||
|
||||
this.triggerEvent(`widgetCollapsedStateChanged`, {widgetName: this.widgetName, collapse});
|
||||
}
|
||||
|
||||
updateToggleButton(collapse) {
|
||||
updateToggleIcon(collapse) {
|
||||
if (collapse) {
|
||||
this.$toggleButton
|
||||
.addClass("bx-window")
|
||||
.removeClass("bx-minus")
|
||||
this.$toggleIcon
|
||||
.addClass("bx-chevron-right")
|
||||
.removeClass("bx-chevron-down")
|
||||
.attr("title", "Show");
|
||||
} else {
|
||||
this.$toggleButton
|
||||
.addClass("bx-minus")
|
||||
.removeClass("bx-window")
|
||||
this.$toggleIcon
|
||||
.addClass("bx-chevron-down")
|
||||
.removeClass("bx-chevron-right")
|
||||
.attr("title", "Hide");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@ import linkService from "../../services/link.js";
|
||||
import server from "../../services/server.js";
|
||||
import treeCache from "../../services/tree_cache.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="edited-notes-widget">
|
||||
<style>
|
||||
.edited-notes-widget .edited-note-line {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class EditedNotesWidget extends CollapsibleWidget {
|
||||
get widgetTitle() { return "Edited notes on this day"; }
|
||||
|
||||
@@ -17,6 +29,11 @@ export default class EditedNotesWidget extends CollapsibleWidget {
|
||||
&& this.note.hasOwnedLabel("dateNote");
|
||||
}
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.html(TPL);
|
||||
this.$editedNotes = this.$body.find('.edited-notes-widget');
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
// remember which title was when we found the similar notes
|
||||
this.title = note.title;
|
||||
@@ -33,13 +50,18 @@ export default class EditedNotesWidget extends CollapsibleWidget {
|
||||
|
||||
await treeCache.getNotes(noteIds, true); // preload all at once
|
||||
|
||||
const $list = $('<ul>');
|
||||
const $list = $('<div>'); // not using <ul> because it's difficult to style correctly with text-overflow
|
||||
|
||||
for (const editedNote of editedNotes) {
|
||||
const $item = $("<li>");
|
||||
const $item = $('<div class="edited-note-line">');
|
||||
|
||||
if (editedNote.isDeleted) {
|
||||
$item.append($("<i>").text(editedNote.title + " (deleted)"));
|
||||
const title = editedNote.title + " (deleted)";
|
||||
$item.append(
|
||||
$("<i>")
|
||||
.text(title)
|
||||
.attr("title", title)
|
||||
);
|
||||
}
|
||||
else {
|
||||
$item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title);
|
||||
@@ -48,6 +70,6 @@ export default class EditedNotesWidget extends CollapsibleWidget {
|
||||
$list.append($item);
|
||||
}
|
||||
|
||||
this.$body.empty().append($list);
|
||||
this.$editedNotes.empty().append($list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class LinkMapWidget extends CollapsibleWidget {
|
||||
|
||||
get headerActions() {
|
||||
const $showFullButton = $("<a>")
|
||||
.addClass("bx bx-map-alt")
|
||||
.addClass("bx bx-network-chart")
|
||||
.addClass('widget-header-action')
|
||||
.attr('title', 'Show full link map');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import CollapsibleWidget from "../collapsible_widget.js";
|
||||
import server from "../../services/server.js";
|
||||
|
||||
const TPL = `
|
||||
<table class="note-info-widget-table">
|
||||
@@ -38,6 +39,21 @@ const TPL = `
|
||||
<th>Modified:</th>
|
||||
<td class="note-info-date-modified"></td>
|
||||
</tr>
|
||||
<tr title="Note size provides rough estimate of storage requirements for this note. It takes into account note's content and content of its note revisions.">
|
||||
<th>Note size:</th>
|
||||
|
||||
<td colspan="3">
|
||||
<button class="btn btn-sm calculate-button" style="padding: 0px 10px 0px 10px;">
|
||||
<span class="bx bx-calculator"></span> calculate
|
||||
</button>
|
||||
|
||||
<span class="note-sizes-wrapper">
|
||||
<span class="note-size"></span>
|
||||
|
||||
(subtree size: <span class="subtree-size"></span>)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
@@ -56,6 +72,25 @@ export default class NoteInfoWidget extends CollapsibleWidget {
|
||||
this.$dateModified = this.$body.find(".note-info-date-modified");
|
||||
this.$type = this.$body.find(".note-info-type");
|
||||
this.$mime = this.$body.find(".note-info-mime");
|
||||
|
||||
this.$noteSizesWrapper = this.$body.find('.note-sizes-wrapper');
|
||||
this.$noteSize = this.$body.find(".note-size");
|
||||
this.$subTreeSize = this.$body.find(".subtree-size");
|
||||
|
||||
this.$calculateButton = this.$body.find(".calculate-button");
|
||||
this.$calculateButton.on('click', async () => {
|
||||
this.$noteSizesWrapper.show();
|
||||
this.$calculateButton.hide();
|
||||
|
||||
this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
|
||||
this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
|
||||
|
||||
const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`);
|
||||
this.$noteSize.text(this.formatSize(noteSizeResp.noteSize));
|
||||
|
||||
const subTreeSizeResp = await server.get(`stats/subtree-size/${this.noteId}`);
|
||||
this.$subTreeSize.text(this.formatSize(subTreeSizeResp.subTreeSize));
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
@@ -78,6 +113,20 @@ export default class NoteInfoWidget extends CollapsibleWidget {
|
||||
else {
|
||||
this.$mime.empty();
|
||||
}
|
||||
|
||||
this.$calculateButton.show();
|
||||
this.$noteSizesWrapper.hide();
|
||||
}
|
||||
|
||||
formatSize(size) {
|
||||
size = Math.max(Math.round(size / 1024), 1);
|
||||
|
||||
if (size < 1024) {
|
||||
return `${size} KiB`;
|
||||
}
|
||||
else {
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export default class WhatLinksHereWidget extends CollapsibleWidget {
|
||||
|
||||
get headerActions() {
|
||||
const $showFullButton = $("<a>")
|
||||
.addClass("bx bx-map-alt")
|
||||
.addClass("bx bx-network-chart")
|
||||
.addClass('widget-header-action')
|
||||
.attr('title', 'Show full link map');
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
|
||||
export default class Container extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.children = [];
|
||||
|
||||
this.positionCounter = 10;
|
||||
}
|
||||
|
||||
child(...components) {
|
||||
if (!components) {
|
||||
return this;
|
||||
}
|
||||
|
||||
super.child(...components);
|
||||
|
||||
for (const component of components) {
|
||||
if (!component.position) {
|
||||
component.position = this.positionCounter;
|
||||
this.positionCounter += 10;
|
||||
}
|
||||
}
|
||||
|
||||
this.children.sort((a, b) => a.position - b.position < 0 ? -1 : 1);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(`<div>`);
|
||||
|
||||
for (const widget of this.children) {
|
||||
this.$widget.append(widget.render());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import Container from "./container.js";
|
||||
|
||||
export default class FlexContainer extends BasicWidget {
|
||||
export default class FlexContainer extends Container {
|
||||
constructor(direction) {
|
||||
super();
|
||||
|
||||
@@ -9,36 +9,5 @@ export default class FlexContainer extends BasicWidget {
|
||||
}
|
||||
|
||||
this.attrs.style = `display: flex; flex-direction: ${direction};`;
|
||||
|
||||
this.children = [];
|
||||
|
||||
this.positionCounter = 10;
|
||||
}
|
||||
|
||||
child(...components) {
|
||||
if (!components) {
|
||||
return this;
|
||||
}
|
||||
|
||||
super.child(...components);
|
||||
|
||||
for (const component of components) {
|
||||
if (!component.position) {
|
||||
component.position = this.positionCounter;
|
||||
this.positionCounter += 10;
|
||||
}
|
||||
}
|
||||
|
||||
this.children.sort((a, b) => a.position - b.position < 0 ? -1 : 1);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(`<div>`);
|
||||
|
||||
for (const widget of this.children) {
|
||||
this.$widget.append(widget.render());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
|
||||
const WIDGET_TPL = `
|
||||
<div class="global-buttons">
|
||||
<style>
|
||||
.global-buttons {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 7px;
|
||||
margin: 3px 5px 5px 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<a data-trigger-command="createTopLevelNote"
|
||||
title="Create new top level note"
|
||||
class="icon-action bx bx-folder-plus"></a>
|
||||
|
||||
<a data-trigger-command="collapseTree"
|
||||
title="Collapse note tree"
|
||||
class="icon-action bx bx-layer-minus"></a>
|
||||
|
||||
<a data-trigger-command="scrollToActiveNote"
|
||||
title="Scroll to active note"
|
||||
class="icon-action bx bx-crosshair"></a>
|
||||
|
||||
<a data-trigger-command="searchNotes"
|
||||
title="Search in notes"
|
||||
class="icon-action bx bx-search"></a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
class GlobalButtonsWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(WIDGET_TPL);
|
||||
this.overflowing();
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalButtonsWidget;
|
||||
@@ -17,9 +17,17 @@ const TPL = `
|
||||
}
|
||||
|
||||
.global-menu button {
|
||||
margin-right: 10px;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
height: 34px;
|
||||
border: none;
|
||||
font-size: 130%;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.global-menu button:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.global-menu .dropdown-menu {
|
||||
@@ -28,10 +36,8 @@ const TPL = `
|
||||
</style>
|
||||
|
||||
<div class="dropdown global-menu">
|
||||
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
|
||||
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm" title="Menu">
|
||||
<span class="bx bx-menu"></span>
|
||||
Menu
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item options-button" data-trigger-command="showOptions">
|
||||
@@ -122,6 +128,6 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
this.$widget.find(".open-dev-tools-button").toggle(utils.isElectron());
|
||||
|
||||
this.$widget.on('click', '.dropdown-item',
|
||||
() => this.$widget.find('.dropdown-toggle').dropdown('toggle'));
|
||||
() => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -59,7 +59,7 @@ const TPL = `
|
||||
</style>
|
||||
|
||||
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
|
||||
Note actions
|
||||
Actions
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
|
||||
@@ -9,7 +9,6 @@ import EditableTextTypeWidget from "./type_widgets/editable_text.js";
|
||||
import EditableCodeTypeWidget from "./type_widgets/editable_code.js";
|
||||
import FileTypeWidget from "./type_widgets/file.js";
|
||||
import ImageTypeWidget from "./type_widgets/image.js";
|
||||
import SearchTypeWidget from "./type_widgets/search.js";
|
||||
import RenderTypeWidget from "./type_widgets/render.js";
|
||||
import RelationMapTypeWidget from "./type_widgets/relation_map.js";
|
||||
import ProtectedSessionTypeWidget from "./type_widgets/protected_session.js";
|
||||
@@ -20,13 +19,12 @@ import noteCreateService from "../services/note_create.js";
|
||||
import DeletedTypeWidget from "./type_widgets/deleted.js";
|
||||
import ReadOnlyTextTypeWidget from "./type_widgets/read_only_text.js";
|
||||
import ReadOnlyCodeTypeWidget from "./type_widgets/read_only_code.js";
|
||||
import NoneTypeWidget from "./type_widgets/none.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail">
|
||||
<style>
|
||||
.note-detail {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
font-family: var(--detail-font-family);
|
||||
font-size: var(--detail-font-size);
|
||||
}
|
||||
@@ -43,7 +41,7 @@ const typeWidgetClasses = {
|
||||
'read-only-code': ReadOnlyCodeTypeWidget,
|
||||
'file': FileTypeWidget,
|
||||
'image': ImageTypeWidget,
|
||||
'search': SearchTypeWidget,
|
||||
'search': NoneTypeWidget,
|
||||
'render': RenderTypeWidget,
|
||||
'relation-map': RelationMapTypeWidget,
|
||||
'protected-session': ProtectedSessionTypeWidget,
|
||||
@@ -75,6 +73,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
|
||||
this.$widget.on("dragover", e => e.preventDefault());
|
||||
|
||||
@@ -170,17 +169,6 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
||||
|
||||
let type = note.type;
|
||||
|
||||
if (type === 'text' && !this.tabContext.autoBookDisabled
|
||||
&& note.hasChildren()
|
||||
&& utils.isDesktop()) {
|
||||
|
||||
const noteComplement = await this.tabContext.getNoteComplement();
|
||||
|
||||
if (utils.isHtmlEmpty(noteComplement.content)) {
|
||||
type = 'book';
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'text' && !this.tabContext.textPreviewDisabled) {
|
||||
const noteComplement = await this.tabContext.getNoteComplement();
|
||||
|
||||
@@ -261,7 +249,6 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
||||
loadCSS: [
|
||||
"libraries/codemirror/codemirror.css",
|
||||
"libraries/ckeditor/ckeditor-content.css",
|
||||
"libraries/ckeditor/ckeditor-content.css",
|
||||
"libraries/bootstrap/css/bootstrap.min.css",
|
||||
"libraries/katex/katex.min.css",
|
||||
"stylesheets/print.css",
|
||||
@@ -272,8 +259,10 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
||||
});
|
||||
}
|
||||
|
||||
hoistedNoteChangedEvent() {
|
||||
this.refresh();
|
||||
hoistedNoteChangedEvent({tabId}) {
|
||||
if (this.isTab(tabId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
@@ -307,12 +296,6 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
||||
this.spacedUpdate.updateNowIfNecessary();
|
||||
}
|
||||
|
||||
autoBookDisabledEvent({tabContext}) {
|
||||
if (this.isTab(tabContext.tabId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
textPreviewDisabledEvent({tabContext}) {
|
||||
if (this.isTab(tabContext.tabId)) {
|
||||
this.refresh();
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import TabAwareWidget from "./tab_aware_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-icon-container dropdown">
|
||||
<style>
|
||||
.note-icon-container {
|
||||
padding-top: 3px;
|
||||
padding-left: 7px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.note-icon-container button {
|
||||
font-size: 180%;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-icon-container button:hover {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.note-icon-container .dropdown-menu {
|
||||
border-radius: 10px;
|
||||
border-width: 2px;
|
||||
box-shadow: 10px 10px 93px -25px black;
|
||||
padding: 10px 15px 10px 15px !important;
|
||||
}
|
||||
|
||||
.note-icon-container .filter-row {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-right: 20px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.note-icon-container .filter-row span {
|
||||
display: block;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.note-icon-container .icon-list {
|
||||
height: 500px;
|
||||
overflow: auto;
|
||||
font-size: 180%;
|
||||
}
|
||||
|
||||
.note-icon-container .icon-list span {
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.note-icon-container .icon-list span:hover {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Change note icon"></button>
|
||||
<div class="dropdown-menu" aria-labelledby="note-path-list-button" style="width: 610px;">
|
||||
<div class="filter-row">
|
||||
<span>Category:</span> <select name="icon-category" class="form-control"></select>
|
||||
|
||||
<span>Search:</span> <input type="text" name="icon-search" class="form-control" />
|
||||
</div>
|
||||
|
||||
<div class="icon-list"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
let icons = [];
|
||||
let categories = [];
|
||||
|
||||
export default class NoteIconWidget extends TabAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
this.$icon = this.$widget.find('button');
|
||||
this.$iconList = this.$widget.find('.icon-list');
|
||||
this.$iconList.on('click', 'span', async e => {
|
||||
const clazz = $(e.target).attr('class');
|
||||
|
||||
await attributeService.setLabel(this.noteId,
|
||||
this.note.hasOwnedLabel('workspace') ? 'workspaceIconClass' : 'iconClass',
|
||||
clazz
|
||||
);
|
||||
});
|
||||
|
||||
this.$iconCategory = this.$widget.find("select[name='icon-category']");
|
||||
this.$iconCategory.on('change', () => this.renderFilteredDropdown());
|
||||
this.$iconCategory.on('click', e => e.stopPropagation());
|
||||
|
||||
this.$iconSearch = this.$widget.find("input[name='icon-search']");
|
||||
this.$iconSearch.on('input', () => this.renderFilteredDropdown());
|
||||
|
||||
this.$notePathList = this.$widget.find(".note-path-list");
|
||||
this.$widget.on('show.bs.dropdown', async () => {
|
||||
const {categories} = (await import('./icon_list.js')).default;
|
||||
|
||||
this.$iconCategory.empty();
|
||||
|
||||
for (const category of categories) {
|
||||
this.$iconCategory.append(
|
||||
$("<option>")
|
||||
.text(category.name)
|
||||
.attr("value", category.id)
|
||||
);
|
||||
}
|
||||
|
||||
this.$iconSearch.val('');
|
||||
|
||||
this.renderDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$icon.removeClass().addClass(note.getIcon());
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
for (const attr of loadResults.getAttributes()) {
|
||||
if (attr.type === 'label'
|
||||
&& ['iconClass', 'workspaceIconClass'].includes(attr.name)
|
||||
&& attr.isAffecting(this.note)) {
|
||||
|
||||
this.refresh();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderFilteredDropdown() {
|
||||
const categoryId = parseInt(this.$iconCategory.find('option:selected').val());
|
||||
const search = this.$iconSearch.val();
|
||||
|
||||
this.renderDropdown(categoryId, search);
|
||||
}
|
||||
|
||||
async renderDropdown(categoryId, search) {
|
||||
this.$iconList.empty();
|
||||
|
||||
const {icons} = (await import('./icon_list.js')).default;
|
||||
|
||||
for (const icon of icons) {
|
||||
if (categoryId && icon.category_id !== categoryId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (search && search.trim() && !icon.name.includes(search.trim().toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.$iconList.append(
|
||||
$('<span>')
|
||||
.addClass(this.getIconClass(icon))
|
||||
.attr("title", icon.name)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getIconClass(icon) {
|
||||
if (icon.type_of_icon === 'LOGO') {
|
||||
return "bx bxl-" + icon.name;
|
||||
}
|
||||
else if (icon.type_of_icon === 'SOLID') {
|
||||
return "bx bxs-" + icon.name;
|
||||
}
|
||||
else {
|
||||
return "bx bx-" + icon.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import TabAwareWidget from "./tab_aware_widget.js";
|
||||
import NoteListRenderer from "../services/note_list_renderer.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-list-widget">
|
||||
<style>
|
||||
.note-list-widget {
|
||||
flex-grow: 100000;
|
||||
flex-shrink: 100000;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.note-list-widget .note-list {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="note-list-widget-content">
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class NoteListWidget extends TabAwareWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
|
||||
&& (
|
||||
['book', 'search', 'code'].includes(this.note.type)
|
||||
|| (this.note.type === 'text' && this.note.hasChildren())
|
||||
)
|
||||
&& !this.note.hasLabel('hideChildrenOverview');
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$content = this.$widget.find('.note-list-widget-content');
|
||||
this.contentSized();
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
this.isIntersecting = entries[0].isIntersecting;
|
||||
|
||||
this.checkRenderStatus();
|
||||
}, {
|
||||
rootMargin: '50px',
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
observer.observe(this.$widget[0]);
|
||||
}
|
||||
|
||||
checkRenderStatus() {
|
||||
// console.log("this.isIntersecting", this.isIntersecting);
|
||||
// console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
|
||||
// console.log("this.shownNoteId !== this.noteId", this.shownNoteId !== this.noteId);
|
||||
|
||||
if (this.isIntersecting
|
||||
&& this.noteIdRefreshed === this.noteId
|
||||
&& this.shownNoteId !== this.noteId) {
|
||||
|
||||
this.shownNoteId = this.noteId;
|
||||
this.renderNoteList(this.note);
|
||||
}
|
||||
}
|
||||
|
||||
async renderNoteList(note) {
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
|
||||
await noteListRenderer.renderList();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.$content.empty();
|
||||
this.shownNoteId = null;
|
||||
|
||||
await super.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* We have this event so that we evaluate intersection only after note detail is loaded.
|
||||
* If it's evaluated before note detail then it's clearly intersected (visible) although after note detail load
|
||||
* it is not intersected (visible) anymore.
|
||||
*/
|
||||
noteDetailRefreshedEvent({tabId}) {
|
||||
if (!this.isTab(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.noteIdRefreshed = this.noteId;
|
||||
|
||||
setTimeout(() => this.checkRenderStatus(), 100);
|
||||
}
|
||||
|
||||
searchRefreshedEvent({tabId}) {
|
||||
if (!this.isTab(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.noteIdRefreshed = this.noteId;
|
||||
this.shownNoteId = null;
|
||||
|
||||
this.checkRenderStatus();
|
||||
}
|
||||
|
||||
notesReloadedEvent({noteIds}) {
|
||||
if (noteIds.includes(this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,52 +4,25 @@ import linkService from "../services/link.js";
|
||||
import hoistedNoteService from "../services/hoisted_note.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-paths-widget">
|
||||
<div class="dropdown note-paths-widget">
|
||||
<style>
|
||||
.note-paths-widget {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
padding: 5px 10px 5px 10px;
|
||||
}
|
||||
|
||||
.note-path-list a.current {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.note-path-list-button {
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 5px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
font-size: 120% !important;
|
||||
}
|
||||
|
||||
.note-path-list-button::after {
|
||||
display: none !important; // disabling the standard caret
|
||||
}
|
||||
|
||||
.current-path {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.note-path-list {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="current-path"></div>
|
||||
|
||||
<div class="dropdown hide-in-zen-mode">
|
||||
<button class="btn btn-sm dropdown-toggle note-path-list-button bx bx-caret-down" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
|
||||
<ul class="note-path-list dropdown-menu dropdown-menu-right" aria-labelledby="note-path-list-button">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="btn dropdown-toggle note-path-list-button bx bx-collection" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Note paths"></button>
|
||||
<ul class="note-path-list dropdown-menu dropdown-menu-right" aria-labelledby="note-path-list-button">
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
export default class NotePathsWidget extends TabAwareWidget {
|
||||
@@ -57,52 +30,8 @@ export default class NotePathsWidget extends TabAwareWidget {
|
||||
this.$widget = $(TPL);
|
||||
this.overflowing();
|
||||
|
||||
this.$currentPath = this.$widget.find('.current-path');
|
||||
this.$dropdown = this.$widget.find(".dropdown");
|
||||
this.$dropdownToggle = this.$widget.find('.dropdown-toggle');
|
||||
|
||||
this.$notePathList = this.$dropdown.find(".note-path-list");
|
||||
|
||||
this.$dropdown.on('show.bs.dropdown', () => this.renderDropdown());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const noteIdsPath = treeService.parseNotePath(this.notePath);
|
||||
|
||||
this.$currentPath.empty();
|
||||
|
||||
let parentNoteId = 'root';
|
||||
let curPath = '';
|
||||
|
||||
let passedHoistedNote = false;
|
||||
|
||||
for (let i = 0; i < noteIdsPath.length; i++) {
|
||||
const noteId = noteIdsPath[i];
|
||||
|
||||
curPath += (curPath ? '/' : '') + noteId;
|
||||
|
||||
if (noteId === hoistedNoteService.getHoistedNoteId()) {
|
||||
passedHoistedNote = true;
|
||||
}
|
||||
|
||||
if (passedHoistedNote && (noteId !== hoistedNoteService.getHoistedNoteId() || noteIdsPath.length - i < 3)) {
|
||||
this.$currentPath.append(
|
||||
$("<a>")
|
||||
.attr('href', '#' + curPath)
|
||||
.attr('data-note-path', curPath)
|
||||
.addClass('no-tooltip-preview')
|
||||
.text(await treeService.getNoteTitle(noteId, parentNoteId))
|
||||
);
|
||||
|
||||
if (i !== noteIdsPath.length - 1) {
|
||||
this.$currentPath.append(' / ');
|
||||
}
|
||||
}
|
||||
|
||||
parentNoteId = noteId;
|
||||
}
|
||||
|
||||
this.$dropdownToggle.dropdown('hide');
|
||||
this.$notePathList = this.$widget.find(".note-path-list");
|
||||
this.$widget.on('show.bs.dropdown', () => this.renderDropdown());
|
||||
}
|
||||
|
||||
async renderDropdown() {
|
||||
|
||||
@@ -9,11 +9,10 @@ const TPL = `
|
||||
<style>
|
||||
.note-title-container {
|
||||
flex-grow: 100;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.note-title-container input.note-title {
|
||||
font-size: 150%;
|
||||
font-size: 180%;
|
||||
border: 0;
|
||||
min-width: 5em;
|
||||
width: 100%;
|
||||
@@ -24,7 +23,7 @@ const TPL = `
|
||||
}
|
||||
</style>
|
||||
|
||||
<input autocomplete="off" value="" class="note-title" tabindex="100">
|
||||
<input autocomplete="off" value="" placeholder="type note's title here..." class="note-title" tabindex="100">
|
||||
</div>`;
|
||||
|
||||
export default class NoteTitleWidget extends TabAwareWidget {
|
||||
@@ -42,6 +41,7 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
this.$noteTitle = this.$widget.find(".note-title");
|
||||
|
||||
this.$noteTitle.on('input', () => this.spacedUpdate.scheduleUpdate());
|
||||
@@ -94,6 +94,10 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
||||
// not updating the title specifically since the synced title might be older than what the user is currently typing
|
||||
this.setProtectedStatus(this.note);
|
||||
}
|
||||
|
||||
if (loadResults.isNoteReloaded(this.noteId, this.componentId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
beforeUnloadEvent() {
|
||||
|
||||
+166
-116
@@ -34,24 +34,26 @@ const TPL = `
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding-bottom: 35px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.collapse-tree-button {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 80px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.refresh-search-button {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
border: 1px solid transparent;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.refresh-search-button:hover {
|
||||
border-color: var(--button-border-color);
|
||||
.scroll-to-active-note-button {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 45px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tree-settings-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -64,7 +66,7 @@ const TPL = `
|
||||
padding: 20px;
|
||||
z-index: 1000;
|
||||
width: 320px;
|
||||
border-radius: 10px 0 10px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
ul.fancytree-container {
|
||||
@@ -128,8 +130,33 @@ const TPL = `
|
||||
span.fancytree-node.archived {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tree-item-button {
|
||||
font-size: 120%;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--main-background-color);
|
||||
margin-left: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.tree-item-button:hover {
|
||||
border-color: var(--main-border-color);
|
||||
}
|
||||
|
||||
.add-note-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span.fancytree-node:hover .add-note-button {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class="btn btn-sm icon-button bx bx-layer-minus collapse-tree-button" title="Collapse note tree" data-trigger-command="collapseTree"></button>
|
||||
|
||||
<button class="btn btn-sm icon-button bx bx-crosshair scroll-to-active-note-button" title="Scroll to active note" data-trigger-command="scrollToActiveNote"></button>
|
||||
|
||||
<button class="btn btn-sm icon-button bx bx-cog tree-settings-button" title="Tree settings"></button>
|
||||
|
||||
<div class="tree-settings-popup">
|
||||
@@ -159,6 +186,8 @@ const TPL = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
const MAX_SEARCH_RESULTS_IN_TREE = 100;
|
||||
|
||||
export default class NoteTreeWidget extends TabAwareWidget {
|
||||
constructor(treeName) {
|
||||
super();
|
||||
@@ -170,8 +199,21 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
this.$widget = $(TPL);
|
||||
this.$tree = this.$widget.find('.tree');
|
||||
|
||||
this.$tree.on("click", ".unhoist-button", hoistedNoteService.unhoist);
|
||||
this.$tree.on("click", ".refresh-search-button", () => this.refreshSearch());
|
||||
this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist());
|
||||
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
|
||||
this.$tree.on("mousedown", ".add-note-button", e => {
|
||||
const node = $.ui.fancytree.getNode(e);
|
||||
|
||||
noteCreateService.createNote(node.data.noteId, {
|
||||
isProtected: node.data.isProtected
|
||||
});
|
||||
});
|
||||
|
||||
this.$tree.on("mousedown", ".enter-workspace-button", e => {
|
||||
const node = $.ui.fancytree.getNode(e);
|
||||
|
||||
this.triggerCommand('hoistNote', {noteId: node.data.noteId});
|
||||
});
|
||||
|
||||
// fancytree doesn't support middle click so this is a way to support it
|
||||
this.$tree.on('mousedown', '.fancytree-title', e => {
|
||||
@@ -181,7 +223,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
const notePath = treeService.getNotePath(node);
|
||||
|
||||
if (notePath) {
|
||||
appContext.tabManager.openTabWithNote(notePath);
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
@@ -205,7 +247,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
let top = this.$treeSettingsButton[0].offsetTop;
|
||||
let left = this.$treeSettingsButton[0].offsetLeft;
|
||||
top += this.$treeSettingsButton.outerHeight();
|
||||
top -= this.$treeSettingsPopup.outerHeight() + 10;
|
||||
left += this.$treeSettingsButton.outerWidth() - this.$treeSettingsPopup.outerWidth();
|
||||
|
||||
if (left < 0) {
|
||||
@@ -290,7 +332,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
this.$tree.fancytree({
|
||||
titlesTabbable: true,
|
||||
keyboard: true,
|
||||
extensions: ["dnd5", "clones"],
|
||||
extensions: ["dnd5", "clones", "filter"],
|
||||
source: treeData,
|
||||
scrollOfs: {
|
||||
top: 100,
|
||||
@@ -311,7 +353,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
else if (event.ctrlKey) {
|
||||
const notePath = treeService.getNotePath(node);
|
||||
appContext.tabManager.openTabWithNote(notePath);
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
||||
}
|
||||
else if (data.node.isActive()) {
|
||||
// this is important for single column mobile view, otherwise it's not possible to see again previously displayed note
|
||||
@@ -336,13 +378,19 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
await activeTabContext.setNote(notePath);
|
||||
|
||||
if (utils.isMobile()) {
|
||||
this.triggerCommand('setActiveScreen', {screen:'detail'});
|
||||
this.triggerCommand('setActiveScreen', {screen: 'detail'});
|
||||
}
|
||||
},
|
||||
expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
|
||||
collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
|
||||
filter: {
|
||||
counter: false,
|
||||
mode: "hide",
|
||||
autoExpand: true
|
||||
},
|
||||
dnd5: {
|
||||
autoExpandMS: 600,
|
||||
preventLazyParents: false,
|
||||
dragStart: (node, data) => {
|
||||
// don't allow dragging root node
|
||||
if (node.data.noteId === hoistedNoteService.getHoistedNoteId()
|
||||
@@ -430,9 +478,19 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
data.result = treeCache.reloadNotes([noteId]).then(() => {
|
||||
const note = treeCache.getNoteFromCache(noteId);
|
||||
const note = treeCache.getNoteFromCache(noteId);
|
||||
|
||||
return this.prepareChildren(note);
|
||||
let childNoteIds = note.getChildNoteIds();
|
||||
|
||||
if (childNoteIds.length > MAX_SEARCH_RESULTS_IN_TREE) {
|
||||
childNoteIds = childNoteIds.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
||||
}
|
||||
|
||||
return treeCache.getNotes(childNoteIds);
|
||||
}).then(() => {
|
||||
const note = treeCache.getNoteFromCache(noteId);
|
||||
|
||||
return this.prepareChildren(note);
|
||||
});
|
||||
}
|
||||
else {
|
||||
@@ -444,29 +502,46 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
},
|
||||
enhanceTitle: async function (event, data) {
|
||||
const node = data.node;
|
||||
const $span = $(node.span);
|
||||
|
||||
if (node.data.noteId !== 'root'
|
||||
&& node.data.noteId === hoistedNoteService.getHoistedNoteId()
|
||||
&& $span.find('.unhoist-button').length === 0) {
|
||||
|
||||
const action = await keyboardActionsService.getAction('unhoist');
|
||||
let shortcuts = action.effectiveShortcuts.join(',');
|
||||
shortcuts = shortcuts ? `(${shortcuts})` : '';
|
||||
|
||||
const unhoistButton = $(`<span class="unhoist-button-wrapper" title="Unhoist current note to show the whole note tree ${shortcuts}">[<a class="unhoist-button">unhoist</a>]</span>`);
|
||||
|
||||
// prepending since appending could push out (when note title is too long)
|
||||
// the button too much to the right so that it's not visible
|
||||
$span.prepend(unhoistButton);
|
||||
if (!node.data.noteId) {
|
||||
// if there's "non-note" node, then don't enhance
|
||||
// this can happen for e.g. "Load error!" node
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await treeCache.getNote(node.data.noteId);
|
||||
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
if (note.type === 'search' && $span.find('.refresh-search-button').length === 0) {
|
||||
const refreshSearchButton = $('<span> <span class="refresh-search-button bx bx-refresh" title="Refresh saved search results"></span></span>');
|
||||
const $span = $(node.span);
|
||||
|
||||
$span.append(refreshSearchButton);
|
||||
$span.find('.tree-item-button').remove();
|
||||
|
||||
const isHoistedNote = activeTabContext && activeTabContext.hoistedNoteId === note.noteId && note.noteId !== 'root';
|
||||
|
||||
if (isHoistedNote) {
|
||||
const $unhoistButton = $('<span class="tree-item-button unhoist-button bx bx-door-open" title="Unhoist"></span>');
|
||||
|
||||
$unhoistButton.on('click', () => alert("bebe"));
|
||||
|
||||
$span.append($unhoistButton);
|
||||
}
|
||||
|
||||
if (note.hasLabel('workspace') && !isHoistedNote) {
|
||||
const $enterWorkspaceButton = $('<span class="tree-item-button enter-workspace-button bx bx-door-open" title="Hoist this note (workspace)"></span>');
|
||||
|
||||
$span.append($enterWorkspaceButton);
|
||||
}
|
||||
|
||||
if (note.type === 'search') {
|
||||
const $refreshSearchButton = $('<span class="tree-item-button refresh-search-button bx bx-refresh" title="Refresh saved search results"></span>');
|
||||
|
||||
$span.append($refreshSearchButton);
|
||||
}
|
||||
|
||||
if (note.type !== 'search') {
|
||||
const $createChildNoteButton = $('<span class="tree-item-button add-note-button bx bx-plus" title="Create child note"></span>');
|
||||
|
||||
$span.append($createChildNoteButton);
|
||||
}
|
||||
},
|
||||
// this is done to automatically lazy load all expanded notes after tree load
|
||||
@@ -510,19 +585,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
prepareRootNode() {
|
||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
let hoistedBranch;
|
||||
|
||||
if (hoistedNoteId === 'root') {
|
||||
hoistedBranch = treeCache.getBranch('root');
|
||||
}
|
||||
else {
|
||||
const hoistedNote = treeCache.getNoteFromCache(hoistedNoteId);
|
||||
hoistedBranch = hoistedNote.getBranches()[0];
|
||||
}
|
||||
|
||||
return this.prepareNode(hoistedBranch);
|
||||
return this.prepareNode(treeCache.getBranch('root'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -535,7 +598,13 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
const hideArchivedNotes = this.hideArchivedNotes;
|
||||
|
||||
for (const branch of this.getChildBranches(parentNote)) {
|
||||
let childBranches = parentNote.getFilteredChildBranches();
|
||||
|
||||
if (childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) {
|
||||
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
||||
}
|
||||
|
||||
for (const branch of childBranches) {
|
||||
if (hideArchivedNotes) {
|
||||
const note = branch.getNoteFromCache();
|
||||
|
||||
@@ -552,32 +621,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
return noteList;
|
||||
}
|
||||
|
||||
getIcon(note, isFolder) {
|
||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
if (note.noteId !== 'root' && note.noteId === hoistedNoteId) {
|
||||
return "bx bxs-arrow-from-bottom";
|
||||
}
|
||||
|
||||
return note.getIcon(isFolder);
|
||||
}
|
||||
|
||||
updateNode(node) {
|
||||
const note = treeCache.getNoteFromCache(node.data.noteId);
|
||||
const branch = treeCache.getBranch(node.data.branchId);
|
||||
|
||||
const isFolder = this.isFolder(note);
|
||||
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
|
||||
|
||||
node.data.isProtected = note.isProtected;
|
||||
node.data.noteType = note.type;
|
||||
node.folder = isFolder;
|
||||
node.icon = this.getIcon(note, isFolder);
|
||||
node.folder = note.isFolder();
|
||||
node.icon = note.getIcon();
|
||||
node.extraClasses = this.getExtraClasses(note);
|
||||
node.title = utils.escapeHtml(title);
|
||||
|
||||
if (node.isExpanded() !== branch.isExpanded) {
|
||||
node.setExpanded(branch.isExpanded, {noEvents: true});
|
||||
node.setExpanded(branch.isExpanded, {noEvents: true, noAnimation: true});
|
||||
}
|
||||
|
||||
node.renderTitle();
|
||||
@@ -594,9 +651,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
|
||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
const isFolder = this.isFolder(note);
|
||||
const isFolder = note.isFolder();
|
||||
|
||||
const node = {
|
||||
noteId: note.noteId,
|
||||
@@ -606,11 +662,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
noteType: note.type,
|
||||
title: utils.escapeHtml(title),
|
||||
extraClasses: this.getExtraClasses(note),
|
||||
icon: this.getIcon(note, isFolder),
|
||||
icon: note.getIcon(isFolder),
|
||||
refKey: note.noteId,
|
||||
lazy: true,
|
||||
folder: isFolder,
|
||||
expanded: (branch.isExpanded || hoistedNoteId === note.noteId) && note.type !== 'search',
|
||||
expanded: branch.isExpanded && note.type !== 'search',
|
||||
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
||||
};
|
||||
|
||||
@@ -621,34 +677,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
return node;
|
||||
}
|
||||
|
||||
isFolder(note) {
|
||||
return note.type === 'search'
|
||||
|| this.getChildBranches(note).length > 0;
|
||||
}
|
||||
|
||||
getChildBranches(parentNote) {
|
||||
let childBranches = parentNote.getChildBranches();
|
||||
|
||||
if (!childBranches) {
|
||||
ws.logError(`No children for ${parentNote}. This shouldn't happen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hideIncludedImages) {
|
||||
const imageLinks = parentNote.getRelations('imageLink');
|
||||
|
||||
// image is already visible in the parent note so no need to display it separately in the book
|
||||
childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
|
||||
}
|
||||
|
||||
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
|
||||
// which would seriously slow down everything.
|
||||
// we check this flag only once user chooses to expand the parent. This has the negative consequence that
|
||||
// note may appear as folder but not contain any children when all of them are archived
|
||||
|
||||
return childBranches;
|
||||
}
|
||||
|
||||
getExtraClasses(note) {
|
||||
utils.assertArguments(note);
|
||||
|
||||
@@ -711,13 +739,14 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
const {branchIds} = await server.put(`branches/${node.data.branchId}/expanded-subtree/${isExpanded ? 1 : 0}`);
|
||||
|
||||
treeCache.getBranches(branchIds, true).forEach(branch => branch.isExpanded = isExpanded);
|
||||
treeCache.getBranches(branchIds, true)
|
||||
.forEach(branch => branch.isExpanded = !!isExpanded);
|
||||
|
||||
await this.batchUpdate(async () => {
|
||||
await node.load(true);
|
||||
|
||||
if (node.data.noteId !== 'root') { // root is always expanded
|
||||
await node.setExpanded(isExpanded, {noEvents: true});
|
||||
await node.setExpanded(isExpanded, {noEvents: true, noAnimation: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -730,6 +759,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
await this.setExpandedStatusForSubtree(node, false);
|
||||
}
|
||||
|
||||
collapseTreeCommand() { this.collapseTree(); }
|
||||
|
||||
/**
|
||||
* @return {FancytreeNode|null}
|
||||
*/
|
||||
@@ -752,7 +783,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
async scrollToActiveNoteEvent() {
|
||||
async scrollToActiveNoteCommand() {
|
||||
const activeContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
if (activeContext && activeContext.notePath) {
|
||||
@@ -798,7 +829,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
if (expand) {
|
||||
await parentNode.setExpanded(true);
|
||||
await parentNode.setExpanded(true, {noAnimation: true});
|
||||
|
||||
// although previous line should set the expanded status, it seems to happen asynchronously
|
||||
// so we need to make sure it is set properly before calling updateNode which uses this flag
|
||||
@@ -862,9 +893,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
|
||||
}
|
||||
|
||||
// must be event since it's triggered from outside the tree
|
||||
collapseTreeEvent() { this.collapseTree(); }
|
||||
|
||||
isEnabled() {
|
||||
return !!this.tabContext;
|
||||
}
|
||||
@@ -897,13 +925,15 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
newActiveNode.makeVisible({scrollIntoView: true});
|
||||
}
|
||||
}
|
||||
|
||||
this.filterHoistedBranch();
|
||||
}
|
||||
|
||||
async refreshSearch() {
|
||||
const activeNode = this.getActiveNode();
|
||||
async refreshSearch(e) {
|
||||
const activeNode = $.ui.fancytree.getNode(e);
|
||||
|
||||
activeNode.load(true);
|
||||
activeNode.setExpanded(true);
|
||||
activeNode.setExpanded(true, {noAnimation: true});
|
||||
|
||||
toastService.showMessage("Saved search note refreshed.");
|
||||
}
|
||||
@@ -971,7 +1001,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
const noteIdsToReload = new Set();
|
||||
|
||||
for (const attr of loadResults.getAttributes()) {
|
||||
if (attr.type === 'label' && ['iconClass', 'cssClass'].includes(attr.name)) {
|
||||
if (attr.type === 'label' && ['iconClass', 'cssClass', 'workspace', 'workspaceIconClass', 'archived'].includes(attr.name)) {
|
||||
if (attr.isInheritable) {
|
||||
noteIdsToReload.add(attr.noteId);
|
||||
}
|
||||
@@ -1102,6 +1132,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
await newActiveNode.setFocus(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (noteIdsToReload.size > 0 || noteIdsToUpdate.size > 0) {
|
||||
// workaround for https://github.com/mar10/fancytree/issues/1054
|
||||
this.filterHoistedBranch();
|
||||
}
|
||||
}
|
||||
|
||||
sortChildren(node) {
|
||||
@@ -1117,7 +1152,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
});
|
||||
}
|
||||
|
||||
async setExpanded(branchId, isExpanded) {
|
||||
setExpanded(branchId, isExpanded) {
|
||||
utils.assertArguments(branchId);
|
||||
|
||||
const branch = treeCache.getBranch(branchId, true);
|
||||
@@ -1135,7 +1170,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
branch.isExpanded = isExpanded;
|
||||
|
||||
await server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`);
|
||||
server.put(`branches/${branchId}/expanded/${isExpanded ? 1 : 0}`);
|
||||
}
|
||||
|
||||
async reloadTreeFromCache() {
|
||||
@@ -1156,8 +1191,21 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
hoistedNoteChangedEvent() {
|
||||
this.reloadTreeFromCache();
|
||||
async hoistedNoteChangedEvent({tabId}) {
|
||||
if (this.isTab(tabId)) {
|
||||
this.filterHoistedBranch();
|
||||
}
|
||||
}
|
||||
|
||||
filterHoistedBranch() {
|
||||
if (this.tabContext) {
|
||||
if (this.tabContext.hoistedNoteId === 'root') {
|
||||
this.tree.clearFilter();
|
||||
}
|
||||
else {
|
||||
this.tree.filterBranches(node => node.data.noteId === this.tabContext.hoistedNoteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
treeCacheReloadedEvent() {
|
||||
@@ -1171,7 +1219,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
for (const action of actions) {
|
||||
for (const shortcut of action.effectiveShortcuts) {
|
||||
hotKeyMap[utils.normalizeShortcut(shortcut)] = node => {
|
||||
this.triggerCommand(action.actionName, {node});
|
||||
const notePath = treeService.getNotePath(node);
|
||||
|
||||
this.triggerCommand(action.actionName, {node, notePath});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user