Compare commits

...

39 Commits

Author SHA1 Message Date
zadam
12b468d3dc release 0.46.4-beta 2021-03-10 23:35:12 +01:00
zadam
6f901e6852 use icons instead of plain text to differentiate between different paths in note paths widget 2021-03-10 23:11:48 +01:00
zadam
09e9ac4d00 prevent cycles in resolving the notepath, fixes #1730 2021-03-10 22:54:55 +01:00
zadam
a33ac65fdf fix focus on moving notes with keyboard 2021-03-09 22:24:59 +01:00
zadam
f8fb071a6f added option to bring back plain (non-markdown) headings, closes #1678 2021-03-09 22:06:40 +01:00
zadam
a654078e56 fix broken template copy, closes #1724 2021-03-09 20:51:57 +01:00
zadam
fba68681aa when resolving note path check if there's a hoisted note in it, if not, try again to find some path with hoisted note, closes #1718 2021-03-09 20:37:56 +01:00
zadam
50c84e0f5f release 0.46.3-beta 2021-03-08 23:11:11 +01:00
zadam
f27370d44f fix putting focus back to the note tree after note deletion 2021-03-08 23:10:34 +01:00
zadam
c6c9202c00 fix note icon color in dark mode 2021-03-08 22:06:26 +01:00
zadam
2cafda5f66 note paths visually distinguishes between different note paths, closes #1669 2021-03-08 22:04:52 +01:00
zadam
873953cbaf fix bug 2021-03-08 00:09:48 +01:00
zadam
d51744ce19 make note paths current path underlined 2021-03-08 00:07:00 +01:00
zadam
7df8c940b6 have paths in "note paths" widget also sorted by priority 2021-03-08 00:04:43 +01:00
zadam
9bac2a4819 refresh inherited attribute list after attr change, closes #1717 2021-03-07 23:31:56 +01:00
zadam
88147f7a0a fixed "create note after" position issue 2021-03-06 23:53:10 +01:00
zadam
ca77211b38 improved template code with better heuristics on when to copy things from the template 2021-03-06 21:34:03 +01:00
zadam
4606e8d118 non-search notes should have no children limit, #1673 2021-03-06 20:31:12 +01:00
zadam
9f002fa802 change the heuristics to choose the best note path when ambiguous/incomplete/just noteId is provided, #1711 2021-03-06 20:23:29 +01:00
zadam
060d4fc27b fix duplication of hoisted note tree when hoisted note has clones 2021-03-04 23:19:27 +01:00
zadam
bf0fbe201e hack when hoisted note is cloned then it could be filtered multiple times while we want only 1 2021-03-03 23:00:16 +01:00
zadam
721e5da672 use notePath instead of noteId for note creation to correctly work with cloned ancestors 2021-03-03 22:48:06 +01:00
zadam
8192b51b8a cleanup of createTopLevelNote 2021-03-03 22:27:57 +01:00
zadam
73514a63d8 when resolving note path attempt to find one going through hoisted note 2021-03-03 21:49:57 +01:00
zadam
f8c310eb8f added license mention into the README noting AGPL v3+, #1708 2021-03-03 21:41:44 +01:00
zadam
b9422b0efd cssClass is now added also to link map and relation map, closes #1702 2021-03-02 23:20:53 +01:00
zadam
14ced949a9 fix modifying index in note cache when deleting attribute, closes #1706 2021-03-02 23:10:42 +01:00
zadam
5b5c2a2dbb fix null utcDateChanged in entity_changes, closes #1705 2021-03-02 22:08:29 +01:00
zadam
2c958eaacb Merge branch 'sort-by' 2021-02-28 23:40:37 +01:00
zadam
4aa27b6033 added "sort by" dialog 2021-02-28 23:40:15 +01:00
zadam
89a0c5a1c9 fix "no data" when switching between tabs with different hoisted notes, closes #1699 2021-02-28 19:46:04 +01:00
zadam
78e48095e6 prompt user when there are unsaved changes, #1692 2021-02-27 23:39:02 +01:00
zadam
02016ed031 fix create full search note 2021-02-27 21:19:54 +01:00
zadam
cb91dadeca add possibility to define search home for hoisted notes, #1694 2021-02-27 21:18:10 +01:00
zadam
2c755bcc38 don't fail (immediatelly) when sql insert doesn't return lastInsertRowid, #1665 2021-02-27 21:09:13 +01:00
zadam
3c7a6bc1e4 use longer update interval for web 2021-02-27 21:08:27 +01:00
zadam
3fe87259e2 if search note would end up outside of current hoisting, save it under the hoisted note, closes #1694 2021-02-26 23:33:22 +01:00
zadam
d476dfc53b fix searching the second time in quick search, #1694 2021-02-26 23:20:49 +01:00
zadam
1c59bc4d3c sort child notes by ... WIP 2021-02-25 22:26:46 +01:00
51 changed files with 1265 additions and 445 deletions

