mirror of
https://github.com/zadam/trilium.git
synced 2025-11-03 11:56:01 +01:00
Compare commits
60 Commits
v0.46.1-be
...
v0.46.4-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12b468d3dc | ||
|
|
6f901e6852 | ||
|
|
09e9ac4d00 | ||
|
|
a33ac65fdf | ||
|
|
f8fb071a6f | ||
|
|
a654078e56 | ||
|
|
fba68681aa | ||
|
|
50c84e0f5f | ||
|
|
f27370d44f | ||
|
|
c6c9202c00 | ||
|
|
2cafda5f66 | ||
|
|
873953cbaf | ||
|
|
d51744ce19 | ||
|
|
7df8c940b6 | ||
|
|
9bac2a4819 | ||
|
|
88147f7a0a | ||
|
|
ca77211b38 | ||
|
|
4606e8d118 | ||
|
|
9f002fa802 | ||
|
|
060d4fc27b | ||
|
|
bf0fbe201e | ||
|
|
721e5da672 | ||
|
|
8192b51b8a | ||
|
|
73514a63d8 | ||
|
|
f8c310eb8f | ||
|
|
b9422b0efd | ||
|
|
14ced949a9 | ||
|
|
5b5c2a2dbb | ||
|
|
2c958eaacb | ||
|
|
4aa27b6033 | ||
|
|
89a0c5a1c9 | ||
|
|
78e48095e6 | ||
|
|
02016ed031 | ||
|
|
cb91dadeca | ||
|
|
2c755bcc38 | ||
|
|
3c7a6bc1e4 | ||
|
|
3fe87259e2 | ||
|
|
d476dfc53b | ||
|
|
19821b634f | ||
|
|
cde41b268e | ||
|
|
1c59bc4d3c | ||
|
|
cb6d35236c | ||
|
|
7572ee284b | ||
|
|
d0eaf623a8 | ||
|
|
25c2db6c3a | ||
|
|
5a173ff14e | ||
|
|
7fab75b085 | ||
|
|
0f065536d0 | ||
|
|
d0747abded | ||
|
|
f62b4a581e | ||
|
|
dcf1c62ec1 | ||
|
|
93d55b3e7b | ||
|
|
90c6852423 | ||
|
|
208baa56e9 | ||
|
|
ddf8438b22 | ||
|
|
6008dc891f | ||
|
|
cc887a00f2 | ||
|
|
fbbd51d0b1 | ||
|
|
081b8b126a | ||
|
|
7a6bb81345 |
@@ -55,3 +55,7 @@ npm run start-server
|
|||||||
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
|
* [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
|
* [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)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|||||||
945
package-lock.json
generated
945
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"productName": "Trilium Notes",
|
"productName": "Trilium Notes",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.46.1-beta",
|
"version": "0.46.4-beta",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "electron.js",
|
"main": "electron.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"test-all": "npm run test && npm run test-es6"
|
"test-all": "npm run test && npm run test-es6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-mutex": "0.3.0",
|
"async-mutex": "0.3.1",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.1",
|
||||||
"better-sqlite3": "7.1.2",
|
"better-sqlite3": "7.1.2",
|
||||||
"body-parser": "1.19.0",
|
"body-parser": "1.19.0",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"dayjs": "1.10.4",
|
"dayjs": "1.10.4",
|
||||||
"ejs": "3.1.6",
|
"ejs": "3.1.6",
|
||||||
"electron-debug": "3.2.0",
|
"electron-debug": "3.2.0",
|
||||||
"electron-dl": "3.1.0",
|
"electron-dl": "3.2.0",
|
||||||
"electron-find": "1.0.6",
|
"electron-find": "1.0.6",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
@@ -51,10 +51,10 @@
|
|||||||
"is-animated": "^2.0.1",
|
"is-animated": "^2.0.1",
|
||||||
"is-svg": "4.2.1",
|
"is-svg": "4.2.1",
|
||||||
"jimp": "0.16.1",
|
"jimp": "0.16.1",
|
||||||
"jsdom": "^16.4.0",
|
"jsdom": "16.5.0",
|
||||||
"mime-types": "2.1.29",
|
"mime-types": "2.1.29",
|
||||||
"multer": "1.4.2",
|
"multer": "1.4.2",
|
||||||
"node-abi": "2.19.3",
|
"node-abi": "2.21.0",
|
||||||
"open": "7.4.2",
|
"open": "7.4.2",
|
||||||
"portscanner": "2.2.0",
|
"portscanner": "2.2.0",
|
||||||
"rand-token": "1.0.1",
|
"rand-token": "1.0.1",
|
||||||
@@ -70,16 +70,16 @@
|
|||||||
"striptags": "3.1.1",
|
"striptags": "3.1.1",
|
||||||
"tmp": "^0.2.1",
|
"tmp": "^0.2.1",
|
||||||
"turndown": "7.0.0",
|
"turndown": "7.0.0",
|
||||||
"turndown-plugin-gfm": "1.0.2",
|
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||||
"unescape": "1.0.1",
|
"unescape": "1.0.1",
|
||||||
"ws": "7.4.3",
|
"ws": "7.4.4",
|
||||||
"yauzl": "2.10.0",
|
"yauzl": "2.10.0",
|
||||||
"yazl": "2.5.1"
|
"yazl": "2.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"electron": "9.4.3",
|
"electron": "9.4.4",
|
||||||
"electron-builder": "22.9.1",
|
"electron-builder": "22.10.5",
|
||||||
"electron-packager": "15.2.0",
|
"electron-packager": "15.2.0",
|
||||||
"electron-rebuild": "2.3.5",
|
"electron-rebuild": "2.3.5",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"jsdoc": "3.6.6",
|
"jsdoc": "3.6.6",
|
||||||
"lorem-ipsum": "2.0.3",
|
"lorem-ipsum": "2.0.3",
|
||||||
"rcedit": "3.0.0",
|
"rcedit": "3.0.0",
|
||||||
"webpack": "5.23.0",
|
"webpack": "5.24.4",
|
||||||
"webpack-cli": "4.5.0"
|
"webpack-cli": "4.5.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUtcDateChanged() {
|
getUtcDateChanged() {
|
||||||
return this.utcDateModified;
|
return this.utcDateModified || this.utcDateCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
get repository() {
|
get repository() {
|
||||||
|
|||||||
@@ -681,7 +681,7 @@ class Note extends Entity {
|
|||||||
* Update's given relation's value or creates it if it doesn't exist
|
* Update's given relation's value or creates it if it doesn't exist
|
||||||
*
|
*
|
||||||
* @param {string} name - relation name
|
* @param {string} name - relation name
|
||||||
* @param {string} [value] - relation value (noteId)
|
* @param {string} value - relation value (noteId)
|
||||||
*/
|
*/
|
||||||
setRelation(name, value) { return this.setAttribute(RELATION, name, value); }
|
setRelation(name, value) { return this.setAttribute(RELATION, name, value); }
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ ws.subscribeToMessages(async message => {
|
|||||||
toastService.showPersistent(makeToast(message.taskId, "Export in progress: " + message.progressCount));
|
toastService.showPersistent(makeToast(message.taskId, "Export in progress: " + message.progressCount));
|
||||||
}
|
}
|
||||||
else if (message.type === 'task-succeeded') {
|
else if (message.type === 'task-succeeded') {
|
||||||
const toast = makeToast(message.taskId, "Import finished successfully.");
|
const toast = makeToast(message.taskId, "Export finished successfully.");
|
||||||
toast.closeAfter = 5000;
|
toast.closeAfter = 5000;
|
||||||
|
|
||||||
toastService.showPersistent(toast);
|
toastService.showPersistent(toast);
|
||||||
|
|||||||
@@ -29,7 +29,17 @@ const TPL = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Zooming can be controlled with CTRL-+ and CTRL-= shortcuts as well.</p>
|
<div class="form-group row">
|
||||||
|
<div class="col-4">
|
||||||
|
<label for="heading-style">Heading style</label>
|
||||||
|
<select class="form-control" id="heading-style">
|
||||||
|
<option value="plain">Plain</option>
|
||||||
|
<option value="markdown">Markdown-style</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Zooming can be controlled with CTRL+- and CTRL+= shortcuts as well.</p>
|
||||||
|
|
||||||
<h4>Font sizes</h4>
|
<h4>Font sizes</h4>
|
||||||
|
|
||||||
@@ -78,6 +88,7 @@ export default class ApperanceOptions {
|
|||||||
this.$themeSelect = $("#theme-select");
|
this.$themeSelect = $("#theme-select");
|
||||||
this.$zoomFactorSelect = $("#zoom-factor-select");
|
this.$zoomFactorSelect = $("#zoom-factor-select");
|
||||||
this.$nativeTitleBarSelect = $("#native-title-bar-select");
|
this.$nativeTitleBarSelect = $("#native-title-bar-select");
|
||||||
|
this.$headingStyle = $("#heading-style");
|
||||||
this.$mainFontSize = $("#main-font-size");
|
this.$mainFontSize = $("#main-font-size");
|
||||||
this.$treeFontSize = $("#tree-font-size");
|
this.$treeFontSize = $("#tree-font-size");
|
||||||
this.$detailFontSize = $("#detail-font-size");
|
this.$detailFontSize = $("#detail-font-size");
|
||||||
@@ -86,11 +97,7 @@ export default class ApperanceOptions {
|
|||||||
this.$themeSelect.on('change', () => {
|
this.$themeSelect.on('change', () => {
|
||||||
const newTheme = this.$themeSelect.val();
|
const newTheme = this.$themeSelect.val();
|
||||||
|
|
||||||
for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes
|
this.toggleBodyClass("theme-", newTheme);
|
||||||
if (clazz.startsWith("theme-")) {
|
|
||||||
this.$body.removeClass(clazz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const noteId = $(this).find(":selected").attr("data-note-id");
|
const noteId = $(this).find(":selected").attr("data-note-id");
|
||||||
|
|
||||||
@@ -100,8 +107,6 @@ export default class ApperanceOptions {
|
|||||||
libraryLoader.requireCss(`api/notes/download/${noteId}`);
|
libraryLoader.requireCss(`api/notes/download/${noteId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$body.addClass("theme-" + newTheme);
|
|
||||||
|
|
||||||
server.put('options/theme/' + newTheme);
|
server.put('options/theme/' + newTheme);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,6 +118,14 @@ export default class ApperanceOptions {
|
|||||||
server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible);
|
server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$headingStyle.on('change', () => {
|
||||||
|
const newHeadingStyle = this.$headingStyle.val();
|
||||||
|
|
||||||
|
this.toggleBodyClass("heading-style-", newHeadingStyle);
|
||||||
|
|
||||||
|
server.put('options/headingStyle/' + newHeadingStyle);
|
||||||
|
});
|
||||||
|
|
||||||
this.$mainFontSize.on('change', async () => {
|
this.$mainFontSize.on('change', async () => {
|
||||||
await server.put('options/mainFontSize/' + this.$mainFontSize.val());
|
await server.put('options/mainFontSize/' + this.$mainFontSize.val());
|
||||||
|
|
||||||
@@ -132,6 +145,16 @@ export default class ApperanceOptions {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleBodyClass(prefix, value) {
|
||||||
|
for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes
|
||||||
|
if (clazz.startsWith(prefix)) {
|
||||||
|
this.$body.removeClass(clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$body.addClass(prefix + value);
|
||||||
|
}
|
||||||
|
|
||||||
async optionsLoaded(options) {
|
async optionsLoaded(options) {
|
||||||
const themes = [
|
const themes = [
|
||||||
{ val: 'white', title: 'White' },
|
{ val: 'white', title: 'White' },
|
||||||
@@ -159,6 +182,8 @@ export default class ApperanceOptions {
|
|||||||
|
|
||||||
this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide');
|
this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide');
|
||||||
|
|
||||||
|
this.$headingStyle.val(options.headingStyle);
|
||||||
|
|
||||||
this.$mainFontSize.val(options.mainFontSize);
|
this.$mainFontSize.val(options.mainFontSize);
|
||||||
this.$treeFontSize.val(options.treeFontSize);
|
this.$treeFontSize.val(options.treeFontSize);
|
||||||
this.$detailFontSize.val(options.detailFontSize);
|
this.$detailFontSize.val(options.detailFontSize);
|
||||||
|
|||||||
24
src/public/app/dialogs/sort_child_notes.js
Normal file
24
src/public/app/dialogs/sort_child_notes.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import server from "../services/server.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
|
const $dialog = $("#sort-child-notes-dialog");
|
||||||
|
const $form = $("#sort-child-notes-form");
|
||||||
|
|
||||||
|
let parentNoteId = null;
|
||||||
|
|
||||||
|
$form.on('submit', async () => {
|
||||||
|
const sortBy = $form.find("input[name='sort-by']:checked").val();
|
||||||
|
const sortDirection = $form.find("input[name='sort-direction']:checked").val();
|
||||||
|
|
||||||
|
await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection});
|
||||||
|
|
||||||
|
utils.closeActiveDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function showDialog(noteId) {
|
||||||
|
parentNoteId = noteId;
|
||||||
|
|
||||||
|
utils.openDialog($dialog);
|
||||||
|
|
||||||
|
$form.find('input:first').focus();
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import server from '../services/server.js';
|
|||||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
import ws from "../services/ws.js";
|
import ws from "../services/ws.js";
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
|
import treeCache from "../services/tree_cache.js";
|
||||||
|
|
||||||
const LABEL = 'label';
|
const LABEL = 'label';
|
||||||
const RELATION = 'relation';
|
const RELATION = 'relation';
|
||||||
@@ -158,6 +159,26 @@ class NoteShort {
|
|||||||
return this.treeCache.getNotesFromCache(this.parents);
|
return this.treeCache.getNotesFromCache(this.parents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// will sort the parents so that non-search & non-archived are first and archived at the end
|
||||||
|
// this is done so that non-search & non-archived paths are always explored as first when looking for note path
|
||||||
|
resortParents() {
|
||||||
|
this.parents.sort((aNoteId, bNoteId) => {
|
||||||
|
const aBranchId = this.parentToBranch[aNoteId];
|
||||||
|
|
||||||
|
if (aBranchId && aBranchId.startsWith('virt-')) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aNote = this.treeCache.getNoteFromCache([aNoteId]);
|
||||||
|
|
||||||
|
if (aNote.hasLabel('archived')) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns {string[]} */
|
/** @returns {string[]} */
|
||||||
getChildNoteIds() {
|
getChildNoteIds() {
|
||||||
return this.children;
|
return this.children;
|
||||||
@@ -233,6 +254,72 @@ class NoteShort {
|
|||||||
return noteAttributeCache.attributes[this.noteId];
|
return noteAttributeCache.attributes[this.noteId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllNotePaths(encounteredNoteIds = null) {
|
||||||
|
if (this.noteId === 'root') {
|
||||||
|
return [['root']];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encounteredNoteIds) {
|
||||||
|
encounteredNoteIds = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
encounteredNoteIds.add(this.noteId);
|
||||||
|
|
||||||
|
const parentNotes = this.getParentNotes();
|
||||||
|
let paths;
|
||||||
|
|
||||||
|
if (parentNotes.length === 1) { // optimization for the most common case
|
||||||
|
if (encounteredNoteIds.has(parentNotes[0].noteId)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
paths = parentNotes[0].getAllNotePaths(encounteredNoteIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
paths = [];
|
||||||
|
|
||||||
|
for (const parentNote of parentNotes) {
|
||||||
|
if (encounteredNoteIds.has(parentNote.noteId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSet = new Set(encounteredNoteIds);
|
||||||
|
|
||||||
|
paths.push(...parentNote.getAllNotePaths(newSet));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
path.push(this.noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortedNotePaths(hoistedNotePath = 'root') {
|
||||||
|
const notePaths = this.getAllNotePaths().map(path => ({
|
||||||
|
notePath: path,
|
||||||
|
isInHoistedSubTree: path.includes(hoistedNotePath),
|
||||||
|
isArchived: path.find(noteId => treeCache.notes[noteId].hasLabel('archived')),
|
||||||
|
isSearch: path.find(noteId => treeCache.notes[noteId].type === 'search')
|
||||||
|
}));
|
||||||
|
|
||||||
|
notePaths.sort((a, b) => {
|
||||||
|
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
|
||||||
|
return a.isInHoistedSubTree ? -1 : 1;
|
||||||
|
} else if (a.isSearch !== b.isSearch) {
|
||||||
|
return a.isSearch ? 1 : -1;
|
||||||
|
} else if (a.isArchived !== b.isArchived) {
|
||||||
|
return a.isArchived ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return a.notePath.length - b.notePath.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return notePaths;
|
||||||
|
}
|
||||||
|
|
||||||
__filterAttrs(attributes, type, name) {
|
__filterAttrs(attributes, type, name) {
|
||||||
if (!type && !name) {
|
if (!type && !name) {
|
||||||
return attributes;
|
return attributes;
|
||||||
@@ -522,7 +609,7 @@ class NoteShort {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAncestor(ancestorNote, visitedNoteIds) {
|
hasAncestor(ancestorNote, visitedNoteIds = null) {
|
||||||
if (this.noteId === ancestorNote.noteId) {
|
if (this.noteId === ancestorNote.noteId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import keyboardActionsService from "./keyboard_actions.js";
|
|||||||
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
|
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
|
||||||
import MainTreeExecutors from "./main_tree_executors.js";
|
import MainTreeExecutors from "./main_tree_executors.js";
|
||||||
import protectedSessionHolder from "./protected_session_holder.js";
|
import protectedSessionHolder from "./protected_session_holder.js";
|
||||||
|
import toast from "./toast.js";
|
||||||
|
|
||||||
class AppContext extends Component {
|
class AppContext extends Component {
|
||||||
constructor(isMainWindow) {
|
constructor(isMainWindow) {
|
||||||
@@ -19,6 +20,7 @@ class AppContext extends Component {
|
|||||||
|
|
||||||
this.isMainWindow = isMainWindow;
|
this.isMainWindow = isMainWindow;
|
||||||
this.executors = [];
|
this.executors = [];
|
||||||
|
this.beforeUnloadListeners = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
setLayout(layout) {
|
setLayout(layout) {
|
||||||
@@ -104,6 +106,15 @@ class AppContext extends Component {
|
|||||||
getComponentByEl(el) {
|
getComponentByEl(el) {
|
||||||
return $(el).closest(".component").prop('component');
|
return $(el).closest(".component").prop('component');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addBeforeUnloadListener(obj) {
|
||||||
|
if (typeof WeakRef !== "function") {
|
||||||
|
// older browsers don't support WeakRef
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.beforeUnloadListeners.push(new WeakRef(obj));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const appContext = new AppContext(window.glob.isMainWindow);
|
const appContext = new AppContext(window.glob.isMainWindow);
|
||||||
@@ -112,7 +123,29 @@ const appContext = new AppContext(window.glob.isMainWindow);
|
|||||||
$(window).on('beforeunload', () => {
|
$(window).on('beforeunload', () => {
|
||||||
protectedSessionHolder.resetSessionCookie();
|
protectedSessionHolder.resetSessionCookie();
|
||||||
|
|
||||||
appContext.triggerEvent('beforeUnload');
|
let allSaved = true;
|
||||||
|
|
||||||
|
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());
|
||||||
|
|
||||||
|
for (const weakRef of appContext.beforeUnloadListeners) {
|
||||||
|
const component = weakRef.deref();
|
||||||
|
|
||||||
|
if (!component) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component.beforeUnloadEvent()) {
|
||||||
|
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||||
|
|
||||||
|
toast.showMessage("Please wait for a couple of seconds for the save to finish, then you can try again.", 10000);
|
||||||
|
|
||||||
|
allSaved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allSaved) {
|
||||||
|
return "some string";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function isNotePathInAddress() {
|
function isNotePathInAddress() {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ async function deleteNotes(branchIdsToDelete) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function moveNodeUpInHierarchy(node) {
|
async function moveNodeUpInHierarchy(node) {
|
||||||
if (hoistedNoteService.isRootNode(node)
|
if (hoistedNoteService.isHoistedNode(node)
|
||||||
|| hoistedNoteService.isTopLevelNode(node)
|
|| hoistedNoteService.isTopLevelNode(node)
|
||||||
|| node.getParent().data.noteType === 'search') {
|
|| node.getParent().data.noteType === 'search') {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ async function getTodayNote() {
|
|||||||
async function getDateNote(date) {
|
async function getDateNote(date) {
|
||||||
const note = await server.get('date-notes/date/' + date, "date-note");
|
const note = await server.get('date-notes/date/' + date, "date-note");
|
||||||
|
|
||||||
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
|
||||||
return await treeCache.getNote(note.noteId);
|
return await treeCache.getNote(note.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ async function getDateNote(date) {
|
|||||||
async function getMonthNote(month) {
|
async function getMonthNote(month) {
|
||||||
const note = await server.get('date-notes/month/' + month, "date-note");
|
const note = await server.get('date-notes/month/' + month, "date-note");
|
||||||
|
|
||||||
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
|
||||||
return await treeCache.getNote(note.noteId);
|
return await treeCache.getNote(note.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +36,8 @@ async function getMonthNote(month) {
|
|||||||
async function getYearNote(year) {
|
async function getYearNote(year) {
|
||||||
const note = await server.get('date-notes/year/' + year, "date-note");
|
const note = await server.get('date-notes/year/' + year, "date-note");
|
||||||
|
|
||||||
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
|
||||||
return await treeCache.getNote(note.noteId);
|
return await treeCache.getNote(note.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,21 +45,14 @@ async function getYearNote(year) {
|
|||||||
async function createSqlConsole() {
|
async function createSqlConsole() {
|
||||||
const note = await server.post('sql-console');
|
const note = await server.post('sql-console');
|
||||||
|
|
||||||
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
|
||||||
return await treeCache.getNote(note.noteId);
|
return await treeCache.getNote(note.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return {NoteShort} */
|
/** @return {NoteShort} */
|
||||||
async function createSearchNote(opts = {}) {
|
async function createSearchNote(opts = {}) {
|
||||||
const note = await server.post('search-note');
|
const note = await server.post('search-note', opts);
|
||||||
|
|
||||||
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();
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ export default class Entrypoints extends Component {
|
|||||||
utils.reloadApp();
|
utils.reloadApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
|
|
||||||
|
|
||||||
async openInWindowCommand({notePath, hoistedNoteId}) {
|
async openInWindowCommand({notePath, hoistedNoteId}) {
|
||||||
if (!hoistedNoteId) {
|
if (!hoistedNoteId) {
|
||||||
hoistedNoteId = 'root';
|
hoistedNoteId = 'root';
|
||||||
|
|||||||
@@ -166,8 +166,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
|||||||
}, "script");
|
}, "script");
|
||||||
|
|
||||||
if (ret.success) {
|
if (ret.success) {
|
||||||
// wait until all the changes done in the script has been synced to frontend before continuing
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
await ws.waitForEntityChangeId(ret.maxEntityChangeId);
|
|
||||||
|
|
||||||
return ret.executionResult;
|
return ret.executionResult;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,19 +16,17 @@ async function unhoist() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isTopLevelNode(node) {
|
function isTopLevelNode(node) {
|
||||||
return isRootNode(node.getParent());
|
return isHoistedNode(node.getParent());
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRootNode(node) {
|
function isHoistedNode(node) {
|
||||||
// even though check for 'root' should not be necessary, we keep it just in case
|
// even though check for 'root' should not be necessary, we keep it just in case
|
||||||
return node.data.noteId === "root"
|
return node.data.noteId === "root"
|
||||||
|| node.data.noteId === getHoistedNoteId();
|
|| node.data.noteId === getHoistedNoteId();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkNoteAccess(notePath, tabContext) {
|
async function checkNoteAccess(notePath, tabContext) {
|
||||||
// notePath argument can contain only noteId which is not good when hoisted since
|
const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId);
|
||||||
// then we need to check the whole note path
|
|
||||||
const resolvedNotePath = await treeService.resolveNotePath(notePath);
|
|
||||||
|
|
||||||
if (!resolvedNotePath) {
|
if (!resolvedNotePath) {
|
||||||
console.log("Cannot activate " + notePath);
|
console.log("Cannot activate " + notePath);
|
||||||
@@ -37,7 +35,7 @@ async function checkNoteAccess(notePath, tabContext) {
|
|||||||
|
|
||||||
const hoistedNoteId = tabContext.hoistedNoteId;
|
const hoistedNoteId = tabContext.hoistedNoteId;
|
||||||
|
|
||||||
if (hoistedNoteId !== 'root' && !resolvedNotePath.includes(hoistedNoteId)) {
|
if (!resolvedNotePath.includes(hoistedNoteId)) {
|
||||||
const confirmDialog = await import('../dialogs/confirm.js');
|
const confirmDialog = await import('../dialogs/confirm.js');
|
||||||
|
|
||||||
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?")) {
|
if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?")) {
|
||||||
@@ -55,6 +53,6 @@ export default {
|
|||||||
getHoistedNoteId,
|
getHoistedNoteId,
|
||||||
unhoist,
|
unhoist,
|
||||||
isTopLevelNode,
|
isTopLevelNode,
|
||||||
isRootNode,
|
isHoistedNode,
|
||||||
checkNoteAccess
|
checkNoteAccess
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ export default class LinkMap {
|
|||||||
|
|
||||||
const $noteBox = $("<div>")
|
const $noteBox = $("<div>")
|
||||||
.addClass("note-box")
|
.addClass("note-box")
|
||||||
.prop("id", noteBoxId);
|
.prop("id", noteBoxId)
|
||||||
|
.addClass(note.getCssClass());
|
||||||
|
|
||||||
const $link = $linkTitles[noteId];
|
const $link = $linkTitles[noteId];
|
||||||
|
|
||||||
|
|||||||
@@ -27,28 +27,28 @@ export default class MainTreeExecutors extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNoteIntoCommand() {
|
async createNoteIntoCommand() {
|
||||||
const activeNote = appContext.tabManager.getActiveTabNote();
|
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||||
|
|
||||||
if (!activeNote) {
|
if (!activeTabContext) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(activeNote.noteId, {
|
await noteCreateService.createNote(activeTabContext.notePath, {
|
||||||
isProtected: activeNote.isProtected,
|
isProtected: activeTabContext.note.isProtected,
|
||||||
saveSelection: false
|
saveSelection: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteAfterCommand() {
|
async createNoteAfterCommand() {
|
||||||
const node = this.tree.getActiveNode();
|
const node = this.tree.getActiveNode();
|
||||||
const parentNoteId = node.data.parentNoteId;
|
const parentNotePath = treeService.getNotePath(node.getParent());
|
||||||
const isProtected = await treeService.getParentProtectedStatus(node);
|
const isProtected = await treeService.getParentProtectedStatus(node);
|
||||||
|
|
||||||
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
|
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(parentNoteId, {
|
await noteCreateService.createNote(parentNotePath, {
|
||||||
target: 'after',
|
target: 'after',
|
||||||
targetBranchId: node.data.branchId,
|
targetBranchId: node.data.branchId,
|
||||||
isProtected: isProtected,
|
isProtected: isProtected,
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import hoistedNoteService from "./hoisted_note.js";
|
|
||||||
import appContext from "./app_context.js";
|
import appContext from "./app_context.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import protectedSessionHolder from "./protected_session_holder.js";
|
import protectedSessionHolder from "./protected_session_holder.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import ws from "./ws.js";
|
import ws from "./ws.js";
|
||||||
import treeCache from "./tree_cache.js";
|
import treeCache from "./tree_cache.js";
|
||||||
|
import treeService from "./tree.js";
|
||||||
import toastService from "./toast.js";
|
import toastService from "./toast.js";
|
||||||
|
|
||||||
async function createNewTopLevelNote() {
|
async function createNote(parentNotePath, options = {}) {
|
||||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
|
||||||
|
|
||||||
await createNote(hoistedNoteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createNote(parentNoteId, options = {}) {
|
|
||||||
options = Object.assign({
|
options = Object.assign({
|
||||||
activate: true,
|
activate: true,
|
||||||
focus: 'title',
|
focus: 'title',
|
||||||
@@ -36,6 +30,8 @@ async function createNote(parentNoteId, options = {}) {
|
|||||||
|
|
||||||
const newNoteName = options.title || "new note";
|
const newNoteName = options.title || "new note";
|
||||||
|
|
||||||
|
const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath);
|
||||||
|
|
||||||
const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId}`, {
|
const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId}`, {
|
||||||
title: newNoteName,
|
title: newNoteName,
|
||||||
content: options.content || "",
|
content: options.content || "",
|
||||||
@@ -53,7 +49,7 @@ async function createNote(parentNoteId, options = {}) {
|
|||||||
|
|
||||||
if (options.activate) {
|
if (options.activate) {
|
||||||
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||||
await activeTabContext.setNote(note.noteId);
|
await activeTabContext.setNote(`${parentNotePath}/${note.noteId}`);
|
||||||
|
|
||||||
if (options.focus === 'title') {
|
if (options.focus === 'title') {
|
||||||
appContext.triggerEvent('focusAndSelectTitle');
|
appContext.triggerEvent('focusAndSelectTitle');
|
||||||
@@ -88,12 +84,13 @@ function parseSelectedHtml(selectedHtml) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateSubtree(noteId, parentNoteId) {
|
async function duplicateSubtree(noteId, parentNotePath) {
|
||||||
|
const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath);
|
||||||
const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`);
|
const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`);
|
||||||
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
|
||||||
await appContext.tabManager.activateOrOpenNote(note.noteId);
|
await appContext.tabManager.activateOrOpenNote(`${parentNotePath}/${note.noteId}`);
|
||||||
|
|
||||||
const origNote = await treeCache.getNote(noteId);
|
const origNote = await treeCache.getNote(noteId);
|
||||||
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
|
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
|
||||||
@@ -101,6 +98,5 @@ async function duplicateSubtree(noteId, parentNoteId) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
createNote,
|
createNote,
|
||||||
createNewTopLevelNote,
|
|
||||||
duplicateSubtree
|
duplicateSubtree
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ const TPL = `
|
|||||||
|
|
||||||
.note-book-title {
|
.note-book-title {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* not-expanded title is limited to one line only */
|
||||||
|
.note-book-card:not(.expanded) .note-book-title {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-book-title .rendered-note-attributes {
|
.note-book-title .rendered-note-attributes {
|
||||||
@@ -148,7 +156,7 @@ class NoteListRenderer {
|
|||||||
/*
|
/*
|
||||||
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
||||||
*/
|
*/
|
||||||
constructor($parent, parentNote, noteIds) {
|
constructor($parent, parentNote, noteIds, showNotePath = false) {
|
||||||
this.$noteList = $(TPL);
|
this.$noteList = $(TPL);
|
||||||
|
|
||||||
// note list must be added to the DOM immediatelly, otherwise some functionality scripting (canvas) won't work
|
// note list must be added to the DOM immediatelly, otherwise some functionality scripting (canvas) won't work
|
||||||
@@ -200,6 +208,8 @@ class NoteListRenderer {
|
|||||||
|
|
||||||
await this.renderList();
|
await this.renderList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.showNotePath = showNotePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return {Set<string>} list of noteIds included (images, included notes) into a parent note and which
|
/** @return {Set<string>} list of noteIds included (images, included notes) into a parent note and which
|
||||||
@@ -298,7 +308,7 @@ class NoteListRenderer {
|
|||||||
.append(
|
.append(
|
||||||
$('<h5 class="note-book-title">')
|
$('<h5 class="note-book-title">')
|
||||||
.append($expander)
|
.append($expander)
|
||||||
.append(await linkService.createNoteLink(note.noteId, {showTooltip: false}))
|
.append(await linkService.createNoteLink(note.noteId, {showTooltip: false, showNotePath: this.showNotePath}))
|
||||||
.append($renderedAttributes)
|
.append($renderedAttributes)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,25 @@ export default class SpacedUpdate {
|
|||||||
|
|
||||||
async updateNowIfNecessary() {
|
async updateNowIfNecessary() {
|
||||||
if (this.changed) {
|
if (this.changed) {
|
||||||
this.changed = false;
|
this.changed = false; // optimistic...
|
||||||
|
|
||||||
|
try {
|
||||||
await this.updater();
|
await this.updater();
|
||||||
}
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.changed = true;
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllSavedAndTriggerUpdate() {
|
||||||
|
const allSaved = !this.changed;
|
||||||
|
|
||||||
|
this.updateNowIfNecessary();
|
||||||
|
|
||||||
|
return allSaved;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerUpdate() {
|
triggerUpdate() {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class TabContext extends Component {
|
|||||||
return inputNotePath;
|
return inputNotePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath);
|
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId);
|
||||||
|
|
||||||
if (!resolvedNotePath) {
|
if (!resolvedNotePath) {
|
||||||
logError(`Cannot resolve note path ${inputNotePath}`);
|
logError(`Cannot resolve note path ${inputNotePath}`);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export default class TabManager extends Component {
|
|||||||
openTabs: JSON.stringify(openTabs)
|
openTabs: JSON.stringify(openTabs)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appContext.addBeforeUnloadListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {TabContext[]} */
|
/** @type {TabContext[]} */
|
||||||
@@ -203,7 +205,7 @@ export default class TabManager extends Component {
|
|||||||
let hoistedNoteId = 'root';
|
let hoistedNoteId = 'root';
|
||||||
|
|
||||||
if (tabContext) {
|
if (tabContext) {
|
||||||
const resolvedNotePath = await treeService.resolveNotePath(notePath);
|
const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId);
|
||||||
|
|
||||||
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
|
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
|
||||||
hoistedNoteId = tabContext.hoistedNoteId;
|
hoistedNoteId = tabContext.hoistedNoteId;
|
||||||
@@ -329,6 +331,8 @@ export default class TabManager extends Component {
|
|||||||
|
|
||||||
beforeUnloadEvent() {
|
beforeUnloadEvent() {
|
||||||
this.tabsUpdate.updateNowIfNecessary();
|
this.tabsUpdate.updateNowIfNecessary();
|
||||||
|
|
||||||
|
return true; // don't block closing the tab, this metadata is not that important
|
||||||
}
|
}
|
||||||
|
|
||||||
openNewTabCommand() {
|
openNewTabCommand() {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import appContext from "./app_context.js";
|
|||||||
/**
|
/**
|
||||||
* @return {string|null}
|
* @return {string|null}
|
||||||
*/
|
*/
|
||||||
async function resolveNotePath(notePath) {
|
async function resolveNotePath(notePath, hoistedNoteId = 'root') {
|
||||||
const runPath = await resolveNotePathToSegments(notePath);
|
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||||
|
|
||||||
return runPath ? runPath.join("/") : null;
|
return runPath ? runPath.join("/") : null;
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ async function resolveNotePath(notePath) {
|
|||||||
*
|
*
|
||||||
* @return {string[]}
|
* @return {string[]}
|
||||||
*/
|
*/
|
||||||
async function resolveNotePathToSegments(notePath, logErrors = true) {
|
async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logErrors = true) {
|
||||||
utils.assertArguments(notePath);
|
utils.assertArguments(notePath);
|
||||||
|
|
||||||
// we might get notePath with the tabId suffix, remove it if present
|
// we might get notePath with the tabId suffix, remove it if present
|
||||||
@@ -37,7 +37,7 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
|
|||||||
path.push('root');
|
path.push('root');
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectivePath = [];
|
const effectivePathSegments = [];
|
||||||
let childNoteId = null;
|
let childNoteId = null;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
@@ -56,6 +56,8 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
child.resortParents();
|
||||||
|
|
||||||
const parents = child.getParentNotes();
|
const parents = child.getParentNotes();
|
||||||
|
|
||||||
if (!parents.length) {
|
if (!parents.length) {
|
||||||
@@ -73,13 +75,13 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
|
|||||||
console.log(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}`);
|
console.log(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const someNotePath = getSomeNotePath(parents[0]);
|
const someNotePath = getSomeNotePath(child, hoistedNoteId);
|
||||||
|
|
||||||
if (someNotePath) { // in case it's root the path may be empty
|
if (someNotePath) { // in case it's root the path may be empty
|
||||||
const pathToRoot = someNotePath.split("/").reverse();
|
const pathToRoot = someNotePath.split("/").reverse().slice(1);
|
||||||
|
|
||||||
for (const noteId of pathToRoot) {
|
for (const noteId of pathToRoot) {
|
||||||
effectivePath.push(noteId);
|
effectivePathSegments.push(noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,36 +89,37 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
effectivePath.push(parentNoteId);
|
effectivePathSegments.push(parentNoteId);
|
||||||
childNoteId = parentNoteId;
|
childNoteId = parentNoteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return effectivePath.reverse();
|
effectivePathSegments.reverse();
|
||||||
|
|
||||||
|
if (effectivePathSegments.includes(hoistedNoteId)) {
|
||||||
|
return effectivePathSegments;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const note = await treeCache.getNote(getNoteIdFromNotePath(notePath));
|
||||||
|
|
||||||
|
const someNotePathSegments = getSomeNotePathSegments(note, hoistedNoteId);
|
||||||
|
|
||||||
|
// if there isn't actually any note path with hoisted note then return the original resolved note path
|
||||||
|
return someNotePathSegments.includes(hoistedNoteId) ? someNotePathSegments : effectivePathSegments;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSomeNotePath(note) {
|
function getSomeNotePathSegments(note, hoistedNotePath = 'root') {
|
||||||
utils.assertArguments(note);
|
utils.assertArguments(note);
|
||||||
|
|
||||||
const path = [];
|
const notePaths = note.getSortedNotePaths(hoistedNotePath);
|
||||||
|
|
||||||
let cur = note;
|
return notePaths[0].notePath;
|
||||||
|
}
|
||||||
|
|
||||||
while (cur.noteId !== 'root') {
|
function getSomeNotePath(note, hoistedNotePath = 'root') {
|
||||||
path.push(cur.noteId);
|
const notePath = getSomeNotePathSegments(note, hoistedNotePath);
|
||||||
|
|
||||||
const parents = cur.getParentNotes().filter(note => note.type !== 'search');
|
return notePath.join('/');
|
||||||
|
|
||||||
if (!parents.length) {
|
|
||||||
logError(`Can't find parents for note ${cur.noteId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cur = parents[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
path.push('root');
|
|
||||||
|
|
||||||
return path.reverse().join('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sortAlphabetically(noteId) {
|
async function sortAlphabetically(noteId) {
|
||||||
@@ -136,7 +139,7 @@ ws.subscribeToMessages(message => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getParentProtectedStatus(node) {
|
function getParentProtectedStatus(node) {
|
||||||
return hoistedNoteService.isRootNode(node) ? 0 : node.getParent().data.isProtected;
|
return hoistedNoteService.isHoistedNode(node) ? 0 : node.getParent().data.isProtected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteIdFromNotePath(notePath) {
|
function getNoteIdFromNotePath(notePath) {
|
||||||
@@ -196,7 +199,7 @@ function getNotePath(node) {
|
|||||||
|
|
||||||
const path = [];
|
const path = [];
|
||||||
|
|
||||||
while (node && !hoistedNoteService.isRootNode(node)) {
|
while (node) {
|
||||||
if (node.data.noteId) {
|
if (node.data.noteId) {
|
||||||
path.push(node.data.noteId);
|
path.push(node.data.noteId);
|
||||||
}
|
}
|
||||||
@@ -204,10 +207,6 @@ function getNotePath(node) {
|
|||||||
node = node.getParent();
|
node = node.getParent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node) { // null node can happen directly after unhoisting when tree is still hoisted but option has been changed already
|
|
||||||
path.push(node.data.noteId); // root or hoisted noteId
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.reverse().join("/");
|
return path.reverse().join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,6 +310,7 @@ export default {
|
|||||||
resolveNotePath,
|
resolveNotePath,
|
||||||
resolveNotePathToSegments,
|
resolveNotePathToSegments,
|
||||||
getSomeNotePath,
|
getSomeNotePath,
|
||||||
|
getSomeNotePathSegments,
|
||||||
getParentProtectedStatus,
|
getParentProtectedStatus,
|
||||||
getNotePath,
|
getNotePath,
|
||||||
getNoteIdFromNotePath,
|
getNoteIdFromNotePath,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class TreeContextMenu {
|
|||||||
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes },
|
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes },
|
||||||
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes },
|
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes },
|
||||||
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
|
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
|
||||||
{ title: 'Sort alphabetically <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
||||||
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes }
|
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes }
|
||||||
] },
|
] },
|
||||||
{ title: "----" },
|
{ title: "----" },
|
||||||
@@ -112,10 +112,10 @@ class TreeContextMenu {
|
|||||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
||||||
}
|
}
|
||||||
else if (command === "insertNoteAfter") {
|
else if (command === "insertNoteAfter") {
|
||||||
const parentNoteId = this.node.data.parentNoteId;
|
const parentNotePath = treeService.getNotePath(this.node.getParent());
|
||||||
const isProtected = await treeService.getParentProtectedStatus(this.node);
|
const isProtected = await treeService.getParentProtectedStatus(this.node);
|
||||||
|
|
||||||
noteCreateService.createNote(parentNoteId, {
|
noteCreateService.createNote(parentNotePath, {
|
||||||
target: 'after',
|
target: 'after',
|
||||||
targetBranchId: this.node.data.branchId,
|
targetBranchId: this.node.data.branchId,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -123,14 +123,14 @@ class TreeContextMenu {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (command === "insertChildNote") {
|
else if (command === "insertChildNote") {
|
||||||
noteCreateService.createNote(noteId, {
|
const parentNotePath = treeService.getNotePath(this.node);
|
||||||
|
|
||||||
|
noteCreateService.createNote(parentNotePath, {
|
||||||
type: type,
|
type: type,
|
||||||
isProtected: this.node.data.isProtected
|
isProtected: this.node.data.isProtected
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("Triggering", command, notePath);
|
|
||||||
|
|
||||||
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
|
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,10 @@ function getNoteTypeClass(type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMimeTypeClass(mime) {
|
function getMimeTypeClass(mime) {
|
||||||
|
if (!mime) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
const semicolonIdx = mime.indexOf(';');
|
const semicolonIdx = mime.indexOf(';');
|
||||||
|
|
||||||
if (semicolonIdx !== -1) {
|
if (semicolonIdx !== -1) {
|
||||||
@@ -296,9 +300,13 @@ function dynamicRequire(moduleName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeLimit(promise, limitMs) {
|
function timeLimit(promise, limitMs, errorMessage) {
|
||||||
|
if (!promise || !promise.then) { // it's not actually a promise
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
// better stack trace if created outside of promise
|
// better stack trace if created outside of promise
|
||||||
const error = new Error('Process exceeded time limit ' + limitMs);
|
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
|
||||||
|
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ const processedEntityChangeIds = new Set();
|
|||||||
function logRows(entityChanges) {
|
function logRows(entityChanges) {
|
||||||
const filteredRows = entityChanges.filter(row =>
|
const filteredRows = entityChanges.filter(row =>
|
||||||
!processedEntityChangeIds.has(row.id)
|
!processedEntityChangeIds.has(row.id)
|
||||||
&& row.entityName !== 'recent_notes'
|
|
||||||
&& (row.entityName !== 'options' || row.entityId !== 'openTabs'));
|
&& (row.entityName !== 'options' || row.entityId !== 'openTabs'));
|
||||||
|
|
||||||
if (filteredRows.length > 0) {
|
if (filteredRows.length > 0) {
|
||||||
@@ -103,7 +102,7 @@ function waitForEntityChangeId(desiredEntityChangeId) {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("Waiting for", desiredEntityChangeId, 'current is', lastProcessedEntityChangeId);
|
console.debug(`Waiting for ${desiredEntityChangeId}, last processed is ${lastProcessedEntityChangeId}, last accepted ${lastAcceptedEntityChangeId}`);
|
||||||
|
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
entityChangeIdReachedListeners.push({
|
entityChangeIdReachedListeners.push({
|
||||||
@@ -127,7 +126,7 @@ function checkEntityChangeIdListeners() {
|
|||||||
.filter(l => l.desiredEntityChangeId > lastProcessedEntityChangeId);
|
.filter(l => l.desiredEntityChangeId > lastProcessedEntityChangeId);
|
||||||
|
|
||||||
entityChangeIdReachedListeners.filter(l => Date.now() > l.start - 60000)
|
entityChangeIdReachedListeners.filter(l => Date.now() > l.start - 60000)
|
||||||
.forEach(l => console.log(`Waiting for entityChangeId ${l.desiredEntityChangeId} while current is ${lastProcessedEntityChangeId} for ${Math.floor((Date.now() - l.start) / 1000)}s`));
|
.forEach(l => console.log(`Waiting for entityChangeId ${l.desiredEntityChangeId} while last processed is ${lastProcessedEntityChangeId} (last accepted ${lastAcceptedEntityChangeId}) for ${Math.floor((Date.now() - l.start) / 1000)}s`));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSafely(syncHandler, syncData) {
|
async function runSafely(syncHandler, syncData) {
|
||||||
@@ -230,25 +229,6 @@ subscribeToMessages(message => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function processEntityChanges(entityChanges) {
|
async function processEntityChanges(entityChanges) {
|
||||||
const missingNoteIds = [];
|
|
||||||
|
|
||||||
for (const {entityName, entity} of entityChanges) {
|
|
||||||
if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) {
|
|
||||||
missingNoteIds.push(entity.parentNoteId);
|
|
||||||
}
|
|
||||||
else if (entityName === 'attributes'
|
|
||||||
&& entity.type === 'relation'
|
|
||||||
&& entity.name === 'template'
|
|
||||||
&& !(entity.noteId in treeCache.notes)) {
|
|
||||||
|
|
||||||
missingNoteIds.push(entity.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingNoteIds.length > 0) {
|
|
||||||
await treeCache.reloadNotes(missingNoteIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadResults = new LoadResults(treeCache);
|
const loadResults = new LoadResults(treeCache);
|
||||||
|
|
||||||
for (const ec of entityChanges.filter(ec => ec.entityName === 'notes')) {
|
for (const ec of entityChanges.filter(ec => ec.entityName === 'notes')) {
|
||||||
@@ -391,6 +371,25 @@ async function processEntityChanges(entityChanges) {
|
|||||||
loadResults.addOption(ec.entity.name);
|
loadResults.addOption(ec.entity.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const missingNoteIds = [];
|
||||||
|
|
||||||
|
for (const {entityName, entity} of entityChanges) {
|
||||||
|
if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) {
|
||||||
|
missingNoteIds.push(entity.parentNoteId);
|
||||||
|
}
|
||||||
|
else if (entityName === 'attributes'
|
||||||
|
&& entity.type === 'relation'
|
||||||
|
&& entity.name === 'template'
|
||||||
|
&& !(entity.value in treeCache.notes)) {
|
||||||
|
|
||||||
|
missingNoteIds.push(entity.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingNoteIds.length > 0) {
|
||||||
|
await treeCache.reloadNotes(missingNoteIds);
|
||||||
|
}
|
||||||
|
|
||||||
if (!loadResults.isEmpty()) {
|
if (!loadResults.isEmpty()) {
|
||||||
if (loadResults.hasAttributeRelatedChanges()) {
|
if (loadResults.hasAttributeRelatedChanges()) {
|
||||||
noteAttributeCache.invalidate();
|
noteAttributeCache.invalidate();
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNoteForReferenceLink(title) {
|
async createNoteForReferenceLink(title) {
|
||||||
const {note} = await noteCreateService.createNote(this.noteId, {
|
const {note} = await noteCreateService.createNote(this.notePath, {
|
||||||
activate: false,
|
activate: false,
|
||||||
title: title
|
title: title
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ const TPL = `
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<div class="no-edited-notes-found">No edited notes on this day yet ...</div>
|
||||||
|
|
||||||
|
<div class="edited-notes-list"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -31,18 +35,20 @@ export default class EditedNotesWidget extends CollapsibleWidget {
|
|||||||
|
|
||||||
async doRenderBody() {
|
async doRenderBody() {
|
||||||
this.$body.html(TPL);
|
this.$body.html(TPL);
|
||||||
this.$editedNotes = this.$body.find('.edited-notes-widget');
|
this.$list = this.$body.find('.edited-notes-list');
|
||||||
|
this.$noneFound = this.$body.find('.no-edited-notes-found');
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshWithNote(note) {
|
async refreshWithNote(note) {
|
||||||
// remember which title was when we found the similar notes
|
|
||||||
this.title = note.title;
|
|
||||||
let editedNotes = await server.get('edited-notes/' + note.getLabelValue("dateNote"));
|
let editedNotes = await server.get('edited-notes/' + note.getLabelValue("dateNote"));
|
||||||
|
|
||||||
editedNotes = editedNotes.filter(n => n.noteId !== note.noteId);
|
editedNotes = editedNotes.filter(n => n.noteId !== note.noteId);
|
||||||
|
|
||||||
|
this.$list.empty();
|
||||||
|
this.$noneFound.hide();
|
||||||
|
|
||||||
if (editedNotes.length === 0) {
|
if (editedNotes.length === 0) {
|
||||||
this.$body.text("No edited notes on this day yet ...");
|
this.$noneFound.show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +56,6 @@ export default class EditedNotesWidget extends CollapsibleWidget {
|
|||||||
|
|
||||||
await treeCache.getNotes(noteIds, true); // preload all at once
|
await treeCache.getNotes(noteIds, true); // preload all at once
|
||||||
|
|
||||||
const $list = $('<div>'); // not using <ul> because it's difficult to style correctly with text-overflow
|
|
||||||
|
|
||||||
for (const editedNote of editedNotes) {
|
for (const editedNote of editedNotes) {
|
||||||
const $item = $('<div class="edited-note-line">');
|
const $item = $('<div class="edited-note-line">');
|
||||||
|
|
||||||
@@ -67,9 +71,7 @@ export default class EditedNotesWidget extends CollapsibleWidget {
|
|||||||
$item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title);
|
$item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
$list.append($item);
|
this.$list.append($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$editedNotes.empty().append($list);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,13 @@ export default class Component {
|
|||||||
console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`);
|
console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (glob.isDev) {
|
||||||
|
await utils.timeLimit(promise, 20000, `Time limit failed on ${this.constructor.name} with ${fun.name}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// cheaper and in non-dev the extra reporting is lost anyway through reload
|
||||||
await promise;
|
await promise;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class MobileDetailMenuWidget extends BasicWidget {
|
|||||||
],
|
],
|
||||||
selectMenuItemHandler: async ({command}) => {
|
selectMenuItemHandler: async ({command}) => {
|
||||||
if (command === "insertChildNote") {
|
if (command === "insertChildNote") {
|
||||||
noteCreateService.createNote(note.noteId);
|
noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath());
|
||||||
}
|
}
|
||||||
else if (command === "delete") {
|
else if (command === "delete") {
|
||||||
const notePath = appContext.tabManager.getActiveTabNotePath();
|
const notePath = appContext.tabManager.getActiveTabNotePath();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const WIDGET_TPL = `
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<a data-trigger-command="createTopLevelNote" title="Create new top level note" class="icon-action bx bx-folder-plus"></a>
|
<a data-trigger-command="createNoteIntoInbox" title="New 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="collapseTree" title="Collapse note tree" class="icon-action bx bx-layer-minus"></a>
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
await server.put('notes/' + noteId, dto, this.componentId);
|
await server.put('notes/' + noteId, dto, this.componentId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appContext.addBeforeUnloadListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
@@ -276,7 +278,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
const label = attrs.find(attr =>
|
const label = attrs.find(attr =>
|
||||||
attr.type === 'label'
|
attr.type === 'label'
|
||||||
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'bookZoomLevel'].includes(attr.name)
|
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'bookZoomLevel', 'displayRelations'].includes(attr.name)
|
||||||
&& attr.isAffecting(this.note));
|
&& attr.isAffecting(this.note));
|
||||||
|
|
||||||
const relation = attrs.find(attr =>
|
const relation = attrs.find(attr =>
|
||||||
@@ -293,7 +295,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeUnloadEvent() {
|
beforeUnloadEvent() {
|
||||||
this.spacedUpdate.updateNowIfNecessary();
|
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
textPreviewDisabledEvent({tabContext}) {
|
textPreviewDisabledEvent({tabContext}) {
|
||||||
@@ -316,7 +318,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// without await as this otherwise causes deadlock through component mutex
|
// without await as this otherwise causes deadlock through component mutex
|
||||||
noteCreateService.createNote(note.noteId, {
|
noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath(), {
|
||||||
isProtected: note.isProtected,
|
isProtected: note.isProtected,
|
||||||
saveSelection: true
|
saveSelection: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const TPL = `
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-icon-container button.note-icon:hover {
|
.note-icon-container button.note-icon:hover {
|
||||||
|
|||||||
@@ -15,9 +15,21 @@ const TPL = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-path-list {
|
.note-path-list {
|
||||||
max-height: 600px;
|
max-height: 700px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-path-list .path-current {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-path-list .path-archived {
|
||||||
|
color: var(--muted-text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-path-list .path-search {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<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>
|
<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>
|
||||||
@@ -43,20 +55,12 @@ export default class NotePathsWidget extends TabAwareWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (this.noteId === 'root') {
|
if (this.noteId === 'root') {
|
||||||
await this.addPath('root', true);
|
await this.addPath('root');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathSegments = treeService.parseNotePath(this.notePath);
|
for (const notePathRecord of this.note.getSortedNotePaths(this.hoistedNoteId)) {
|
||||||
const activeNoteParentNoteId = pathSegments[pathSegments.length - 2]; // we know this is not root so there must be a parent
|
await this.addPath(notePathRecord);
|
||||||
|
|
||||||
for (const parentNote of this.note.getParentNotes()) {
|
|
||||||
const parentNotePath = treeService.getSomeNotePath(parentNote);
|
|
||||||
// this is to avoid having root notes leading '/'
|
|
||||||
const notePath = parentNotePath ? (parentNotePath + '/' + this.noteId) : this.noteId;
|
|
||||||
const isCurrent = activeNoteParentNoteId === parentNote.noteId;
|
|
||||||
|
|
||||||
await this.addPath(notePath, isCurrent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloneLink = $("<div>")
|
const cloneLink = $("<div>")
|
||||||
@@ -70,7 +74,9 @@ export default class NotePathsWidget extends TabAwareWidget {
|
|||||||
this.$notePathList.append(cloneLink);
|
this.$notePathList.append(cloneLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPath(notePath, isCurrent) {
|
async addPath(notePathRecord) {
|
||||||
|
const notePath = notePathRecord.notePath.join('/');
|
||||||
|
|
||||||
const title = await treeService.getNotePathTitle(notePath);
|
const title = await treeService.getNotePathTitle(notePath);
|
||||||
|
|
||||||
const $noteLink = await linkService.createNoteLink(notePath, {title});
|
const $noteLink = await linkService.createNoteLink(notePath, {title});
|
||||||
@@ -82,8 +88,33 @@ export default class NotePathsWidget extends TabAwareWidget {
|
|||||||
.find('a')
|
.find('a')
|
||||||
.addClass("no-tooltip-preview");
|
.addClass("no-tooltip-preview");
|
||||||
|
|
||||||
if (isCurrent) {
|
const icons = [];
|
||||||
$noteLink.addClass("current");
|
|
||||||
|
if (this.notePath === notePath) {
|
||||||
|
$noteLink.addClass("path-current");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notePathRecord.isInHoistedSubTree) {
|
||||||
|
$noteLink.addClass("path-in-hoisted-subtree");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
icons.push(`<span class="bx bx-trending-up" title="This path is outside of hoisted note and you would have to unhoist."></span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notePathRecord.isArchived) {
|
||||||
|
$noteLink.addClass("path-archived");
|
||||||
|
|
||||||
|
icons.push(`<span class="bx bx-archive" title="Archived"></span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notePathRecord.isSearch) {
|
||||||
|
$noteLink.addClass("path-search");
|
||||||
|
|
||||||
|
icons.push(`<span class="bx bx-search" title="Search"></span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icons.length > 0) {
|
||||||
|
$noteLink.append(` ${icons.join(' ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notePathList.append($noteLink);
|
this.$notePathList.append($noteLink);
|
||||||
@@ -96,4 +127,10 @@ export default class NotePathsWidget extends TabAwareWidget {
|
|||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
await super.refresh();
|
||||||
|
|
||||||
|
this.$widget.find('.dropdown-toggle').dropdown('hide');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import utils from "../services/utils.js";
|
|||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import SpacedUpdate from "../services/spaced_update.js";
|
import SpacedUpdate from "../services/spaced_update.js";
|
||||||
|
import appContext from "../services/app_context.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="note-title-container">
|
<div class="note-title-container">
|
||||||
@@ -37,6 +38,8 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
await server.put(`notes/${this.noteId}/change-title`, {title});
|
await server.put(`notes/${this.noteId}/change-title`, {title});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appContext.addBeforeUnloadListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
@@ -101,6 +104,6 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeUnloadEvent() {
|
beforeUnloadEvent() {
|
||||||
this.spacedUpdate.updateNowIfNecessary();
|
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,8 +203,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
|
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
|
||||||
this.$tree.on("mousedown", ".add-note-button", e => {
|
this.$tree.on("mousedown", ".add-note-button", e => {
|
||||||
const node = $.ui.fancytree.getNode(e);
|
const node = $.ui.fancytree.getNode(e);
|
||||||
|
const parentNotePath = treeService.getNotePath(node);
|
||||||
|
|
||||||
noteCreateService.createNote(node.data.noteId, {
|
noteCreateService.createNote(parentNotePath, {
|
||||||
isProtected: node.data.isProtected
|
isProtected: node.data.isProtected
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -476,7 +477,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
let childNoteIds = note.getChildNoteIds();
|
let childNoteIds = note.getChildNoteIds();
|
||||||
|
|
||||||
if (childNoteIds.length > MAX_SEARCH_RESULTS_IN_TREE) {
|
if (note.type === 'search' && childNoteIds.length > MAX_SEARCH_RESULTS_IN_TREE) {
|
||||||
childNoteIds = childNoteIds.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
childNoteIds = childNoteIds.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +595,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
let childBranches = parentNote.getFilteredChildBranches();
|
let childBranches = parentNote.getFilteredChildBranches();
|
||||||
|
|
||||||
if (childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) {
|
if (parentNote.type === 'search' && childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) {
|
||||||
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,19 +798,17 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
const node = await this.expandToNote(activeContext.notePath);
|
const node = await this.expandToNote(activeContext.notePath);
|
||||||
|
|
||||||
await node.makeVisible({scrollIntoView: true});
|
await node.makeVisible({scrollIntoView: true});
|
||||||
node.setFocus(true);
|
node.setActive(true, {noEvents: true, noFocus: false});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return {FancytreeNode} */
|
/** @return {FancytreeNode} */
|
||||||
async getNodeFromPath(notePath, expand = false, logErrors = true) {
|
async getNodeFromPath(notePath, expand = false, logErrors = true) {
|
||||||
utils.assertArguments(notePath);
|
utils.assertArguments(notePath);
|
||||||
|
/** @let {FancytreeNode} */
|
||||||
|
let parentNode = this.getNodesByNoteId('root')[0];
|
||||||
|
|
||||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
let resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, this.hoistedNoteId, logErrors);
|
||||||
/** @const {FancytreeNode} */
|
|
||||||
let parentNode = null;
|
|
||||||
|
|
||||||
const resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, logErrors);
|
|
||||||
|
|
||||||
if (!resolvedNotePathSegments) {
|
if (!resolvedNotePathSegments) {
|
||||||
if (logErrors) {
|
if (logErrors) {
|
||||||
@@ -819,14 +818,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolvedNotePathSegments = resolvedNotePathSegments.slice(1);
|
||||||
|
|
||||||
for (const childNoteId of resolvedNotePathSegments) {
|
for (const childNoteId of resolvedNotePathSegments) {
|
||||||
if (childNoteId === hoistedNoteId) {
|
|
||||||
// there must be exactly one node with given hoistedNoteId
|
|
||||||
parentNode = this.getNodesByNoteId(childNoteId)[0];
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we expand only after hoisted note since before then nodes are not actually present in the tree
|
// we expand only after hoisted note since before then nodes are not actually present in the tree
|
||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
if (!parentNode.isLoaded()) {
|
if (!parentNode.isLoaded()) {
|
||||||
@@ -857,7 +851,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
// these are real notes with real notePath, user can display them in a detail
|
// these are real notes with real notePath, user can display them in a detail
|
||||||
// but they don't have a node in the tree
|
// but they don't have a node in the tree
|
||||||
|
|
||||||
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`);
|
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -999,6 +993,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
const activeNodeFocused = activeNode && activeNode.hasFocus();
|
const activeNodeFocused = activeNode && activeNode.hasFocus();
|
||||||
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
|
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
|
||||||
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
|
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
|
||||||
|
|
||||||
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
|
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
|
||||||
const activeNoteId = activeNode ? activeNode.data.noteId : null;
|
const activeNoteId = activeNode ? activeNode.data.noteId : null;
|
||||||
|
|
||||||
@@ -1030,6 +1025,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const branch of loadResults.getBranches()) {
|
for (const branch of loadResults.getBranches()) {
|
||||||
|
// adding noteId itself to update all potential clones
|
||||||
|
noteIdsToUpdate.add(branch.noteId);
|
||||||
|
|
||||||
for (const node of this.getNodesByBranchId(branch.branchId)) {
|
for (const node of this.getNodesByBranchId(branch.branchId)) {
|
||||||
if (branch.isDeleted) {
|
if (branch.isDeleted) {
|
||||||
if (node.isActive()) {
|
if (node.isActive()) {
|
||||||
@@ -1048,9 +1046,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
noteIdsToUpdate.add(branch.parentNoteId);
|
noteIdsToUpdate.add(branch.parentNoteId);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
noteIdsToUpdate.add(branch.noteId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!branch.isDeleted) {
|
if (!branch.isDeleted) {
|
||||||
@@ -1119,22 +1114,31 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
node.setActive(true, {noEvents: true, noFocus: true});
|
node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
|
||||||
|
|
||||||
|
if (activeNodeFocused) {
|
||||||
|
node.setFocus(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// this is used when original note has been deleted and we want to move the focus to the note above/below
|
// this is used when original note has been deleted and we want to move the focus to the note above/below
|
||||||
node = await this.expandToNote(nextNotePath, false);
|
node = await this.expandToNote(nextNotePath, false);
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
await appContext.tabManager.getActiveTabContext().setNote(nextNotePath);
|
// FIXME: this is conceptually wrong
|
||||||
}
|
// here note tree is responsible for updating global state of the application
|
||||||
}
|
// this should be done by tabcontext / tabmanager and note tree should only listen to
|
||||||
|
// changes in active note and just set the "active" state
|
||||||
|
// We don't await since that can bring up infinite cycles when e.g. custom widget does some backend requests which wait for max sync ID processed
|
||||||
|
appContext.tabManager.getActiveTabContext().setNote(nextNotePath).then(() => {
|
||||||
const newActiveNode = this.getActiveNode();
|
const newActiveNode = this.getActiveNode();
|
||||||
|
|
||||||
// return focus if the previously active node was also focused
|
// return focus if the previously active node was also focused
|
||||||
if (newActiveNode && activeNodeFocused) {
|
if (newActiveNode && activeNodeFocused) {console.log("FOCUSING!!!");
|
||||||
await newActiveNode.setFocus(true);
|
newActiveNode.setFocus(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,13 +1206,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filterHoistedBranch() {
|
async filterHoistedBranch() {
|
||||||
if (this.tabContext) {
|
if (this.tabContext) {
|
||||||
|
// make sure the hoisted node is loaded (can be unloaded e.g. after tree collapse in another tab)
|
||||||
|
const hoistedNotePath = await treeService.resolveNotePath(this.tabContext.hoistedNoteId);
|
||||||
|
await this.getNodeFromPath(hoistedNotePath);
|
||||||
|
|
||||||
if (this.tabContext.hoistedNoteId === 'root') {
|
if (this.tabContext.hoistedNoteId === 'root') {
|
||||||
this.tree.clearFilter();
|
this.tree.clearFilter();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.tree.filterBranches(node => node.data.noteId === this.tabContext.hoistedNoteId);
|
// hack when hoisted note is cloned then it could be filtered multiple times while we want only 1
|
||||||
|
this.tree.filterBranches(node =>
|
||||||
|
node.data.noteId === this.tabContext.hoistedNoteId // optimization to not having always resolve the node path
|
||||||
|
&& treeService.getNotePath(node) === hoistedNotePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1365,7 +1376,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sortChildNotesCommand({node}) {
|
sortChildNotesCommand({node}) {
|
||||||
treeService.sortAlphabetically(node.data.noteId);
|
import("../dialogs/sort_child_notes.js").then(d => d.showDialog(node.data.noteId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async recentChangesInSubtreeCommand({node}) {
|
async recentChangesInSubtreeCommand({node}) {
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ export default class QuickSearchWidget extends BasicWidget {
|
|||||||
this.$widget.find('.input-group-append').on('shown.bs.dropdown', () => this.search());
|
this.$widget.find('.input-group-append').on('shown.bs.dropdown', () => this.search());
|
||||||
|
|
||||||
utils.bindElShortcut(this.$searchString, 'return', () => {
|
utils.bindElShortcut(this.$searchString, 'return', () => {
|
||||||
|
if (this.$dropdownMenu.is(":visible")) {
|
||||||
|
this.search(); // just update already visible dropdown
|
||||||
|
} else {
|
||||||
this.$dropdownToggle.dropdown('show');
|
this.$dropdownToggle.dropdown('show');
|
||||||
|
}
|
||||||
|
|
||||||
this.$searchString.focus();
|
this.$searchString.focus();
|
||||||
});
|
});
|
||||||
@@ -90,12 +94,18 @@ export default class QuickSearchWidget extends BasicWidget {
|
|||||||
const $link = await linkService.createNoteLink(note.noteId, {showNotePath: true});
|
const $link = await linkService.createNoteLink(note.noteId, {showNotePath: true});
|
||||||
$link.addClass('dropdown-item');
|
$link.addClass('dropdown-item');
|
||||||
$link.attr("tabIndex", "0");
|
$link.attr("tabIndex", "0");
|
||||||
$link.on('click', () => this.$dropdownToggle.dropdown("hide"));
|
$link.on('click', e => {
|
||||||
utils.bindElShortcut($link, 'return', () => {
|
this.$dropdownToggle.dropdown("hide");
|
||||||
$link.find('a').trigger({
|
|
||||||
type: 'click',
|
if (!e.target || e.target.nodeName !== 'A') {
|
||||||
which: 1 // left click
|
// click on the link is handled by link handling but we want the whole item clickable
|
||||||
|
appContext.tabManager.getActiveTabContext().setNote(note.noteId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
utils.bindElShortcut($link, 'return', () => {
|
||||||
|
this.$dropdownToggle.dropdown("hide");
|
||||||
|
|
||||||
|
appContext.tabManager.getActiveTabContext().setNote(note.noteId);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$dropdownMenu.append($link);
|
this.$dropdownMenu.append($link);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default class SearchResultWidget extends TabAwareWidget {
|
|||||||
this.$noResults.toggle(note.getChildNoteIds().length === 0 && !!note.searchResultsLoaded);
|
this.$noResults.toggle(note.getChildNoteIds().length === 0 && !!note.searchResultsLoaded);
|
||||||
this.$notExecutedYet.toggle(!note.searchResultsLoaded);
|
this.$notExecutedYet.toggle(!note.searchResultsLoaded);
|
||||||
|
|
||||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
|
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds(), true);
|
||||||
await noteListRenderer.renderList();
|
await noteListRenderer.renderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export default class TabAwareWidget extends BasicWidget {
|
|||||||
return this.tabContext && this.tabContext.notePath;
|
return this.tabContext && this.tabContext.notePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hoistedNoteId() {
|
||||||
|
return this.tabContext && this.tabContext.hoistedNoteId;
|
||||||
|
}
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
return !!this.note;
|
return !!this.note;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,4 +83,10 @@ export default class InheritedAttributesWidget extends TabAwareWidget {
|
|||||||
getInheritedAttributes(note) {
|
getInheritedAttributes(note) {
|
||||||
return note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
return note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entitiesReloadedEvent({loadResults}) {
|
||||||
|
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,15 +49,16 @@ const TPL = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-detail-editable-text h2 { font-size: 1.8em; }
|
.note-detail-editable-text h2 { font-size: 1.8em; }
|
||||||
.note-detail-editable-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
|
|
||||||
.note-detail-editable-text h3 { font-size: 1.6em; }
|
.note-detail-editable-text h3 { font-size: 1.6em; }
|
||||||
.note-detail-editable-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
|
|
||||||
.note-detail-editable-text h4 { font-size: 1.4em; }
|
.note-detail-editable-text h4 { font-size: 1.4em; }
|
||||||
.note-detail-editable-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
|
|
||||||
.note-detail-editable-text h5 { font-size: 1.2em; }
|
.note-detail-editable-text h5 { font-size: 1.2em; }
|
||||||
.note-detail-editable-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
|
|
||||||
.note-detail-editable-text h6 { font-size: 1.1em; }
|
.note-detail-editable-text h6 { font-size: 1.1em; }
|
||||||
.note-detail-editable-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
|
|
||||||
|
body.heading-style-markdown .note-detail-editable-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
|
||||||
|
body.heading-style-markdown .note-detail-editable-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
|
||||||
|
body.heading-style-markdown .note-detail-editable-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
|
||||||
|
body.heading-style-markdown .note-detail-editable-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
|
||||||
|
body.heading-style-markdown .note-detail-editable-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
|
||||||
|
|
||||||
.note-detail-editable-text-editor {
|
.note-detail-editable-text-editor {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
@@ -274,7 +275,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNoteForReferenceLink(title) {
|
async createNoteForReferenceLink(title) {
|
||||||
const {note} = await noteCreateService.createNote(this.noteId, {
|
const {note} = await noteCreateService.createNote(this.notePath, {
|
||||||
activate: false,
|
activate: false,
|
||||||
title: title
|
title: title
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,16 +14,11 @@ const TPL = `
|
|||||||
.note-detail-readonly-text h5 { font-size: 1.2em; }
|
.note-detail-readonly-text h5 { font-size: 1.2em; }
|
||||||
.note-detail-readonly-text h6 { font-size: 1.1em; }
|
.note-detail-readonly-text h6 { font-size: 1.1em; }
|
||||||
|
|
||||||
.note-detail-readonly-text h2 { font-size: 1.8em; }
|
body.heading-style-markdown .note-detail-readonly-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
|
||||||
.note-detail-readonly-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-readonly-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
|
||||||
.note-detail-readonly-text h3 { font-size: 1.6em; }
|
body.heading-style-markdown .note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
|
||||||
.note-detail-readonly-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-readonly-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
|
||||||
.note-detail-readonly-text h4 { font-size: 1.4em; }
|
body.heading-style-markdown .note-detail-readonly-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
|
||||||
.note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
|
|
||||||
.note-detail-readonly-text h5 { font-size: 1.2em; }
|
|
||||||
.note-detail-readonly-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
|
|
||||||
.note-detail-readonly-text h6 { font-size: 1.1em; }
|
|
||||||
.note-detail-readonly-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
|
|
||||||
|
|
||||||
.note-detail-readonly-text {
|
.note-detail-readonly-text {
|
||||||
padding-left: 22px;
|
padding-left: 22px;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import attributeAutocompleteService from "../../services/attribute_autocomplete.
|
|||||||
import TypeWidget from "./type_widget.js";
|
import TypeWidget from "./type_widget.js";
|
||||||
import appContext from "../../services/app_context.js";
|
import appContext from "../../services/app_context.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
|
import treeCache from "../../services/tree_cache.js";
|
||||||
|
|
||||||
const uniDirectionalOverlays = [
|
const uniDirectionalOverlays = [
|
||||||
[ "Arrow", {
|
[ "Arrow", {
|
||||||
@@ -285,7 +286,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
|
|
||||||
async loadNotesAndRelations() {
|
async loadNotesAndRelations() {
|
||||||
const noteIds = this.mapData.notes.map(note => note.noteId);
|
const noteIds = this.mapData.notes.map(note => note.noteId);
|
||||||
const data = await server.post("notes/relation-map", {noteIds});
|
const data = await server.post("notes/relation-map", {noteIds, relationMapNoteId: this.noteId});
|
||||||
|
|
||||||
this.relations = [];
|
this.relations = [];
|
||||||
|
|
||||||
@@ -531,8 +532,11 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
linkService.goToLink(e);
|
linkService.goToLink(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const note = await treeCache.getNote(noteId);
|
||||||
|
|
||||||
const $noteBox = $("<div>")
|
const $noteBox = $("<div>")
|
||||||
.addClass("note-box")
|
.addClass("note-box")
|
||||||
|
.addClass(note.getCssClass())
|
||||||
.prop("id", this.noteIdToId(noteId))
|
.prop("id", this.noteIdToId(noteId))
|
||||||
.append($("<span>").addClass("title").append($link))
|
.append($("<span>").addClass("title").append($link))
|
||||||
.append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note."))
|
.append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note."))
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const sql = require('../../services/sql');
|
|||||||
const dateUtils = require('../../services/date_utils');
|
const dateUtils = require('../../services/date_utils');
|
||||||
const noteService = require('../../services/notes');
|
const noteService = require('../../services/notes');
|
||||||
const attributeService = require('../../services/attributes');
|
const attributeService = require('../../services/attributes');
|
||||||
|
const cls = require('../../services/cls');
|
||||||
|
const repository = require('../../services/repository');
|
||||||
|
|
||||||
function getInboxNote(req) {
|
function getInboxNote(req) {
|
||||||
return attributeService.getNoteWithLabel('inbox')
|
return attributeService.getNoteWithLabel('inbox')
|
||||||
@@ -59,21 +61,54 @@ function createSqlConsole() {
|
|||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSearchNote() {
|
function createSearchNote(req) {
|
||||||
|
const params = req.body;
|
||||||
|
const searchString = params.searchString || "";
|
||||||
|
let ancestorNoteId = params.ancestorNoteId;
|
||||||
|
|
||||||
|
const hoistedNote = cls.getHoistedNoteId() && cls.getHoistedNoteId() !== 'root'
|
||||||
|
? repository.getNote(cls.getHoistedNoteId())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let searchHome;
|
||||||
|
|
||||||
|
if (hoistedNote) {
|
||||||
|
([searchHome] = hoistedNote.getDescendantNotesWithLabel('hoistedSearchHome'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchHome) {
|
||||||
const today = dateUtils.localNowDate();
|
const today = dateUtils.localNowDate();
|
||||||
|
|
||||||
const searchHome =
|
searchHome = attributeService.getNoteWithLabel('searchHome')
|
||||||
attributeService.getNoteWithLabel('searchHome')
|
|
||||||
|| dateNoteService.getDateNote(today);
|
|| dateNoteService.getDateNote(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hoistedNote) {
|
||||||
|
|
||||||
|
if (!hoistedNote.getDescendantNoteIds().includes(searchHome.noteId)) {
|
||||||
|
// otherwise the note would be saved outside of the hoisted context which is weird
|
||||||
|
searchHome = hoistedNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ancestorNoteId) {
|
||||||
|
ancestorNoteId = hoistedNote.noteId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const {note} = noteService.createNewNote({
|
const {note} = noteService.createNewNote({
|
||||||
parentNoteId: searchHome.noteId,
|
parentNoteId: searchHome.noteId,
|
||||||
title: 'Search: ',
|
title: 'Search: ' + searchString,
|
||||||
content: "",
|
content: "",
|
||||||
type: 'search',
|
type: 'search',
|
||||||
mime: 'application/json'
|
mime: 'application/json'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
note.setLabel('searchString', searchString);
|
||||||
|
|
||||||
|
if (ancestorNoteId) {
|
||||||
|
note.setRelation('ancestor', ancestorNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const noteService = require('../../services/notes');
|
|||||||
const treeService = require('../../services/tree');
|
const treeService = require('../../services/tree');
|
||||||
const repository = require('../../services/repository');
|
const repository = require('../../services/repository');
|
||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
|
const log = require('../../services/log');
|
||||||
const TaskContext = require('../../services/task_context');
|
const TaskContext = require('../../services/task_context');
|
||||||
|
|
||||||
function getNote(req) {
|
function getNote(req) {
|
||||||
@@ -85,10 +86,20 @@ function undeleteNote(req) {
|
|||||||
taskContext.taskSucceeded();
|
taskContext.taskSucceeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortNotes(req) {
|
function sortChildNotes(req) {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
const {sortBy, sortDirection} = req.body;
|
||||||
|
|
||||||
treeService.sortNotesAlphabetically(noteId);
|
log.info(`Sorting ${noteId} children with ${sortBy} ${sortDirection}`);
|
||||||
|
|
||||||
|
const reverse = sortDirection === 'desc';
|
||||||
|
|
||||||
|
if (sortBy === 'title') {
|
||||||
|
treeService.sortNotesByTitle(noteId, false, reverse);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
treeService.sortNotes(noteId, sortBy, reverse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function protectNote(req) {
|
function protectNote(req) {
|
||||||
@@ -117,7 +128,8 @@ function setNoteTypeMime(req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRelationMap(req) {
|
function getRelationMap(req) {
|
||||||
const noteIds = req.body.noteIds;
|
const {relationMapNoteId, noteIds} = req.body;
|
||||||
|
|
||||||
const resp = {
|
const resp = {
|
||||||
// noteId => title
|
// noteId => title
|
||||||
noteTitles: {},
|
noteTitles: {},
|
||||||
@@ -134,12 +146,23 @@ function getRelationMap(req) {
|
|||||||
|
|
||||||
const questionMarks = noteIds.map(noteId => '?').join(',');
|
const questionMarks = noteIds.map(noteId => '?').join(',');
|
||||||
|
|
||||||
|
const relationMapNote = repository.getNote(relationMapNoteId);
|
||||||
|
|
||||||
|
const displayRelationsVal = relationMapNote.getLabelValue('displayRelations');
|
||||||
|
const displayRelations = !displayRelationsVal ? [] : displayRelationsVal
|
||||||
|
.split(",")
|
||||||
|
.map(token => token.trim());
|
||||||
|
|
||||||
|
console.log("displayRelations", displayRelations);
|
||||||
|
|
||||||
const notes = repository.getEntities(`SELECT * FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
|
const notes = repository.getEntities(`SELECT * FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
resp.noteTitles[note.noteId] = note.title;
|
resp.noteTitles[note.noteId] = note.title;
|
||||||
|
|
||||||
resp.relations = resp.relations.concat(note.getRelations()
|
resp.relations = resp.relations.concat(note.getRelations()
|
||||||
|
.filter(relation => !relation.isAutoLink() || displayRelations.includes(relation.name))
|
||||||
|
.filter(relation => displayRelations.length === 0 || displayRelations.includes(relation.name))
|
||||||
.filter(relation => noteIds.includes(relation.value))
|
.filter(relation => noteIds.includes(relation.value))
|
||||||
.map(relation => ({
|
.map(relation => ({
|
||||||
attributeId: relation.attributeId,
|
attributeId: relation.attributeId,
|
||||||
@@ -203,7 +226,7 @@ module.exports = {
|
|||||||
deleteNote,
|
deleteNote,
|
||||||
undeleteNote,
|
undeleteNote,
|
||||||
createNote,
|
createNote,
|
||||||
sortNotes,
|
sortChildNotes,
|
||||||
protectNote,
|
protectNote,
|
||||||
setNoteTypeMime,
|
setNoteTypeMime,
|
||||||
getRelationMap,
|
getRelationMap,
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ const ALLOWED_OPTIONS = new Set([
|
|||||||
'nativeTitleBarVisible',
|
'nativeTitleBarVisible',
|
||||||
'attributeListExpanded',
|
'attributeListExpanded',
|
||||||
'promotedAttributesExpanded',
|
'promotedAttributesExpanded',
|
||||||
'similarNotesExpanded'
|
'similarNotesExpanded',
|
||||||
|
'headingStyle'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function getOptions() {
|
function getOptions() {
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ function getTree(req) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(subTreeNoteId in noteCache.notes)) {
|
||||||
|
return [404, `Note ${subTreeNoteId} not found in the cache`];
|
||||||
|
}
|
||||||
|
|
||||||
collect(noteCache.notes[subTreeNoteId]);
|
collect(noteCache.notes[subTreeNoteId]);
|
||||||
|
|
||||||
return getNotesAndBranchesAndAttributes(collectedNoteIds);
|
return getNotesAndBranchesAndAttributes(collectedNoteIds);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function index(req, res) {
|
|||||||
res.render(view, {
|
res.render(view, {
|
||||||
csrfToken: csrfToken,
|
csrfToken: csrfToken,
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
|
headingStyle: options.headingStyle,
|
||||||
mainFontSize: parseInt(options.mainFontSize),
|
mainFontSize: parseInt(options.mainFontSize),
|
||||||
treeFontSize: parseInt(options.treeFontSize),
|
treeFontSize: parseInt(options.treeFontSize),
|
||||||
detailFontSize: parseInt(options.detailFontSize),
|
detailFontSize: parseInt(options.detailFontSize),
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ function register(app) {
|
|||||||
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
|
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
|
||||||
apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote);
|
apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote);
|
||||||
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
|
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
|
||||||
apiRoute(PUT, '/api/notes/:noteId/sort', notesApiRoute.sortNotes);
|
apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes);
|
||||||
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote);
|
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote);
|
||||||
apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
|
apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
|
||||||
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
|
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const BUILTIN_ATTRIBUTES = [
|
|||||||
{ type: 'label', name: 'workspaceIconClass' },
|
{ type: 'label', name: 'workspaceIconClass' },
|
||||||
{ type: 'label', name: 'workspaceTabBackgroundColor' },
|
{ type: 'label', name: 'workspaceTabBackgroundColor' },
|
||||||
{ type: 'label', name: 'searchHome' },
|
{ type: 'label', name: 'searchHome' },
|
||||||
|
{ type: 'label', name: 'hoistedSearchHome' },
|
||||||
{ type: 'label', name: 'sqlConsoleHome' },
|
{ type: 'label', name: 'sqlConsoleHome' },
|
||||||
{ type: 'label', name: 'datePattern' },
|
{ type: 'label', name: 'datePattern' },
|
||||||
|
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ function BackendScriptApi(currentNote, apiParams) {
|
|||||||
* @method
|
* @method
|
||||||
* @param {string} parentNoteId - this note's child notes will be sorted
|
* @param {string} parentNoteId - this note's child notes will be sorted
|
||||||
*/
|
*/
|
||||||
this.sortNotesAlphabetically = treeService.sortNotesAlphabetically;
|
this.sortNotesByTitle = treeService.sortNotesByTitle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
|
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = { buildDate:"2021-02-19T23:15:18+01:00", buildRevision: "56506d33a7fea668f9ba2683b2836a30d28cd96c" };
|
module.exports = { buildDate:"2021-03-10T23:35:12+01:00", buildRevision: "6f901e6852c33ba0dae6c70efb9f65e5b0028995" };
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ function addEntityChange(entityChange, sourceId, isSynced) {
|
|||||||
cls.addEntityChange(localEntityChange);
|
cls.addEntityChange(localEntityChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addNoteReorderingEntityChange(parentNoteId, sourceId) {
|
||||||
|
addEntityChange({
|
||||||
|
entityName: "note_reordering",
|
||||||
|
entityId: parentNoteId,
|
||||||
|
hash: 'N/A',
|
||||||
|
isErased: false,
|
||||||
|
utcDateChanged: dateUtils.utcNowDateTime()
|
||||||
|
}, sourceId);
|
||||||
|
|
||||||
|
const eventService = require('./events');
|
||||||
|
|
||||||
|
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||||
|
entityName: 'note_reordering',
|
||||||
|
entity: sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [parentNoteId])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function moveEntityChangeToTop(entityName, entityId) {
|
function moveEntityChangeToTop(entityName, entityId) {
|
||||||
const [hash, isSynced] = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]);
|
const [hash, isSynced] = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]);
|
||||||
|
|
||||||
@@ -121,13 +138,7 @@ function fillAllEntityChanges() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addNoteReorderingEntityChange: (parentNoteId, sourceId) => addEntityChange({
|
addNoteReorderingEntityChange,
|
||||||
entityName: "note_reordering",
|
|
||||||
entityId: parentNoteId,
|
|
||||||
hash: 'N/A',
|
|
||||||
isErased: false,
|
|
||||||
utcDateChanged: dateUtils.utcNowDateTime()
|
|
||||||
}, sourceId),
|
|
||||||
moveEntityChangeToTop,
|
moveEntityChangeToTop,
|
||||||
addEntityChange,
|
addEntityChange,
|
||||||
fillAllEntityChanges,
|
fillAllEntityChanges,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const TurndownService = require('turndown');
|
const TurndownService = require('turndown');
|
||||||
const turndownPluginGfm = require('turndown-plugin-gfm');
|
const turndownPluginGfm = require('joplin-turndown-plugin-gfm');
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
|
|||||||
|
|
||||||
for (const parentNote of noteFromCache.parents) {
|
for (const parentNote of noteFromCache.parents) {
|
||||||
if (parentNote.hasLabel("sorted")) {
|
if (parentNote.hasLabel("sorted")) {
|
||||||
treeService.sortNotesAlphabetically(parentNote.noteId);
|
treeService.sortNotesByTitle(parentNote.noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,23 +53,19 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
|||||||
if (entity.type === 'relation' && entity.name === 'template') {
|
if (entity.type === 'relation' && entity.name === 'template') {
|
||||||
const note = repository.getNote(entity.noteId);
|
const note = repository.getNote(entity.noteId);
|
||||||
|
|
||||||
if (!note.isStringNote()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = note.getContent();
|
|
||||||
|
|
||||||
if (content && content.trim().length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templateNote = repository.getNote(entity.value);
|
const templateNote = repository.getNote(entity.value);
|
||||||
|
|
||||||
if (!templateNote) {
|
if (!templateNote) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (templateNote.isStringNote()) {
|
const content = note.getContent();
|
||||||
|
|
||||||
|
if (["text", "code"].includes(note.type)
|
||||||
|
// if the note has already content we're not going to overwrite it with template's one
|
||||||
|
&& (!content || content.trim().length === 0)
|
||||||
|
&& templateNote.isStringNote()) {
|
||||||
|
|
||||||
const templateNoteContent = templateNote.getContent();
|
const templateNoteContent = templateNote.getContent();
|
||||||
|
|
||||||
if (templateNoteContent) {
|
if (templateNoteContent) {
|
||||||
@@ -81,17 +77,21 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
|||||||
note.save();
|
note.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we'll copy the children notes only if there's none so far
|
||||||
|
// this protects against e.g. multiple assignment of template relation resulting in having multiple copies of the subtree
|
||||||
|
if (note.getChildNotes().length === 0 && !note.isDescendantOfNote(templateNote.noteId)) {
|
||||||
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
|
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (entity.type === 'label' && entity.name === 'sorted') {
|
else if (entity.type === 'label' && entity.name === 'sorted') {
|
||||||
treeService.sortNotesAlphabetically(entity.noteId);
|
treeService.sortNotesByTitle(entity.noteId);
|
||||||
|
|
||||||
if (entity.isInheritable) {
|
if (entity.isInheritable) {
|
||||||
const note = noteCache.notes[entity.noteId];
|
const note = noteCache.notes[entity.noteId];
|
||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
for (const noteId of note.subtreeNoteIds) {
|
for (const noteId of note.subtreeNoteIds) {
|
||||||
treeService.sortNotesAlphabetically(noteId);
|
treeService.sortNotesByTitle(noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
if (!metaFile) {
|
if (!metaFile) {
|
||||||
// if there's no meta file then the notes are created based on the order in that tar file but that
|
// if there's no meta file then the notes are created based on the order in that tar file but that
|
||||||
// is usually quite random so we sort the notes in the way they would appear in the file manager
|
// is usually quite random so we sort the notes in the way they would appear in the file manager
|
||||||
treeService.sortNotesAlphabetically(noteId, true);
|
treeService.sortNotesByTitle(noteId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
taskContext.increaseProgressCount();
|
taskContext.increaseProgressCount();
|
||||||
|
|||||||
@@ -180,10 +180,14 @@ class Note {
|
|||||||
return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable);
|
return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// will sort the parents so that non-archived are first and archived at the end
|
// will sort the parents so that non-search & non-archived are first and archived at the end
|
||||||
// this is done so that non-archived paths are always explored as first when searching for note path
|
// this is done so that non-search & non-archived paths are always explored as first when looking for note path
|
||||||
resortParents() {
|
resortParents() {
|
||||||
this.parents.sort((a, b) => a.hasInheritableOwnedArchivedLabel ? 1 : -1);
|
this.parentBranches.sort((a, b) =>
|
||||||
|
a.branchId.startsWith('virt-')
|
||||||
|
|| a.parentNote.hasInheritableOwnedArchivedLabel ? 1 : -1);
|
||||||
|
|
||||||
|
this.parents = this.parentBranches.map(branch => branch.parentNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
|
|||||||
delete noteCache.attributes[attributeId];
|
delete noteCache.attributes[attributeId];
|
||||||
|
|
||||||
if (attr) {
|
if (attr) {
|
||||||
delete noteCache.attributeIndex[`${attr.type}-${attr.name.toLowerCase()}`];
|
const key = `${attr.type}-${attr.name.toLowerCase()}`;
|
||||||
|
|
||||||
|
if (key in noteCache.attributeIndex) {
|
||||||
|
noteCache.attributeIndex[key] = noteCache.attributeIndex[key].filter(attr => attr.attributeId !== attributeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (attributeId in noteCache.attributes) {
|
else if (attributeId in noteCache.attributes) {
|
||||||
@@ -149,6 +153,19 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (entityName === 'note_reordering') {
|
||||||
|
const parentNoteIds = new Set();
|
||||||
|
|
||||||
|
for (const branchId in entity) {
|
||||||
|
const branch = noteCache.branches[branchId];
|
||||||
|
|
||||||
|
if (branch) {
|
||||||
|
branch.notePosition = entity[branchId];
|
||||||
|
|
||||||
|
parentNoteIds.add(branch.parentNoteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
|
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
|
||||||
|
|||||||
@@ -238,7 +238,15 @@ async function findSimilarNotes(noteId) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateLimits = buildDateLimits(baseNote);
|
let dateLimits;
|
||||||
|
|
||||||
|
try {
|
||||||
|
dateLimits = buildDateLimits(baseNote);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
throw new Error(`Date limits failed with ${e.message}, entity: ${JSON.stringify(baseNote.pojo)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const rewardMap = buildRewardMap(baseNote);
|
const rewardMap = buildRewardMap(baseNote);
|
||||||
let ancestorRewardCache = {};
|
let ancestorRewardCache = {};
|
||||||
const ancestorNoteIds = new Set(baseNote.ancestors.map(note => note.noteId));
|
const ancestorNoteIds = new Set(baseNote.ancestors.map(note => note.noteId));
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ const defaultOptions = [
|
|||||||
{ name: 'attributeListExpanded', value: 'false', isSynced: false },
|
{ name: 'attributeListExpanded', value: 'false', isSynced: false },
|
||||||
{ name: 'promotedAttributesExpanded', value: 'true', isSynced: true },
|
{ name: 'promotedAttributesExpanded', value: 'true', isSynced: true },
|
||||||
{ name: 'similarNotesExpanded', value: 'true', isSynced: true },
|
{ name: 'similarNotesExpanded', value: 'true', isSynced: true },
|
||||||
{ name: 'debugModeEnabled', value: 'false', isSynced: false }
|
{ name: 'debugModeEnabled', value: 'false', isSynced: false },
|
||||||
|
{ name: 'headingStyle', value: 'markdown', isSynced: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
function initStartupOptions() {
|
function initStartupOptions() {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class SearchResult {
|
|||||||
computeScore(tokens) {
|
computeScore(tokens) {
|
||||||
this.score = 0;
|
this.score = 0;
|
||||||
|
|
||||||
|
// matches in attributes don't get extra points and thus are implicitly valued less than note path matches
|
||||||
|
|
||||||
const chunks = this.notePathTitle.toLowerCase().split(" ");
|
const chunks = this.notePathTitle.toLowerCase().split(" ");
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function insert(tableName, rec, replace = false) {
|
|||||||
|
|
||||||
const res = execute(query, Object.values(rec));
|
const res = execute(query, Object.values(rec));
|
||||||
|
|
||||||
return res.lastInsertRowid;
|
return res ? res.lastInsertRowid : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replace(tableName, rec) {
|
function replace(tableName, rec) {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ async function doLogin() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (sourceIdService.isLocalSourceId(resp.sourceId)) {
|
if (sourceIdService.isLocalSourceId(resp.sourceId)) {
|
||||||
throw new Error(`Sync server has source ID ${resp.sourceId} which is also local. Your sync setup is probably trying to connect to itself.`);
|
throw new Error(`Sync server has source ID ${resp.sourceId} which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
syncContext.sourceId = resp.sourceId;
|
syncContext.sourceId = resp.sourceId;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const repository = require('./repository');
|
|||||||
const Branch = require('../entities/branch');
|
const Branch = require('../entities/branch');
|
||||||
const entityChangesService = require('./entity_changes.js');
|
const entityChangesService = require('./entity_changes.js');
|
||||||
const protectedSessionService = require('./protected_session');
|
const protectedSessionService = require('./protected_session');
|
||||||
|
const noteCache = require('./note_cache/note_cache');
|
||||||
|
|
||||||
function getNotes(noteIds) {
|
function getNotes(noteIds) {
|
||||||
// we return also deleted notes which have been specifically asked for
|
// we return also deleted notes which have been specifically asked for
|
||||||
@@ -105,7 +106,7 @@ function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
|
function sortNotesByTitle(parentNoteId, foldersFirst = false, reverse = false) {
|
||||||
sql.transactional(() => {
|
sql.transactional(() => {
|
||||||
const notes = sql.getRows(
|
const notes = sql.getRows(
|
||||||
`SELECT branches.branchId, notes.noteId, title, isProtected,
|
`SELECT branches.branchId, notes.noteId, title, isProtected,
|
||||||
@@ -119,7 +120,7 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
|
|||||||
protectedSessionService.decryptNotes(notes);
|
protectedSessionService.decryptNotes(notes);
|
||||||
|
|
||||||
notes.sort((a, b) => {
|
notes.sort((a, b) => {
|
||||||
if (directoriesFirst && ((a.hasChildren && !b.hasChildren) || (!a.hasChildren && b.hasChildren))) {
|
if (foldersFirst && ((a.hasChildren && !b.hasChildren) || (!a.hasChildren && b.hasChildren))) {
|
||||||
// exactly one note of the two is a directory so the sorting will be done based on this status
|
// exactly one note of the two is a directory so the sorting will be done based on this status
|
||||||
return a.hasChildren ? -1 : 1;
|
return a.hasChildren ? -1 : 1;
|
||||||
}
|
}
|
||||||
@@ -128,12 +129,45 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
notes.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
let position = 10;
|
let position = 10;
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
|
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
|
||||||
[position, note.branchId]);
|
[position, note.branchId]);
|
||||||
|
|
||||||
|
noteCache.branches[note.branchId].notePosition = position;
|
||||||
|
|
||||||
|
position += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityChangesService.addNoteReorderingEntityChange(parentNoteId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortNotes(parentNoteId, sortBy, reverse = false) {
|
||||||
|
sql.transactional(() => {
|
||||||
|
const notes = repository.getNote(parentNoteId).getChildNotes();
|
||||||
|
|
||||||
|
notes.sort((a, b) => a[sortBy] < b[sortBy] ? -1 : 1);
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
notes.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = 10;
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
const branch = note.getBranches().find(b => b.parentNoteId === parentNoteId);
|
||||||
|
|
||||||
|
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
|
||||||
|
[position, branch.branchId]);
|
||||||
|
|
||||||
|
noteCache.branches[branch.branchId].notePosition = position;
|
||||||
|
|
||||||
position += 10;
|
position += 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +225,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getNotes,
|
getNotes,
|
||||||
validateParentChild,
|
validateParentChild,
|
||||||
sortNotesAlphabetically,
|
sortNotesByTitle,
|
||||||
|
sortNotes,
|
||||||
setNoteToParent
|
setNoteToParent
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -246,9 +246,13 @@ function getNoteTitle(filePath, replaceUnderscoresWithSpaces, noteMeta) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeLimit(promise, limitMs) {
|
function timeLimit(promise, limitMs, errorMessage) {
|
||||||
|
if (!promise || !promise.then) { // it's not actually a promise
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
// better stack trace if created outside of promise
|
// better stack trace if created outside of promise
|
||||||
const error = new Error('Process exceeded time limit ' + limitMs);
|
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
|
||||||
|
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<link rel="shortcut icon" href="favicon.ico">
|
<link rel="shortcut icon" href="favicon.ico">
|
||||||
<title>Trilium Notes</title>
|
<title>Trilium Notes</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="desktop theme-<%= theme %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
|
<body class="desktop theme-<%= theme %> heading-style-<%= headingStyle %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;">
|
||||||
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<%- include('dialogs/move_to.ejs') %>
|
<%- include('dialogs/move_to.ejs') %>
|
||||||
<%- include('dialogs/backend_log.ejs') %>
|
<%- include('dialogs/backend_log.ejs') %>
|
||||||
<%- include('dialogs/include_note.ejs') %>
|
<%- include('dialogs/include_note.ejs') %>
|
||||||
|
<%- include('dialogs/sort_child_notes.ejs') %>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.baseApiUrl = 'api/';
|
window.baseApiUrl = 'api/';
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
|
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
<ul>
|
<ul>
|
||||||
<li><kbd>#</kbd>, <kbd>##</kbd>, <kbd>###</kbd> etc. followed by space for headings</li>
|
<li><kbd>##</kbd>, <kbd>###</kbd>, <kbd>####</kbd> etc. followed by space for headings</li>
|
||||||
<li><kbd>*</kbd> or <kbd>-</kbd> followed by space for bullet list</li>
|
<li><kbd>*</kbd> or <kbd>-</kbd> followed by space for bullet list</li>
|
||||||
<li><kbd>1.</kbd> or <kbd>1)</kbd> followed by space for numbered list</li>
|
<li><kbd>1.</kbd> or <kbd>1)</kbd> followed by space for numbered list</li>
|
||||||
<li>start a line with <kbd>></kbd> followed by space for block quote</li>
|
<li>start a line with <kbd>></kbd> followed by space for block quote</li>
|
||||||
|
|||||||
60
src/views/dialogs/sort_child_notes.ejs
Normal file
60
src/views/dialogs/sort_child_notes.ejs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<div id="sort-child-notes-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg" style="max-width: 500px" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title mr-auto">Sort children by ...</h5>
|
||||||
|
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="sort-child-notes-form">
|
||||||
|
<div class="modal-body">
|
||||||
|
<h5>Sorting criteria</h5>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="sort-by" value="title" id="sort-by-title" checked>
|
||||||
|
<label class="form-check-label" for="sort-by-title">
|
||||||
|
title
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="sort-by" value="dateCreated" id="sort-by-date-created">
|
||||||
|
<label class="form-check-label" for="sort-by-date-created">
|
||||||
|
date created
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="sort-by" value="dateModified" id="sort-by-date-modified">
|
||||||
|
<label class="form-check-label" for="sort-by-date-modified">
|
||||||
|
date modified
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<h5>Sorting direction</h5>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="sort-direction" value="asc" id="sort-direction-asc" checked>
|
||||||
|
<label class="form-check-label" for="sort-direction-asc">
|
||||||
|
ascending
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="sort-direction" value="desc" id="sort-direction-desc">
|
||||||
|
<label class="form-check-label" for="sort-direction-desc">
|
||||||
|
descending
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary">Sort <kbd>enter</kbd></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="mobile theme-<%= theme %>">
|
<body class="mobile theme-<%= theme %> heading-style-<%= headingStyle %>">
|
||||||
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
||||||
|
|
||||||
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>
|
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user