mirror of
https://github.com/zadam/trilium.git
synced 2025-10-29 17:26:38 +01:00
Compare commits
39 Commits
v0.46.2-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 | ||
|
|
1c59bc4d3c |
@@ -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
923
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -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": {
|
||||
|
||||
@@ -41,7 +41,7 @@ class Entity {
|
||||
}
|
||||
|
||||
getUtcDateChanged() {
|
||||
return this.utcDateModified;
|
||||
return this.utcDateModified || this.utcDateCreated;
|
||||
}
|
||||
|
||||
get repository() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -182,8 +182,6 @@ export default class Entrypoints extends Component {
|
||||
utils.reloadApp();
|
||||
}
|
||||
|
||||
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
|
||||
|
||||
async openInWindowCommand({notePath, hoistedNoteId}) {
|
||||
if (!hoistedNoteId) {
|
||||
hoistedNoteId = 'root';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,7 +40,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'nativeTitleBarVisible',
|
||||
'attributeListExpanded',
|
||||
'promotedAttributesExpanded',
|
||||
'similarNotesExpanded'
|
||||
'similarNotesExpanded',
|
||||
'headingStyle'
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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' },
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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/';
|
||||
|
||||
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>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user