View File

@@ -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.
* [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)
## 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.

923
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.46.2-beta",
"version": "0.46.4-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -35,7 +35,7 @@
"dayjs": "1.10.4",
"ejs": "3.1.6",
"electron-debug": "3.2.0",
"electron-dl": "3.1.0",
"electron-dl": "3.2.0",
"electron-find": "1.0.6",
"electron-window-state": "5.0.3",
"express": "4.17.1",
@@ -51,10 +51,10 @@
"is-animated": "^2.0.1",
"is-svg": "4.2.1",
"jimp": "0.16.1",
"jsdom": "^16.4.0",
"jsdom": "16.5.0",
"mime-types": "2.1.29",
"multer": "1.4.2",
"node-abi": "2.19.3",
"node-abi": "2.21.0",
"open": "7.4.2",
"portscanner": "2.2.0",
"rand-token": "1.0.1",
@@ -72,14 +72,14 @@
"turndown": "7.0.0",
"joplin-turndown-plugin-gfm": "1.0.12",
"unescape": "1.0.1",
"ws": "7.4.3",
"ws": "7.4.4",
"yauzl": "2.10.0",
"yazl": "2.5.1"
},
"devDependencies": {
"cross-env": "7.0.3",
"electron": "9.4.3",
"electron-builder": "22.9.1",
"electron": "9.4.4",
"electron-builder": "22.10.5",
"electron-packager": "15.2.0",
"electron-rebuild": "2.3.5",
"esm": "3.2.25",
@@ -87,7 +87,7 @@
"jsdoc": "3.6.6",
"lorem-ipsum": "2.0.3",
"rcedit": "3.0.0",
"webpack": "5.24.2",
"webpack": "5.24.4",
"webpack-cli": "4.5.0"
},
"optionalDependencies": {

View File

@@ -41,7 +41,7 @@ class Entity {
}
getUtcDateChanged() {
return this.utcDateModified;
return this.utcDateModified || this.utcDateCreated;
}
get repository() {

View File

@@ -28,6 +28,16 @@ const TPL = `
</select>
</div>
</div>
<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>
@@ -78,6 +88,7 @@ export default class ApperanceOptions {
this.$themeSelect = $("#theme-select");
this.$zoomFactorSelect = $("#zoom-factor-select");
this.$nativeTitleBarSelect = $("#native-title-bar-select");
this.$headingStyle = $("#heading-style");
this.$mainFontSize = $("#main-font-size");
this.$treeFontSize = $("#tree-font-size");
this.$detailFontSize = $("#detail-font-size");
@@ -86,11 +97,7 @@ export default class ApperanceOptions {
this.$themeSelect.on('change', () => {
const newTheme = this.$themeSelect.val();
for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes
if (clazz.startsWith("theme-")) {
this.$body.removeClass(clazz);
}
}
this.toggleBodyClass("theme-", newTheme);
const noteId = $(this).find(":selected").attr("data-note-id");
@@ -100,8 +107,6 @@ export default class ApperanceOptions {
libraryLoader.requireCss(`api/notes/download/${noteId}`);
}
this.$body.addClass("theme-" + newTheme);
server.put('options/theme/' + newTheme);
});
@@ -113,6 +118,14 @@ export default class ApperanceOptions {
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 () => {
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) {
const themes = [
{ val: 'white', title: 'White' },
@@ -159,6 +182,8 @@ export default class ApperanceOptions {
this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide');
this.$headingStyle.val(options.headingStyle);
this.$mainFontSize.val(options.mainFontSize);
this.$treeFontSize.val(options.treeFontSize);
this.$detailFontSize.val(options.detailFontSize);

View 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();
}

View File

@@ -2,6 +2,7 @@ import server from '../services/server.js';
import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js";
import options from "../services/options.js";
import treeCache from "../services/tree_cache.js";
const LABEL = 'label';
const RELATION = 'relation';
@@ -253,6 +254,72 @@ class NoteShort {
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) {
if (!type && !name) {
return attributes;
@@ -542,7 +609,7 @@ class NoteShort {
});
}
hasAncestor(ancestorNote, visitedNoteIds) {
hasAncestor(ancestorNote, visitedNoteIds = null) {
if (this.noteId === ancestorNote.noteId) {
return true;
}

View File

@@ -12,6 +12,7 @@ import keyboardActionsService from "./keyboard_actions.js";
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
import MainTreeExecutors from "./main_tree_executors.js";
import protectedSessionHolder from "./protected_session_holder.js";
import toast from "./toast.js";
class AppContext extends Component {
constructor(isMainWindow) {
@@ -19,6 +20,7 @@ class AppContext extends Component {
this.isMainWindow = isMainWindow;
this.executors = [];
this.beforeUnloadListeners = [];
}
setLayout(layout) {
@@ -104,6 +106,15 @@ class AppContext extends Component {
getComponentByEl(el) {
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);
@@ -112,7 +123,29 @@ const appContext = new AppContext(window.glob.isMainWindow);
$(window).on('beforeunload', () => {
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() {

View File

@@ -125,7 +125,7 @@ async function deleteNotes(branchIdsToDelete) {
}
async function moveNodeUpInHierarchy(node) {
if (hoistedNoteService.isRootNode(node)
if (hoistedNoteService.isHoistedNode(node)
|| hoistedNoteService.isTopLevelNode(node)
|| node.getParent().data.noteType === 'search') {
return;

View File

@@ -182,8 +182,6 @@ export default class Entrypoints extends Component {
utils.reloadApp();
}
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
async openInWindowCommand({notePath, hoistedNoteId}) {
if (!hoistedNoteId) {
hoistedNoteId = 'root';

View File

@@ -16,19 +16,17 @@ async function unhoist() {
}
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
return node.data.noteId === "root"
|| node.data.noteId === getHoistedNoteId();
}
async function checkNoteAccess(notePath, tabContext) {
// notePath argument can contain only noteId which is not good when hoisted since
// then we need to check the whole note path
const resolvedNotePath = await treeService.resolveNotePath(notePath);
const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId);
if (!resolvedNotePath) {
console.log("Cannot activate " + notePath);
@@ -37,7 +35,7 @@ async function checkNoteAccess(notePath, tabContext) {
const hoistedNoteId = tabContext.hoistedNoteId;
if (hoistedNoteId !== 'root' && !resolvedNotePath.includes(hoistedNoteId)) {
if (!resolvedNotePath.includes(hoistedNoteId)) {
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?")) {
@@ -55,6 +53,6 @@ export default {
getHoistedNoteId,
unhoist,
isTopLevelNode,
isRootNode,
isHoistedNode,
checkNoteAccess
}

View File

@@ -117,7 +117,8 @@ export default class LinkMap {
const $noteBox = $("<div>")
.addClass("note-box")
.prop("id", noteBoxId);
.prop("id", noteBoxId)
.addClass(note.getCssClass());
const $link = $linkTitles[noteId];

View File

@@ -27,28 +27,28 @@ export default class MainTreeExecutors extends Component {
}
async createNoteIntoCommand() {
const activeNote = appContext.tabManager.getActiveTabNote();
const activeTabContext = appContext.tabManager.getActiveTabContext();
if (!activeNote) {
if (!activeTabContext) {
return;
}
await noteCreateService.createNote(activeNote.noteId, {
isProtected: activeNote.isProtected,
await noteCreateService.createNote(activeTabContext.notePath, {
isProtected: activeTabContext.note.isProtected,
saveSelection: false
});
}
async createNoteAfterCommand() {
const node = this.tree.getActiveNode();
const parentNoteId = node.data.parentNoteId;
const parentNotePath = treeService.getNotePath(node.getParent());
const isProtected = await treeService.getParentProtectedStatus(node);
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
return;
}
await noteCreateService.createNote(parentNoteId, {
await noteCreateService.createNote(parentNotePath, {
target: 'after',
targetBranchId: node.data.branchId,
isProtected: isProtected,

View File

@@ -1,19 +1,13 @@
import hoistedNoteService from "./hoisted_note.js";
import appContext from "./app_context.js";
import utils from "./utils.js";
import protectedSessionHolder from "./protected_session_holder.js";
import server from "./server.js";
import ws from "./ws.js";
import treeCache from "./tree_cache.js";
import treeService from "./tree.js";
import toastService from "./toast.js";
async function createNewTopLevelNote() {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
await createNote(hoistedNoteId);
}
async function createNote(parentNoteId, options = {}) {
async function createNote(parentNotePath, options = {}) {
options = Object.assign({
activate: true,
focus: 'title',
@@ -36,6 +30,8 @@ async function createNote(parentNoteId, options = {}) {
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}`, {
title: newNoteName,
content: options.content || "",
@@ -53,7 +49,7 @@ async function createNote(parentNoteId, options = {}) {
if (options.activate) {
const activeTabContext = appContext.tabManager.getActiveTabContext();
await activeTabContext.setNote(note.noteId);
await activeTabContext.setNote(`${parentNotePath}/${note.noteId}`);
if (options.focus === 'title') {
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}`);
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.activateOrOpenNote(note.noteId);
await appContext.tabManager.activateOrOpenNote(`${parentNotePath}/${note.noteId}`);
const origNote = await treeCache.getNote(noteId);
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
@@ -101,6 +98,5 @@ async function duplicateSubtree(noteId, parentNoteId) {
export default {
createNote,
createNewTopLevelNote,
duplicateSubtree
};

View File

@@ -15,11 +15,27 @@ export default class SpacedUpdate {
async updateNowIfNecessary() {
if (this.changed) {
this.changed = false;
await this.updater();
this.changed = false; // optimistic...
try {
await this.updater();
}
catch (e) {
this.changed = true;
throw e;
}
}
}
isAllSavedAndTriggerUpdate() {
const allSaved = !this.changed;
this.updateNowIfNecessary();
return allSaved;
}
triggerUpdate() {
if (!this.changed) {
return;

View File

@@ -79,7 +79,7 @@ class TabContext extends Component {
return inputNotePath;
}
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath);
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId);
if (!resolvedNotePath) {
logError(`Cannot resolve note path ${inputNotePath}`);

View File

@@ -27,6 +27,8 @@ export default class TabManager extends Component {
openTabs: JSON.stringify(openTabs)
});
});
appContext.addBeforeUnloadListener(this);
}
/** @type {TabContext[]} */
@@ -203,7 +205,7 @@ export default class TabManager extends Component {
let hoistedNoteId = 'root';
if (tabContext) {
const resolvedNotePath = await treeService.resolveNotePath(notePath);
const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId);
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
hoistedNoteId = tabContext.hoistedNoteId;
@@ -329,6 +331,8 @@ export default class TabManager extends Component {
beforeUnloadEvent() {
this.tabsUpdate.updateNowIfNecessary();
return true; // don't block closing the tab, this metadata is not that important
}
openNewTabCommand() {

View File

@@ -8,8 +8,8 @@ import appContext from "./app_context.js";
/**
* @return {string|null}
*/
async function resolveNotePath(notePath) {
const runPath = await resolveNotePathToSegments(notePath);
async function resolveNotePath(notePath, hoistedNoteId = 'root') {
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
return runPath ? runPath.join("/") : null;
}
@@ -21,7 +21,7 @@ async function resolveNotePath(notePath) {
*
* @return {string[]}
*/
async function resolveNotePathToSegments(notePath, logErrors = true) {
async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logErrors = true) {
utils.assertArguments(notePath);
// 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');
}
const effectivePath = [];
const effectivePathSegments = [];
let childNoteId = null;
let i = 0;
@@ -75,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})`)}`);
}
const someNotePath = getSomeNotePath(parents[0]);
const someNotePath = getSomeNotePath(child, hoistedNoteId);
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) {
effectivePath.push(noteId);
effectivePathSegments.push(noteId);
}
}
@@ -89,36 +89,37 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
}
}
effectivePath.push(parentNoteId);
effectivePathSegments.push(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);
const path = [];
const notePaths = note.getSortedNotePaths(hoistedNotePath);
let cur = note;
return notePaths[0].notePath;
}
while (cur.noteId !== 'root') {
path.push(cur.noteId);
function getSomeNotePath(note, hoistedNotePath = 'root') {
const notePath = getSomeNotePathSegments(note, hoistedNotePath);
const parents = cur.getParentNotes().filter(note => note.type !== 'search');
if (!parents.length) {
logError(`Can't find parents for note ${cur.noteId}`);
return;
}
cur = parents[0];
}
path.push('root');
return path.reverse().join('/');
return notePath.join('/');
}
async function sortAlphabetically(noteId) {
@@ -138,7 +139,7 @@ ws.subscribeToMessages(message => {
});
function getParentProtectedStatus(node) {
return hoistedNoteService.isRootNode(node) ? 0 : node.getParent().data.isProtected;
return hoistedNoteService.isHoistedNode(node) ? 0 : node.getParent().data.isProtected;
}
function getNoteIdFromNotePath(notePath) {
@@ -198,7 +199,7 @@ function getNotePath(node) {
const path = [];
while (node && !hoistedNoteService.isRootNode(node)) {
while (node) {
if (node.data.noteId) {
path.push(node.data.noteId);
}
@@ -206,10 +207,6 @@ function getNotePath(node) {
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("/");
}
@@ -313,6 +310,7 @@ export default {
resolveNotePath,
resolveNotePathToSegments,
getSomeNotePath,
getSomeNotePathSegments,
getParentProtectedStatus,
getNotePath,
getNoteIdFromNotePath,

View File

@@ -75,7 +75,7 @@ class TreeContextMenu {
{ 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: "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: "----" },
@@ -112,10 +112,10 @@ class TreeContextMenu {
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
}
else if (command === "insertNoteAfter") {
const parentNoteId = this.node.data.parentNoteId;
const parentNotePath = treeService.getNotePath(this.node.getParent());
const isProtected = await treeService.getParentProtectedStatus(this.node);
noteCreateService.createNote(parentNoteId, {
noteCreateService.createNote(parentNotePath, {
target: 'after',
targetBranchId: this.node.data.branchId,
type: type,
@@ -123,14 +123,14 @@ class TreeContextMenu {
});
}
else if (command === "insertChildNote") {
noteCreateService.createNote(noteId, {
const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
type: type,
isProtected: this.node.data.isProtected
});
}
else {
console.log("Triggering", command, notePath);
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
}
}

View File

@@ -491,7 +491,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
}
async createNoteForReferenceLink(title) {
const {note} = await noteCreateService.createNote(this.noteId, {
const {note} = await noteCreateService.createNote(this.notePath, {
activate: false,
title: title
});

View File

@@ -26,7 +26,7 @@ class MobileDetailMenuWidget extends BasicWidget {
],
selectMenuItemHandler: async ({command}) => {
if (command === "insertChildNote") {
noteCreateService.createNote(note.noteId);
noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath());
}
else if (command === "delete") {
const notePath = appContext.tabManager.getActiveTabNotePath();

View File

@@ -13,7 +13,7 @@ const WIDGET_TPL = `
}
</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>

View File

@@ -65,6 +65,8 @@ export default class NoteDetailWidget extends TabAwareWidget {
await server.put('notes/' + noteId, dto, this.componentId);
});
appContext.addBeforeUnloadListener(this);
}
isEnabled() {
@@ -293,7 +295,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
}
beforeUnloadEvent() {
this.spacedUpdate.updateNowIfNecessary();
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
}
textPreviewDisabledEvent({tabContext}) {
@@ -316,7 +318,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
}
// without await as this otherwise causes deadlock through component mutex
noteCreateService.createNote(note.noteId, {
noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath(), {
isProtected: note.isProtected,
saveSelection: true
});

View File

@@ -16,6 +16,7 @@ const TPL = `
border: 1px solid transparent;
cursor: pointer;
padding: 6px;
color: var(--main-text-color);
}
.note-icon-container button.note-icon:hover {

View File

@@ -15,9 +15,21 @@ const TPL = `
}
.note-path-list {
max-height: 600px;
max-height: 700px;
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>
<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') {
await this.addPath('root', true);
await this.addPath('root');
return;
}
const pathSegments = treeService.parseNotePath(this.notePath);
const activeNoteParentNoteId = pathSegments[pathSegments.length - 2]; // we know this is not root so there must be a parent
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);
for (const notePathRecord of this.note.getSortedNotePaths(this.hoistedNoteId)) {
await this.addPath(notePathRecord);
}
const cloneLink = $("<div>")
@@ -70,7 +74,9 @@ export default class NotePathsWidget extends TabAwareWidget {
this.$notePathList.append(cloneLink);
}
async addPath(notePath, isCurrent) {
async addPath(notePathRecord) {
const notePath = notePathRecord.notePath.join('/');
const title = await treeService.getNotePathTitle(notePath);
const $noteLink = await linkService.createNoteLink(notePath, {title});
@@ -82,8 +88,33 @@ export default class NotePathsWidget extends TabAwareWidget {
.find('a')
.addClass("no-tooltip-preview");
if (isCurrent) {
$noteLink.addClass("current");
const icons = [];
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);
@@ -96,4 +127,10 @@ export default class NotePathsWidget extends TabAwareWidget {
this.refresh();
}
}
async refresh() {
await super.refresh();
this.$widget.find('.dropdown-toggle').dropdown('hide');
}
}

View File

@@ -3,6 +3,7 @@ import utils from "../services/utils.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import SpacedUpdate from "../services/spaced_update.js";
import appContext from "../services/app_context.js";
const TPL = `
<div class="note-title-container">
@@ -37,6 +38,8 @@ export default class NoteTitleWidget extends TabAwareWidget {
await server.put(`notes/${this.noteId}/change-title`, {title});
});
appContext.addBeforeUnloadListener(this);
}
doRender() {
@@ -101,6 +104,6 @@ export default class NoteTitleWidget extends TabAwareWidget {
}
beforeUnloadEvent() {
this.spacedUpdate.updateNowIfNecessary();
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
}
}

View File

@@ -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", ".add-note-button", e => {
const node = $.ui.fancytree.getNode(e);
const parentNotePath = treeService.getNotePath(node);
noteCreateService.createNote(node.data.noteId, {
noteCreateService.createNote(parentNotePath, {
isProtected: node.data.isProtected
});
});
@@ -594,7 +595,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
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);
}
@@ -804,12 +805,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
/** @return {FancytreeNode} */
async getNodeFromPath(notePath, expand = false, logErrors = true) {
utils.assertArguments(notePath);
/** @let {FancytreeNode} */
let parentNode = this.getNodesByNoteId('root')[0];
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
/** @const {FancytreeNode} */
let parentNode = null;
const resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, logErrors);
let resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, this.hoistedNoteId, logErrors);
if (!resolvedNotePathSegments) {
if (logErrors) {
@@ -819,14 +818,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
return;
}
resolvedNotePathSegments = resolvedNotePathSegments.slice(1);
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
if (parentNode) {
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
// 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;
@@ -999,6 +993,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const activeNodeFocused = activeNode && activeNode.hasFocus();
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
const activeNoteId = activeNode ? activeNode.data.noteId : null;
@@ -1119,7 +1114,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (node) {
node.setActive(true, {noEvents: true, noFocus: true});
node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
if (activeNodeFocused) {
node.setFocus(true);
}
}
else {
// this is used when original note has been deleted and we want to move the focus to the note above/below
@@ -1131,16 +1130,16 @@ export default class NoteTreeWidget extends TabAwareWidget {
// 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);
appContext.tabManager.getActiveTabContext().setNote(nextNotePath).then(() => {
const newActiveNode = this.getActiveNode();
// return focus if the previously active node was also focused
if (newActiveNode && activeNodeFocused) {console.log("FOCUSING!!!");
newActiveNode.setFocus(true);
}
});
}
}
const newActiveNode = this.getActiveNode();
// return focus if the previously active node was also focused
if (newActiveNode && activeNodeFocused) {
await newActiveNode.setFocus(true);
}
}
if (noteIdsToReload.size > 0 || noteIdsToUpdate.size > 0) {
@@ -1207,13 +1206,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
}
filterHoistedBranch() {
async filterHoistedBranch() {
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') {
this.tree.clearFilter();
}
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);
}
}
}
@@ -1370,7 +1376,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
sortChildNotesCommand({node}) {
treeService.sortAlphabetically(node.data.noteId);
import("../dialogs/sort_child_notes.js").then(d => d.showDialog(node.data.noteId));
}
async recentChangesInSubtreeCommand({node}) {

View File

@@ -49,7 +49,11 @@ export default class QuickSearchWidget extends BasicWidget {
this.$widget.find('.input-group-append').on('shown.bs.dropdown', () => this.search());
utils.bindElShortcut(this.$searchString, 'return', () => {
this.$dropdownToggle.dropdown('show');
if (this.$dropdownMenu.is(":visible")) {
this.search(); // just update already visible dropdown
} else {
this.$dropdownToggle.dropdown('show');
}
this.$searchString.focus();
});

View File

@@ -22,6 +22,10 @@ export default class TabAwareWidget extends BasicWidget {
return this.tabContext && this.tabContext.notePath;
}
get hoistedNoteId() {
return this.tabContext && this.tabContext.hoistedNoteId;
}
isEnabled() {
return !!this.note;
}

View File

@@ -83,4 +83,10 @@ export default class InheritedAttributesWidget extends TabAwareWidget {
getInheritedAttributes(note) {
return note.getAttributes().filter(attr => attr.noteId !== this.noteId);
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
this.refresh();
}
}
}

View File

@@ -49,15 +49,16 @@ const TPL = `
}
.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::before { content: "###\\2004"; color: var(--muted-text-color); }
.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::before { content: "#####\\2004"; color: var(--muted-text-color); }
.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 {
padding-top: 10px;
@@ -274,7 +275,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}
async createNoteForReferenceLink(title) {
const {note} = await noteCreateService.createNote(this.noteId, {
const {note} = await noteCreateService.createNote(this.notePath, {
activate: false,
title: title
});

View File

@@ -14,16 +14,11 @@ const TPL = `
.note-detail-readonly-text h5 { font-size: 1.2em; }
.note-detail-readonly-text h6 { font-size: 1.1em; }
.note-detail-readonly-text h2 { font-size: 1.8em; }
.note-detail-readonly-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h3 { font-size: 1.6em; }
.note-detail-readonly-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text h4 { font-size: 1.4em; }
.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); }
body.heading-style-markdown .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); }
body.heading-style-markdown .note-detail-readonly-text h4:not(.include-note-title)::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); }
body.heading-style-markdown .note-detail-readonly-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
.note-detail-readonly-text {
padding-left: 22px;

View File

@@ -7,6 +7,7 @@ import attributeAutocompleteService from "../../services/attribute_autocomplete.
import TypeWidget from "./type_widget.js";
import appContext from "../../services/app_context.js";
import utils from "../../services/utils.js";
import treeCache from "../../services/tree_cache.js";
const uniDirectionalOverlays = [
[ "Arrow", {
@@ -531,8 +532,11 @@ export default class RelationMapTypeWidget extends TypeWidget {
linkService.goToLink(e);
});
const note = await treeCache.getNote(noteId);
const $noteBox = $("<div>")
.addClass("note-box")
.addClass(note.getCssClass())
.prop("id", this.noteIdToId(noteId))
.append($("<span>").addClass("title").append($link))
.append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note."))

View File

@@ -5,6 +5,8 @@ const sql = require('../../services/sql');
const dateUtils = require('../../services/date_utils');
const noteService = require('../../services/notes');
const attributeService = require('../../services/attributes');
const cls = require('../../services/cls');
const repository = require('../../services/repository');
function getInboxNote(req) {
return attributeService.getNoteWithLabel('inbox')
@@ -62,12 +64,36 @@ function createSqlConsole() {
function createSearchNote(req) {
const params = req.body;
const searchString = params.searchString || "";
let ancestorNoteId = params.ancestorNoteId;
const today = dateUtils.localNowDate();
const hoistedNote = cls.getHoistedNoteId() && cls.getHoistedNoteId() !== 'root'
? repository.getNote(cls.getHoistedNoteId())
: null;
const searchHome =
attributeService.getNoteWithLabel('searchHome')
|| dateNoteService.getDateNote(today);
let searchHome;
if (hoistedNote) {
([searchHome] = hoistedNote.getDescendantNotesWithLabel('hoistedSearchHome'));
}
if (!searchHome) {
const today = dateUtils.localNowDate();
searchHome = attributeService.getNoteWithLabel('searchHome')
|| 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({
parentNoteId: searchHome.noteId,
@@ -79,8 +105,8 @@ function createSearchNote(req) {
note.setLabel('searchString', searchString);
if (params.ancestorNoteId) {
note.setRelation('ancestor', params.ancestorNoteId);
if (ancestorNoteId) {
note.setRelation('ancestor', ancestorNoteId);
}
return note;

View File

@@ -4,6 +4,7 @@ const noteService = require('../../services/notes');
const treeService = require('../../services/tree');
const repository = require('../../services/repository');
const utils = require('../../services/utils');
const log = require('../../services/log');
const TaskContext = require('../../services/task_context');
function getNote(req) {
@@ -85,10 +86,20 @@ function undeleteNote(req) {
taskContext.taskSucceeded();
}
function sortNotes(req) {
function sortChildNotes(req) {
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) {
@@ -215,7 +226,7 @@ module.exports = {
deleteNote,
undeleteNote,
createNote,
sortNotes,
sortChildNotes,
protectNote,
setNoteTypeMime,
getRelationMap,

View File

@@ -40,7 +40,8 @@ const ALLOWED_OPTIONS = new Set([
'nativeTitleBarVisible',
'attributeListExpanded',
'promotedAttributesExpanded',
'similarNotesExpanded'
'similarNotesExpanded',
'headingStyle'
]);
function getOptions() {

View File

@@ -19,6 +19,7 @@ function index(req, res) {
res.render(view, {
csrfToken: csrfToken,
theme: options.theme,
headingStyle: options.headingStyle,
mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize),

View File

@@ -150,7 +150,7 @@ function register(app) {
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote);
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\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);

View File

@@ -38,6 +38,7 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'label', name: 'workspaceIconClass' },
{ type: 'label', name: 'workspaceTabBackgroundColor' },
{ type: 'label', name: 'searchHome' },
{ type: 'label', name: 'hoistedSearchHome' },
{ type: 'label', name: 'sqlConsoleHome' },
{ type: 'label', name: 'datePattern' },

View File

@@ -359,7 +359,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @method
* @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

View File

@@ -1 +1 @@
module.exports = { buildDate:"2021-02-25T22:41:35+01:00", buildRevision: "cde41b268e4e88b3fe3601d9d19b3f5241625ada" };
module.exports = { buildDate:"2021-03-10T23:35:12+01:00", buildRevision: "6f901e6852c33ba0dae6c70efb9f65e5b0028995" };

View File

@@ -30,6 +30,23 @@ function addEntityChange(entityChange, sourceId, isSynced) {
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) {
const [hash, isSynced] = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]);
@@ -121,13 +138,7 @@ function fillAllEntityChanges() {
}
module.exports = {
addNoteReorderingEntityChange: (parentNoteId, sourceId) => addEntityChange({
entityName: "note_reordering",
entityId: parentNoteId,
hash: 'N/A',
isErased: false,
utcDateChanged: dateUtils.utcNowDateTime()
}, sourceId),
addNoteReorderingEntityChange,
moveEntityChangeToTop,
addEntityChange,
fillAllEntityChanges,

View File

@@ -31,7 +31,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
for (const parentNote of noteFromCache.parents) {
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') {
const note = repository.getNote(entity.noteId);
if (!["text", "code"].includes(note.type)) {
return;
}
const content = note.getContent();
if (content && content.trim().length > 0) {
return;
}
const templateNote = repository.getNote(entity.value);
if (!templateNote) {
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();
if (templateNoteContent) {
@@ -81,17 +77,21 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
note.save();
}
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
// 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);
}
}
else if (entity.type === 'label' && entity.name === 'sorted') {
treeService.sortNotesAlphabetically(entity.noteId);
treeService.sortNotesByTitle(entity.noteId);
if (entity.isInheritable) {
const note = noteCache.notes[entity.noteId];
if (note) {
for (const noteId of note.subtreeNoteIds) {
treeService.sortNotesAlphabetically(noteId);
treeService.sortNotesByTitle(noteId);
}
}
}

View File

@@ -463,7 +463,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
if (!metaFile) {
// 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
treeService.sortNotesAlphabetically(noteId, true);
treeService.sortNotesByTitle(noteId, true);
}
taskContext.increaseProgressCount();

View File

@@ -120,7 +120,11 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
delete noteCache.attributes[attributeId];
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) {
@@ -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, () => {

View File

@@ -84,7 +84,8 @@ const defaultOptions = [
{ name: 'attributeListExpanded', value: 'false', isSynced: false },
{ name: 'promotedAttributesExpanded', 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() {

View File

@@ -31,7 +31,7 @@ function insert(tableName, rec, replace = false) {
const res = execute(query, Object.values(rec));
return res.lastInsertRowid;
return res ? res.lastInsertRowid : null;
}
function replace(tableName, rec) {

View File

@@ -106,7 +106,7 @@ function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) {
}
}
function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
function sortNotesByTitle(parentNoteId, foldersFirst = false, reverse = false) {
sql.transactional(() => {
const notes = sql.getRows(
`SELECT branches.branchId, notes.noteId, title, isProtected,
@@ -120,7 +120,7 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
protectedSessionService.decryptNotes(notes);
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
return a.hasChildren ? -1 : 1;
}
@@ -129,6 +129,10 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
}
});
if (reverse) {
notes.reverse();
}
let position = 10;
for (const note of notes) {
@@ -144,6 +148,33 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
});
}
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;
}
entityChangesService.addNoteReorderingEntityChange(parentNoteId);
});
}
/**
* @deprecated - this will be removed in the future
*/
@@ -194,6 +225,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
module.exports = {
getNotes,
validateParentChild,
sortNotesAlphabetically,
sortNotesByTitle,
sortNotes,
setNoteToParent
};

View File

@@ -5,7 +5,7 @@
<link rel="shortcut icon" href="favicon.ico">
<title>Trilium Notes</title>
</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>
<script>
@@ -39,6 +39,7 @@
<%- include('dialogs/move_to.ejs') %>
<%- include('dialogs/backend_log.ejs') %>
<%- include('dialogs/include_note.ejs') %>
<%- include('dialogs/sort_child_notes.ejs') %>
<script type="text/javascript">
window.baseApiUrl = 'api/';

View 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">&times;</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>

View File

@@ -95,7 +95,7 @@
}
</style>
</head>
<body class="mobile theme-<%= theme %>">
<body class="mobile theme-<%= theme %> heading-style-<%= headingStyle %>">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>