mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 19:05:59 +01:00
Compare commits
32 Commits
v0.46.2-be
...
v0.46.3-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.
|
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
|
||||||
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
|
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
|
||||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
|
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|||||||
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",
|
"name": "trilium",
|
||||||
"productName": "Trilium Notes",
|
"productName": "Trilium Notes",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.46.2-beta",
|
"version": "0.46.3-beta",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "electron.js",
|
"main": "electron.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"dayjs": "1.10.4",
|
"dayjs": "1.10.4",
|
||||||
"ejs": "3.1.6",
|
"ejs": "3.1.6",
|
||||||
"electron-debug": "3.2.0",
|
"electron-debug": "3.2.0",
|
||||||
"electron-dl": "3.1.0",
|
"electron-dl": "3.2.0",
|
||||||
"electron-find": "1.0.6",
|
"electron-find": "1.0.6",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
@@ -51,10 +51,10 @@
|
|||||||
"is-animated": "^2.0.1",
|
"is-animated": "^2.0.1",
|
||||||
"is-svg": "4.2.1",
|
"is-svg": "4.2.1",
|
||||||
"jimp": "0.16.1",
|
"jimp": "0.16.1",
|
||||||
"jsdom": "^16.4.0",
|
"jsdom": "16.5.0",
|
||||||
"mime-types": "2.1.29",
|
"mime-types": "2.1.29",
|
||||||
"multer": "1.4.2",
|
"multer": "1.4.2",
|
||||||
"node-abi": "2.19.3",
|
"node-abi": "2.21.0",
|
||||||
"open": "7.4.2",
|
"open": "7.4.2",
|
||||||
"portscanner": "2.2.0",
|
"portscanner": "2.2.0",
|
||||||
"rand-token": "1.0.1",
|
"rand-token": "1.0.1",
|
||||||
@@ -72,14 +72,14 @@
|
|||||||
"turndown": "7.0.0",
|
"turndown": "7.0.0",
|
||||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||||
"unescape": "1.0.1",
|
"unescape": "1.0.1",
|
||||||
"ws": "7.4.3",
|
"ws": "7.4.4",
|
||||||
"yauzl": "2.10.0",
|
"yauzl": "2.10.0",
|
||||||
"yazl": "2.5.1"
|
"yazl": "2.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"electron": "9.4.3",
|
"electron": "9.4.4",
|
||||||
"electron-builder": "22.9.1",
|
"electron-builder": "22.10.5",
|
||||||
"electron-packager": "15.2.0",
|
"electron-packager": "15.2.0",
|
||||||
"electron-rebuild": "2.3.5",
|
"electron-rebuild": "2.3.5",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"jsdoc": "3.6.6",
|
"jsdoc": "3.6.6",
|
||||||
"lorem-ipsum": "2.0.3",
|
"lorem-ipsum": "2.0.3",
|
||||||
"rcedit": "3.0.0",
|
"rcedit": "3.0.0",
|
||||||
"webpack": "5.24.2",
|
"webpack": "5.24.4",
|
||||||
"webpack-cli": "4.5.0"
|
"webpack-cli": "4.5.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUtcDateChanged() {
|
getUtcDateChanged() {
|
||||||
return this.utcDateModified;
|
return this.utcDateModified || this.utcDateCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
get repository() {
|
get repository() {
|
||||||
|
|||||||
24
src/public/app/dialogs/sort_child_notes.js
Normal file
24
src/public/app/dialogs/sort_child_notes.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import server from "../services/server.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
|
const $dialog = $("#sort-child-notes-dialog");
|
||||||
|
const $form = $("#sort-child-notes-form");
|
||||||
|
|
||||||
|
let parentNoteId = null;
|
||||||
|
|
||||||
|
$form.on('submit', async () => {
|
||||||
|
const sortBy = $form.find("input[name='sort-by']:checked").val();
|
||||||
|
const sortDirection = $form.find("input[name='sort-direction']:checked").val();
|
||||||
|
|
||||||
|
await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection});
|
||||||
|
|
||||||
|
utils.closeActiveDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function showDialog(noteId) {
|
||||||
|
parentNoteId = noteId;
|
||||||
|
|
||||||
|
utils.openDialog($dialog);
|
||||||
|
|
||||||
|
$form.find('input:first').focus();
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import server from '../services/server.js';
|
|||||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
import ws from "../services/ws.js";
|
import ws from "../services/ws.js";
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
|
import treeCache from "../services/tree_cache.js";
|
||||||
|
|
||||||
const LABEL = 'label';
|
const LABEL = 'label';
|
||||||
const RELATION = 'relation';
|
const RELATION = 'relation';
|
||||||
@@ -253,6 +254,55 @@ class NoteShort {
|
|||||||
return noteAttributeCache.attributes[this.noteId];
|
return noteAttributeCache.attributes[this.noteId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllNotePaths() {
|
||||||
|
if (this.noteId === 'root') {
|
||||||
|
return [['root']];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentNotes = this.getParentNotes();
|
||||||
|
let paths;
|
||||||
|
|
||||||
|
if (parentNotes.length === 1) { // optimization for the most common case
|
||||||
|
paths = parentNotes[0].getAllNotePaths();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
paths = [];
|
||||||
|
|
||||||
|
for (const parentNote of parentNotes) {
|
||||||
|
paths.push(...parentNote.getAllNotePaths());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
path.push(this.noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortedNotePaths(hoistedNotePath = 'root') {
|
||||||
|
const notePaths = this.getAllNotePaths().map(path => ({
|
||||||
|
notePath: path,
|
||||||
|
isInHoistedSubTree: path.includes(hoistedNotePath),
|
||||||
|
isArchived: path.find(noteId => treeCache.notes[noteId].hasLabel('archived')),
|
||||||
|
isSearch: path.find(noteId => treeCache.notes[noteId].type === 'search')
|
||||||
|
}));
|
||||||
|
|
||||||
|
notePaths.sort((a, b) => {
|
||||||
|
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
|
||||||
|
return a.isInHoistedSubTree ? -1 : 1;
|
||||||
|
} else if (a.isSearch !== b.isSearch) {
|
||||||
|
return a.isSearch ? 1 : -1;
|
||||||
|
} else if (a.isArchived !== b.isArchived) {
|
||||||
|
return a.isArchived ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return a.notePath.length - b.notePath.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return notePaths;
|
||||||
|
}
|
||||||
|
|
||||||
__filterAttrs(attributes, type, name) {
|
__filterAttrs(attributes, type, name) {
|
||||||
if (!type && !name) {
|
if (!type && !name) {
|
||||||
return attributes;
|
return attributes;
|
||||||
@@ -542,7 +592,7 @@ class NoteShort {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAncestor(ancestorNote, visitedNoteIds) {
|
hasAncestor(ancestorNote, visitedNoteIds = null) {
|
||||||
if (this.noteId === ancestorNote.noteId) {
|
if (this.noteId === ancestorNote.noteId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import keyboardActionsService from "./keyboard_actions.js";
|
|||||||
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
|
import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js";
|
||||||
import MainTreeExecutors from "./main_tree_executors.js";
|
import MainTreeExecutors from "./main_tree_executors.js";
|
||||||
import protectedSessionHolder from "./protected_session_holder.js";
|
import protectedSessionHolder from "./protected_session_holder.js";
|
||||||
|
import toast from "./toast.js";
|
||||||
|
|
||||||
class AppContext extends Component {
|
class AppContext extends Component {
|
||||||
constructor(isMainWindow) {
|
constructor(isMainWindow) {
|
||||||
@@ -19,6 +20,7 @@ class AppContext extends Component {
|
|||||||
|
|
||||||
this.isMainWindow = isMainWindow;
|
this.isMainWindow = isMainWindow;
|
||||||
this.executors = [];
|
this.executors = [];
|
||||||
|
this.beforeUnloadListeners = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
setLayout(layout) {
|
setLayout(layout) {
|
||||||
@@ -104,6 +106,15 @@ class AppContext extends Component {
|
|||||||
getComponentByEl(el) {
|
getComponentByEl(el) {
|
||||||
return $(el).closest(".component").prop('component');
|
return $(el).closest(".component").prop('component');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addBeforeUnloadListener(obj) {
|
||||||
|
if (typeof WeakRef !== "function") {
|
||||||
|
// older browsers don't support WeakRef
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.beforeUnloadListeners.push(new WeakRef(obj));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const appContext = new AppContext(window.glob.isMainWindow);
|
const appContext = new AppContext(window.glob.isMainWindow);
|
||||||
@@ -112,7 +123,29 @@ const appContext = new AppContext(window.glob.isMainWindow);
|
|||||||
$(window).on('beforeunload', () => {
|
$(window).on('beforeunload', () => {
|
||||||
protectedSessionHolder.resetSessionCookie();
|
protectedSessionHolder.resetSessionCookie();
|
||||||
|
|
||||||
appContext.triggerEvent('beforeUnload');
|
let allSaved = true;
|
||||||
|
|
||||||
|
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());
|
||||||
|
|
||||||
|
for (const weakRef of appContext.beforeUnloadListeners) {
|
||||||
|
const component = weakRef.deref();
|
||||||
|
|
||||||
|
if (!component) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component.beforeUnloadEvent()) {
|
||||||
|
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||||
|
|
||||||
|
toast.showMessage("Please wait for a couple of seconds for the save to finish, then you can try again.", 10000);
|
||||||
|
|
||||||
|
allSaved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allSaved) {
|
||||||
|
return "some string";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function isNotePathInAddress() {
|
function isNotePathInAddress() {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ async function deleteNotes(branchIdsToDelete) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function moveNodeUpInHierarchy(node) {
|
async function moveNodeUpInHierarchy(node) {
|
||||||
if (hoistedNoteService.isRootNode(node)
|
if (hoistedNoteService.isHoistedNode(node)
|
||||||
|| hoistedNoteService.isTopLevelNode(node)
|
|| hoistedNoteService.isTopLevelNode(node)
|
||||||
|| node.getParent().data.noteType === 'search') {
|
|| node.getParent().data.noteType === 'search') {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ export default class Entrypoints extends Component {
|
|||||||
utils.reloadApp();
|
utils.reloadApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
|
|
||||||
|
|
||||||
async openInWindowCommand({notePath, hoistedNoteId}) {
|
async openInWindowCommand({notePath, hoistedNoteId}) {
|
||||||
if (!hoistedNoteId) {
|
if (!hoistedNoteId) {
|
||||||
hoistedNoteId = 'root';
|
hoistedNoteId = 'root';
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ async function unhoist() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isTopLevelNode(node) {
|
function isTopLevelNode(node) {
|
||||||
return isRootNode(node.getParent());
|
return isHoistedNode(node.getParent());
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRootNode(node) {
|
function isHoistedNode(node) {
|
||||||
// even though check for 'root' should not be necessary, we keep it just in case
|
// even though check for 'root' should not be necessary, we keep it just in case
|
||||||
return node.data.noteId === "root"
|
return node.data.noteId === "root"
|
||||||
|| node.data.noteId === getHoistedNoteId();
|
|| node.data.noteId === getHoistedNoteId();
|
||||||
@@ -55,6 +55,6 @@ export default {
|
|||||||
getHoistedNoteId,
|
getHoistedNoteId,
|
||||||
unhoist,
|
unhoist,
|
||||||
isTopLevelNode,
|
isTopLevelNode,
|
||||||
isRootNode,
|
isHoistedNode,
|
||||||
checkNoteAccess
|
checkNoteAccess
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ export default class LinkMap {
|
|||||||
|
|
||||||
const $noteBox = $("<div>")
|
const $noteBox = $("<div>")
|
||||||
.addClass("note-box")
|
.addClass("note-box")
|
||||||
.prop("id", noteBoxId);
|
.prop("id", noteBoxId)
|
||||||
|
.addClass(note.getCssClass());
|
||||||
|
|
||||||
const $link = $linkTitles[noteId];
|
const $link = $linkTitles[noteId];
|
||||||
|
|
||||||
|
|||||||
@@ -27,28 +27,28 @@ export default class MainTreeExecutors extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNoteIntoCommand() {
|
async createNoteIntoCommand() {
|
||||||
const activeNote = appContext.tabManager.getActiveTabNote();
|
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||||
|
|
||||||
if (!activeNote) {
|
if (!activeTabContext) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(activeNote.noteId, {
|
await noteCreateService.createNote(activeTabContext.notePath, {
|
||||||
isProtected: activeNote.isProtected,
|
isProtected: activeTabContext.note.isProtected,
|
||||||
saveSelection: false
|
saveSelection: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteAfterCommand() {
|
async createNoteAfterCommand() {
|
||||||
const node = this.tree.getActiveNode();
|
const node = this.tree.getActiveNode();
|
||||||
const parentNoteId = node.data.parentNoteId;
|
const parentNotePath = treeService.getNotePath(node.getParent());
|
||||||
const isProtected = await treeService.getParentProtectedStatus(node);
|
const isProtected = await treeService.getParentProtectedStatus(node);
|
||||||
|
|
||||||
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
|
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(parentNoteId, {
|
await noteCreateService.createNote(parentNotePath, {
|
||||||
target: 'after',
|
target: 'after',
|
||||||
targetBranchId: node.data.branchId,
|
targetBranchId: node.data.branchId,
|
||||||
isProtected: isProtected,
|
isProtected: isProtected,
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import hoistedNoteService from "./hoisted_note.js";
|
|
||||||
import appContext from "./app_context.js";
|
import appContext from "./app_context.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import protectedSessionHolder from "./protected_session_holder.js";
|
import protectedSessionHolder from "./protected_session_holder.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import ws from "./ws.js";
|
import ws from "./ws.js";
|
||||||
import treeCache from "./tree_cache.js";
|
import treeCache from "./tree_cache.js";
|
||||||
|
import treeService from "./tree.js";
|
||||||
import toastService from "./toast.js";
|
import toastService from "./toast.js";
|
||||||
|
|
||||||
async function createNewTopLevelNote() {
|
async function createNote(parentNotePath, options = {}) {
|
||||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
|
||||||
|
|
||||||
await createNote(hoistedNoteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createNote(parentNoteId, options = {}) {
|
|
||||||
options = Object.assign({
|
options = Object.assign({
|
||||||
activate: true,
|
activate: true,
|
||||||
focus: 'title',
|
focus: 'title',
|
||||||
@@ -36,6 +30,8 @@ async function createNote(parentNoteId, options = {}) {
|
|||||||
|
|
||||||
const newNoteName = options.title || "new note";
|
const newNoteName = options.title || "new note";
|
||||||
|
|
||||||
|
const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath);
|
||||||
|
|
||||||
const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId}`, {
|
const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId}`, {
|
||||||
title: newNoteName,
|
title: newNoteName,
|
||||||
content: options.content || "",
|
content: options.content || "",
|
||||||
@@ -53,7 +49,7 @@ async function createNote(parentNoteId, options = {}) {
|
|||||||
|
|
||||||
if (options.activate) {
|
if (options.activate) {
|
||||||
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
const activeTabContext = appContext.tabManager.getActiveTabContext();
|
||||||
await activeTabContext.setNote(note.noteId);
|
await activeTabContext.setNote(`${parentNotePath}/${note.noteId}`);
|
||||||
|
|
||||||
if (options.focus === 'title') {
|
if (options.focus === 'title') {
|
||||||
appContext.triggerEvent('focusAndSelectTitle');
|
appContext.triggerEvent('focusAndSelectTitle');
|
||||||
@@ -88,12 +84,13 @@ function parseSelectedHtml(selectedHtml) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateSubtree(noteId, parentNoteId) {
|
async function duplicateSubtree(noteId, parentNotePath) {
|
||||||
|
const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath);
|
||||||
const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`);
|
const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`);
|
||||||
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
|
||||||
await appContext.tabManager.activateOrOpenNote(note.noteId);
|
await appContext.tabManager.activateOrOpenNote(`${parentNotePath}/${note.noteId}`);
|
||||||
|
|
||||||
const origNote = await treeCache.getNote(noteId);
|
const origNote = await treeCache.getNote(noteId);
|
||||||
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
|
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
|
||||||
@@ -101,6 +98,5 @@ async function duplicateSubtree(noteId, parentNoteId) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
createNote,
|
createNote,
|
||||||
createNewTopLevelNote,
|
|
||||||
duplicateSubtree
|
duplicateSubtree
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,11 +15,27 @@ export default class SpacedUpdate {
|
|||||||
|
|
||||||
async updateNowIfNecessary() {
|
async updateNowIfNecessary() {
|
||||||
if (this.changed) {
|
if (this.changed) {
|
||||||
this.changed = false;
|
this.changed = false; // optimistic...
|
||||||
await this.updater();
|
|
||||||
|
try {
|
||||||
|
await this.updater();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.changed = true;
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAllSavedAndTriggerUpdate() {
|
||||||
|
const allSaved = !this.changed;
|
||||||
|
|
||||||
|
this.updateNowIfNecessary();
|
||||||
|
|
||||||
|
return allSaved;
|
||||||
|
}
|
||||||
|
|
||||||
triggerUpdate() {
|
triggerUpdate() {
|
||||||
if (!this.changed) {
|
if (!this.changed) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class TabContext extends Component {
|
|||||||
return inputNotePath;
|
return inputNotePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath);
|
const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId);
|
||||||
|
|
||||||
if (!resolvedNotePath) {
|
if (!resolvedNotePath) {
|
||||||
logError(`Cannot resolve note path ${inputNotePath}`);
|
logError(`Cannot resolve note path ${inputNotePath}`);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export default class TabManager extends Component {
|
|||||||
openTabs: JSON.stringify(openTabs)
|
openTabs: JSON.stringify(openTabs)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appContext.addBeforeUnloadListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {TabContext[]} */
|
/** @type {TabContext[]} */
|
||||||
@@ -203,7 +205,7 @@ export default class TabManager extends Component {
|
|||||||
let hoistedNoteId = 'root';
|
let hoistedNoteId = 'root';
|
||||||
|
|
||||||
if (tabContext) {
|
if (tabContext) {
|
||||||
const resolvedNotePath = await treeService.resolveNotePath(notePath);
|
const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId);
|
||||||
|
|
||||||
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
|
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
|
||||||
hoistedNoteId = tabContext.hoistedNoteId;
|
hoistedNoteId = tabContext.hoistedNoteId;
|
||||||
@@ -329,6 +331,8 @@ export default class TabManager extends Component {
|
|||||||
|
|
||||||
beforeUnloadEvent() {
|
beforeUnloadEvent() {
|
||||||
this.tabsUpdate.updateNowIfNecessary();
|
this.tabsUpdate.updateNowIfNecessary();
|
||||||
|
|
||||||
|
return true; // don't block closing the tab, this metadata is not that important
|
||||||
}
|
}
|
||||||
|
|
||||||
openNewTabCommand() {
|
openNewTabCommand() {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import appContext from "./app_context.js";
|
|||||||
/**
|
/**
|
||||||
* @return {string|null}
|
* @return {string|null}
|
||||||
*/
|
*/
|
||||||
async function resolveNotePath(notePath) {
|
async function resolveNotePath(notePath, hoistedNoteId = 'root') {
|
||||||
const runPath = await resolveNotePathToSegments(notePath);
|
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||||
|
|
||||||
return runPath ? runPath.join("/") : null;
|
return runPath ? runPath.join("/") : null;
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ async function resolveNotePath(notePath) {
|
|||||||
*
|
*
|
||||||
* @return {string[]}
|
* @return {string[]}
|
||||||
*/
|
*/
|
||||||
async function resolveNotePathToSegments(notePath, logErrors = true) {
|
async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logErrors = true) {
|
||||||
utils.assertArguments(notePath);
|
utils.assertArguments(notePath);
|
||||||
|
|
||||||
// we might get notePath with the tabId suffix, remove it if present
|
// we might get notePath with the tabId suffix, remove it if present
|
||||||
@@ -75,10 +75,10 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
|
|||||||
console.log(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}`);
|
console.log(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const someNotePath = getSomeNotePath(parents[0]);
|
const someNotePath = getSomeNotePath(child, hoistedNoteId);
|
||||||
|
|
||||||
if (someNotePath) { // in case it's root the path may be empty
|
if (someNotePath) { // in case it's root the path may be empty
|
||||||
const pathToRoot = someNotePath.split("/").reverse();
|
const pathToRoot = someNotePath.split("/").reverse().slice(1);
|
||||||
|
|
||||||
for (const noteId of pathToRoot) {
|
for (const noteId of pathToRoot) {
|
||||||
effectivePath.push(noteId);
|
effectivePath.push(noteId);
|
||||||
@@ -96,29 +96,18 @@ async function resolveNotePathToSegments(notePath, logErrors = true) {
|
|||||||
return effectivePath.reverse();
|
return effectivePath.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSomeNotePath(note) {
|
function getSomeNotePathSegments(note, hoistedNotePath = 'root') {
|
||||||
utils.assertArguments(note);
|
utils.assertArguments(note);
|
||||||
|
|
||||||
const path = [];
|
const notePaths = note.getSortedNotePaths(hoistedNotePath);
|
||||||
|
|
||||||
let cur = note;
|
return notePaths[0].notePath;
|
||||||
|
}
|
||||||
|
|
||||||
while (cur.noteId !== 'root') {
|
function getSomeNotePath(note, hoistedNotePath = 'root') {
|
||||||
path.push(cur.noteId);
|
const notePath = getSomeNotePathSegments(note, hoistedNotePath);
|
||||||
|
|
||||||
const parents = cur.getParentNotes().filter(note => note.type !== 'search');
|
return notePath.join('/');
|
||||||
|
|
||||||
if (!parents.length) {
|
|
||||||
logError(`Can't find parents for note ${cur.noteId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cur = parents[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
path.push('root');
|
|
||||||
|
|
||||||
return path.reverse().join('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sortAlphabetically(noteId) {
|
async function sortAlphabetically(noteId) {
|
||||||
@@ -138,7 +127,7 @@ ws.subscribeToMessages(message => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getParentProtectedStatus(node) {
|
function getParentProtectedStatus(node) {
|
||||||
return hoistedNoteService.isRootNode(node) ? 0 : node.getParent().data.isProtected;
|
return hoistedNoteService.isHoistedNode(node) ? 0 : node.getParent().data.isProtected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteIdFromNotePath(notePath) {
|
function getNoteIdFromNotePath(notePath) {
|
||||||
@@ -198,7 +187,7 @@ function getNotePath(node) {
|
|||||||
|
|
||||||
const path = [];
|
const path = [];
|
||||||
|
|
||||||
while (node && !hoistedNoteService.isRootNode(node)) {
|
while (node) {
|
||||||
if (node.data.noteId) {
|
if (node.data.noteId) {
|
||||||
path.push(node.data.noteId);
|
path.push(node.data.noteId);
|
||||||
}
|
}
|
||||||
@@ -206,10 +195,6 @@ function getNotePath(node) {
|
|||||||
node = node.getParent();
|
node = node.getParent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node) { // null node can happen directly after unhoisting when tree is still hoisted but option has been changed already
|
|
||||||
path.push(node.data.noteId); // root or hoisted noteId
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.reverse().join("/");
|
return path.reverse().join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +298,7 @@ export default {
|
|||||||
resolveNotePath,
|
resolveNotePath,
|
||||||
resolveNotePathToSegments,
|
resolveNotePathToSegments,
|
||||||
getSomeNotePath,
|
getSomeNotePath,
|
||||||
|
getSomeNotePathSegments,
|
||||||
getParentProtectedStatus,
|
getParentProtectedStatus,
|
||||||
getNotePath,
|
getNotePath,
|
||||||
getNoteIdFromNotePath,
|
getNoteIdFromNotePath,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class TreeContextMenu {
|
|||||||
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes },
|
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes },
|
||||||
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes },
|
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes },
|
||||||
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
|
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes },
|
||||||
{ title: 'Sort alphabetically <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
||||||
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes }
|
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes }
|
||||||
] },
|
] },
|
||||||
{ title: "----" },
|
{ title: "----" },
|
||||||
@@ -112,10 +112,10 @@ class TreeContextMenu {
|
|||||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
|
||||||
}
|
}
|
||||||
else if (command === "insertNoteAfter") {
|
else if (command === "insertNoteAfter") {
|
||||||
const parentNoteId = this.node.data.parentNoteId;
|
const parentNotePath = treeService.getNotePath(this.node.getParent());
|
||||||
const isProtected = await treeService.getParentProtectedStatus(this.node);
|
const isProtected = await treeService.getParentProtectedStatus(this.node);
|
||||||
|
|
||||||
noteCreateService.createNote(parentNoteId, {
|
noteCreateService.createNote(parentNotePath, {
|
||||||
target: 'after',
|
target: 'after',
|
||||||
targetBranchId: this.node.data.branchId,
|
targetBranchId: this.node.data.branchId,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -123,14 +123,14 @@ class TreeContextMenu {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (command === "insertChildNote") {
|
else if (command === "insertChildNote") {
|
||||||
noteCreateService.createNote(noteId, {
|
const parentNotePath = treeService.getNotePath(this.node);
|
||||||
|
|
||||||
|
noteCreateService.createNote(parentNotePath, {
|
||||||
type: type,
|
type: type,
|
||||||
isProtected: this.node.data.isProtected
|
isProtected: this.node.data.isProtected
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("Triggering", command, notePath);
|
|
||||||
|
|
||||||
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
|
this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNoteForReferenceLink(title) {
|
async createNoteForReferenceLink(title) {
|
||||||
const {note} = await noteCreateService.createNote(this.noteId, {
|
const {note} = await noteCreateService.createNote(this.notePath, {
|
||||||
activate: false,
|
activate: false,
|
||||||
title: title
|
title: title
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class MobileDetailMenuWidget extends BasicWidget {
|
|||||||
],
|
],
|
||||||
selectMenuItemHandler: async ({command}) => {
|
selectMenuItemHandler: async ({command}) => {
|
||||||
if (command === "insertChildNote") {
|
if (command === "insertChildNote") {
|
||||||
noteCreateService.createNote(note.noteId);
|
noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath());
|
||||||
}
|
}
|
||||||
else if (command === "delete") {
|
else if (command === "delete") {
|
||||||
const notePath = appContext.tabManager.getActiveTabNotePath();
|
const notePath = appContext.tabManager.getActiveTabNotePath();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const WIDGET_TPL = `
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<a data-trigger-command="createTopLevelNote" title="Create new top level note" class="icon-action bx bx-folder-plus"></a>
|
<a data-trigger-command="createNoteIntoInbox" title="New note" class="icon-action bx bx-folder-plus"></a>
|
||||||
|
|
||||||
<a data-trigger-command="collapseTree" title="Collapse note tree" class="icon-action bx bx-layer-minus"></a>
|
<a data-trigger-command="collapseTree" title="Collapse note tree" class="icon-action bx bx-layer-minus"></a>
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
await server.put('notes/' + noteId, dto, this.componentId);
|
await server.put('notes/' + noteId, dto, this.componentId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appContext.addBeforeUnloadListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
@@ -293,7 +295,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeUnloadEvent() {
|
beforeUnloadEvent() {
|
||||||
this.spacedUpdate.updateNowIfNecessary();
|
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
textPreviewDisabledEvent({tabContext}) {
|
textPreviewDisabledEvent({tabContext}) {
|
||||||
@@ -316,7 +318,7 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// without await as this otherwise causes deadlock through component mutex
|
// without await as this otherwise causes deadlock through component mutex
|
||||||
noteCreateService.createNote(note.noteId, {
|
noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath(), {
|
||||||
isProtected: note.isProtected,
|
isProtected: note.isProtected,
|
||||||
saveSelection: true
|
saveSelection: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const TPL = `
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-icon-container button.note-icon:hover {
|
.note-icon-container button.note-icon:hover {
|
||||||
|
|||||||
@@ -15,9 +15,21 @@ const TPL = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-path-list {
|
.note-path-list {
|
||||||
max-height: 600px;
|
max-height: 700px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-path-list .path-current {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-path-list .path-archived {
|
||||||
|
color: var(--muted-text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-path-list .path-search {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button class="btn dropdown-toggle note-path-list-button bx bx-collection" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Note paths"></button>
|
<button class="btn dropdown-toggle note-path-list-button bx bx-collection" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Note paths"></button>
|
||||||
@@ -43,20 +55,12 @@ export default class NotePathsWidget extends TabAwareWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (this.noteId === 'root') {
|
if (this.noteId === 'root') {
|
||||||
await this.addPath('root', true);
|
await this.addPath('root');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathSegments = treeService.parseNotePath(this.notePath);
|
for (const notePathRecord of this.note.getSortedNotePaths(this.hoistedNoteId)) {
|
||||||
const activeNoteParentNoteId = pathSegments[pathSegments.length - 2]; // we know this is not root so there must be a parent
|
await this.addPath(notePathRecord);
|
||||||
|
|
||||||
for (const parentNote of this.note.getParentNotes()) {
|
|
||||||
const parentNotePath = treeService.getSomeNotePath(parentNote);
|
|
||||||
// this is to avoid having root notes leading '/'
|
|
||||||
const notePath = parentNotePath ? (parentNotePath + '/' + this.noteId) : this.noteId;
|
|
||||||
const isCurrent = activeNoteParentNoteId === parentNote.noteId;
|
|
||||||
|
|
||||||
await this.addPath(notePath, isCurrent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloneLink = $("<div>")
|
const cloneLink = $("<div>")
|
||||||
@@ -70,7 +74,9 @@ export default class NotePathsWidget extends TabAwareWidget {
|
|||||||
this.$notePathList.append(cloneLink);
|
this.$notePathList.append(cloneLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPath(notePath, isCurrent) {
|
async addPath(notePathRecord) {
|
||||||
|
const notePath = notePathRecord.notePath.join('/');
|
||||||
|
|
||||||
const title = await treeService.getNotePathTitle(notePath);
|
const title = await treeService.getNotePathTitle(notePath);
|
||||||
|
|
||||||
const $noteLink = await linkService.createNoteLink(notePath, {title});
|
const $noteLink = await linkService.createNoteLink(notePath, {title});
|
||||||
@@ -82,8 +88,33 @@ export default class NotePathsWidget extends TabAwareWidget {
|
|||||||
.find('a')
|
.find('a')
|
||||||
.addClass("no-tooltip-preview");
|
.addClass("no-tooltip-preview");
|
||||||
|
|
||||||
if (isCurrent) {
|
const comments = [];
|
||||||
$noteLink.addClass("current");
|
|
||||||
|
if (this.notePath === notePath) {
|
||||||
|
$noteLink.addClass("path-current");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notePathRecord.isInHoistedSubTree) {
|
||||||
|
$noteLink.addClass("path-in-hoisted-subtree");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
comments.push("outside of hoisting");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notePathRecord.isArchived) {
|
||||||
|
$noteLink.addClass("path-archived");
|
||||||
|
|
||||||
|
comments.push("archived");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notePathRecord.isSearch) {
|
||||||
|
$noteLink.addClass("path-search");
|
||||||
|
|
||||||
|
comments.push("search");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comments.length > 0) {
|
||||||
|
$noteLink.append(` (${comments.join(', ')})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notePathList.append($noteLink);
|
this.$notePathList.append($noteLink);
|
||||||
@@ -96,4 +127,10 @@ export default class NotePathsWidget extends TabAwareWidget {
|
|||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
await super.refresh();
|
||||||
|
|
||||||
|
this.$widget.find('.dropdown-toggle').dropdown('hide');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import utils from "../services/utils.js";
|
|||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import SpacedUpdate from "../services/spaced_update.js";
|
import SpacedUpdate from "../services/spaced_update.js";
|
||||||
|
import appContext from "../services/app_context.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="note-title-container">
|
<div class="note-title-container">
|
||||||
@@ -37,6 +38,8 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
await server.put(`notes/${this.noteId}/change-title`, {title});
|
await server.put(`notes/${this.noteId}/change-title`, {title});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appContext.addBeforeUnloadListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
@@ -101,6 +104,6 @@ export default class NoteTitleWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeUnloadEvent() {
|
beforeUnloadEvent() {
|
||||||
this.spacedUpdate.updateNowIfNecessary();
|
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,8 +203,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
|
this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e));
|
||||||
this.$tree.on("mousedown", ".add-note-button", e => {
|
this.$tree.on("mousedown", ".add-note-button", e => {
|
||||||
const node = $.ui.fancytree.getNode(e);
|
const node = $.ui.fancytree.getNode(e);
|
||||||
|
const parentNotePath = treeService.getNotePath(node);
|
||||||
|
|
||||||
noteCreateService.createNote(node.data.noteId, {
|
noteCreateService.createNote(parentNotePath, {
|
||||||
isProtected: node.data.isProtected
|
isProtected: node.data.isProtected
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -594,7 +595,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
let childBranches = parentNote.getFilteredChildBranches();
|
let childBranches = parentNote.getFilteredChildBranches();
|
||||||
|
|
||||||
if (childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) {
|
if (parentNote.type === 'search' && childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) {
|
||||||
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,12 +805,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
/** @return {FancytreeNode} */
|
/** @return {FancytreeNode} */
|
||||||
async getNodeFromPath(notePath, expand = false, logErrors = true) {
|
async getNodeFromPath(notePath, expand = false, logErrors = true) {
|
||||||
utils.assertArguments(notePath);
|
utils.assertArguments(notePath);
|
||||||
|
/** @let {FancytreeNode} */
|
||||||
|
let parentNode = this.getNodesByNoteId('root')[0];
|
||||||
|
|
||||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
let resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, this.hoistedNoteId, logErrors);
|
||||||
/** @const {FancytreeNode} */
|
|
||||||
let parentNode = null;
|
|
||||||
|
|
||||||
const resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, logErrors);
|
|
||||||
|
|
||||||
if (!resolvedNotePathSegments) {
|
if (!resolvedNotePathSegments) {
|
||||||
if (logErrors) {
|
if (logErrors) {
|
||||||
@@ -819,14 +818,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolvedNotePathSegments = resolvedNotePathSegments.slice(1);
|
||||||
|
|
||||||
for (const childNoteId of resolvedNotePathSegments) {
|
for (const childNoteId of resolvedNotePathSegments) {
|
||||||
if (childNoteId === hoistedNoteId) {
|
|
||||||
// there must be exactly one node with given hoistedNoteId
|
|
||||||
parentNode = this.getNodesByNoteId(childNoteId)[0];
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we expand only after hoisted note since before then nodes are not actually present in the tree
|
// we expand only after hoisted note since before then nodes are not actually present in the tree
|
||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
if (!parentNode.isLoaded()) {
|
if (!parentNode.isLoaded()) {
|
||||||
@@ -857,7 +851,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
// these are real notes with real notePath, user can display them in a detail
|
// these are real notes with real notePath, user can display them in a detail
|
||||||
// but they don't have a node in the tree
|
// but they don't have a node in the tree
|
||||||
|
|
||||||
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`);
|
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -999,6 +993,9 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
const activeNodeFocused = activeNode && activeNode.hasFocus();
|
const activeNodeFocused = activeNode && activeNode.hasFocus();
|
||||||
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
|
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
|
||||||
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
|
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
|
||||||
|
|
||||||
|
console.log(activeNotePath, activeNodeFocused);
|
||||||
|
|
||||||
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
|
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
|
||||||
const activeNoteId = activeNode ? activeNode.data.noteId : null;
|
const activeNoteId = activeNode ? activeNode.data.noteId : null;
|
||||||
|
|
||||||
@@ -1119,7 +1116,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
node.setActive(true, {noEvents: true, noFocus: true});
|
node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// this is used when original note has been deleted and we want to move the focus to the note above/below
|
// this is used when original note has been deleted and we want to move the focus to the note above/below
|
||||||
@@ -1131,16 +1128,16 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
// this should be done by tabcontext / tabmanager and note tree should only listen to
|
// this should be done by tabcontext / tabmanager and note tree should only listen to
|
||||||
// changes in active note and just set the "active" state
|
// 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
|
// 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) {
|
if (noteIdsToReload.size > 0 || noteIdsToUpdate.size > 0) {
|
||||||
@@ -1207,13 +1204,20 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filterHoistedBranch() {
|
async filterHoistedBranch() {
|
||||||
if (this.tabContext) {
|
if (this.tabContext) {
|
||||||
|
// make sure the hoisted node is loaded (can be unloaded e.g. after tree collapse in another tab)
|
||||||
|
const hoistedNotePath = await treeService.resolveNotePath(this.tabContext.hoistedNoteId);
|
||||||
|
await this.getNodeFromPath(hoistedNotePath);
|
||||||
|
|
||||||
if (this.tabContext.hoistedNoteId === 'root') {
|
if (this.tabContext.hoistedNoteId === 'root') {
|
||||||
this.tree.clearFilter();
|
this.tree.clearFilter();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.tree.filterBranches(node => node.data.noteId === this.tabContext.hoistedNoteId);
|
// hack when hoisted note is cloned then it could be filtered multiple times while we want only 1
|
||||||
|
this.tree.filterBranches(node =>
|
||||||
|
node.data.noteId === this.tabContext.hoistedNoteId // optimization to not having always resolve the node path
|
||||||
|
&& treeService.getNotePath(node) === hoistedNotePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1370,7 +1374,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sortChildNotesCommand({node}) {
|
sortChildNotesCommand({node}) {
|
||||||
treeService.sortAlphabetically(node.data.noteId);
|
import("../dialogs/sort_child_notes.js").then(d => d.showDialog(node.data.noteId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async recentChangesInSubtreeCommand({node}) {
|
async recentChangesInSubtreeCommand({node}) {
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ export default class QuickSearchWidget extends BasicWidget {
|
|||||||
this.$widget.find('.input-group-append').on('shown.bs.dropdown', () => this.search());
|
this.$widget.find('.input-group-append').on('shown.bs.dropdown', () => this.search());
|
||||||
|
|
||||||
utils.bindElShortcut(this.$searchString, 'return', () => {
|
utils.bindElShortcut(this.$searchString, 'return', () => {
|
||||||
this.$dropdownToggle.dropdown('show');
|
if (this.$dropdownMenu.is(":visible")) {
|
||||||
|
this.search(); // just update already visible dropdown
|
||||||
|
} else {
|
||||||
|
this.$dropdownToggle.dropdown('show');
|
||||||
|
}
|
||||||
|
|
||||||
this.$searchString.focus();
|
this.$searchString.focus();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export default class TabAwareWidget extends BasicWidget {
|
|||||||
return this.tabContext && this.tabContext.notePath;
|
return this.tabContext && this.tabContext.notePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hoistedNoteId() {
|
||||||
|
return this.tabContext && this.tabContext.hoistedNoteId;
|
||||||
|
}
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
return !!this.note;
|
return !!this.note;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,4 +83,10 @@ export default class InheritedAttributesWidget extends TabAwareWidget {
|
|||||||
getInheritedAttributes(note) {
|
getInheritedAttributes(note) {
|
||||||
return note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
return note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entitiesReloadedEvent({loadResults}) {
|
||||||
|
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNoteForReferenceLink(title) {
|
async createNoteForReferenceLink(title) {
|
||||||
const {note} = await noteCreateService.createNote(this.noteId, {
|
const {note} = await noteCreateService.createNote(this.notePath, {
|
||||||
activate: false,
|
activate: false,
|
||||||
title: title
|
title: title
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import attributeAutocompleteService from "../../services/attribute_autocomplete.
|
|||||||
import TypeWidget from "./type_widget.js";
|
import TypeWidget from "./type_widget.js";
|
||||||
import appContext from "../../services/app_context.js";
|
import appContext from "../../services/app_context.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
|
import treeCache from "../../services/tree_cache.js";
|
||||||
|
|
||||||
const uniDirectionalOverlays = [
|
const uniDirectionalOverlays = [
|
||||||
[ "Arrow", {
|
[ "Arrow", {
|
||||||
@@ -531,8 +532,11 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
linkService.goToLink(e);
|
linkService.goToLink(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const note = await treeCache.getNote(noteId);
|
||||||
|
|
||||||
const $noteBox = $("<div>")
|
const $noteBox = $("<div>")
|
||||||
.addClass("note-box")
|
.addClass("note-box")
|
||||||
|
.addClass(note.getCssClass())
|
||||||
.prop("id", this.noteIdToId(noteId))
|
.prop("id", this.noteIdToId(noteId))
|
||||||
.append($("<span>").addClass("title").append($link))
|
.append($("<span>").addClass("title").append($link))
|
||||||
.append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note."))
|
.append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note."))
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const sql = require('../../services/sql');
|
|||||||
const dateUtils = require('../../services/date_utils');
|
const dateUtils = require('../../services/date_utils');
|
||||||
const noteService = require('../../services/notes');
|
const noteService = require('../../services/notes');
|
||||||
const attributeService = require('../../services/attributes');
|
const attributeService = require('../../services/attributes');
|
||||||
|
const cls = require('../../services/cls');
|
||||||
|
const repository = require('../../services/repository');
|
||||||
|
|
||||||
function getInboxNote(req) {
|
function getInboxNote(req) {
|
||||||
return attributeService.getNoteWithLabel('inbox')
|
return attributeService.getNoteWithLabel('inbox')
|
||||||
@@ -62,12 +64,36 @@ function createSqlConsole() {
|
|||||||
function createSearchNote(req) {
|
function createSearchNote(req) {
|
||||||
const params = req.body;
|
const params = req.body;
|
||||||
const searchString = params.searchString || "";
|
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 =
|
let searchHome;
|
||||||
attributeService.getNoteWithLabel('searchHome')
|
|
||||||
|| dateNoteService.getDateNote(today);
|
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({
|
const {note} = noteService.createNewNote({
|
||||||
parentNoteId: searchHome.noteId,
|
parentNoteId: searchHome.noteId,
|
||||||
@@ -79,8 +105,8 @@ function createSearchNote(req) {
|
|||||||
|
|
||||||
note.setLabel('searchString', searchString);
|
note.setLabel('searchString', searchString);
|
||||||
|
|
||||||
if (params.ancestorNoteId) {
|
if (ancestorNoteId) {
|
||||||
note.setRelation('ancestor', params.ancestorNoteId);
|
note.setRelation('ancestor', ancestorNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return note;
|
return note;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const noteService = require('../../services/notes');
|
|||||||
const treeService = require('../../services/tree');
|
const treeService = require('../../services/tree');
|
||||||
const repository = require('../../services/repository');
|
const repository = require('../../services/repository');
|
||||||
const utils = require('../../services/utils');
|
const utils = require('../../services/utils');
|
||||||
|
const log = require('../../services/log');
|
||||||
const TaskContext = require('../../services/task_context');
|
const TaskContext = require('../../services/task_context');
|
||||||
|
|
||||||
function getNote(req) {
|
function getNote(req) {
|
||||||
@@ -85,10 +86,20 @@ function undeleteNote(req) {
|
|||||||
taskContext.taskSucceeded();
|
taskContext.taskSucceeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortNotes(req) {
|
function sortChildNotes(req) {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
const {sortBy, sortDirection} = req.body;
|
||||||
|
|
||||||
treeService.sortNotesAlphabetically(noteId);
|
log.info(`Sorting ${noteId} children with ${sortBy} ${sortDirection}`);
|
||||||
|
|
||||||
|
const reverse = sortDirection === 'desc';
|
||||||
|
|
||||||
|
if (sortBy === 'title') {
|
||||||
|
treeService.sortNotesByTitle(noteId, false, reverse);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
treeService.sortNotes(noteId, sortBy, reverse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function protectNote(req) {
|
function protectNote(req) {
|
||||||
@@ -215,7 +226,7 @@ module.exports = {
|
|||||||
deleteNote,
|
deleteNote,
|
||||||
undeleteNote,
|
undeleteNote,
|
||||||
createNote,
|
createNote,
|
||||||
sortNotes,
|
sortChildNotes,
|
||||||
protectNote,
|
protectNote,
|
||||||
setNoteTypeMime,
|
setNoteTypeMime,
|
||||||
getRelationMap,
|
getRelationMap,
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ function register(app) {
|
|||||||
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
|
apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote);
|
||||||
apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote);
|
apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote);
|
||||||
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
|
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
|
||||||
apiRoute(PUT, '/api/notes/:noteId/sort', notesApiRoute.sortNotes);
|
apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes);
|
||||||
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote);
|
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote);
|
||||||
apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
|
apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime);
|
||||||
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
|
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const BUILTIN_ATTRIBUTES = [
|
|||||||
{ type: 'label', name: 'workspaceIconClass' },
|
{ type: 'label', name: 'workspaceIconClass' },
|
||||||
{ type: 'label', name: 'workspaceTabBackgroundColor' },
|
{ type: 'label', name: 'workspaceTabBackgroundColor' },
|
||||||
{ type: 'label', name: 'searchHome' },
|
{ type: 'label', name: 'searchHome' },
|
||||||
|
{ type: 'label', name: 'hoistedSearchHome' },
|
||||||
{ type: 'label', name: 'sqlConsoleHome' },
|
{ type: 'label', name: 'sqlConsoleHome' },
|
||||||
{ type: 'label', name: 'datePattern' },
|
{ type: 'label', name: 'datePattern' },
|
||||||
|
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ function BackendScriptApi(currentNote, apiParams) {
|
|||||||
* @method
|
* @method
|
||||||
* @param {string} parentNoteId - this note's child notes will be sorted
|
* @param {string} parentNoteId - this note's child notes will be sorted
|
||||||
*/
|
*/
|
||||||
this.sortNotesAlphabetically = treeService.sortNotesAlphabetically;
|
this.sortNotesByTitle = treeService.sortNotesByTitle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
|
* This method finds note by its noteId and prefix and either sets it to the given parentNoteId
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = { buildDate:"2021-02-25T22:41:35+01:00", buildRevision: "cde41b268e4e88b3fe3601d9d19b3f5241625ada" };
|
module.exports = { buildDate:"2021-03-08T23:11:11+01:00", buildRevision: "f27370d44f08afaa22d4cd86cba489584f9c878b" };
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ function addEntityChange(entityChange, sourceId, isSynced) {
|
|||||||
cls.addEntityChange(localEntityChange);
|
cls.addEntityChange(localEntityChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addNoteReorderingEntityChange(parentNoteId, sourceId) {
|
||||||
|
addEntityChange({
|
||||||
|
entityName: "note_reordering",
|
||||||
|
entityId: parentNoteId,
|
||||||
|
hash: 'N/A',
|
||||||
|
isErased: false,
|
||||||
|
utcDateChanged: dateUtils.utcNowDateTime()
|
||||||
|
}, sourceId);
|
||||||
|
|
||||||
|
const eventService = require('./events');
|
||||||
|
|
||||||
|
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||||
|
entityName: 'note_reordering',
|
||||||
|
entity: sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [parentNoteId])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function moveEntityChangeToTop(entityName, entityId) {
|
function moveEntityChangeToTop(entityName, entityId) {
|
||||||
const [hash, isSynced] = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]);
|
const [hash, isSynced] = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]);
|
||||||
|
|
||||||
@@ -121,13 +138,7 @@ function fillAllEntityChanges() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addNoteReorderingEntityChange: (parentNoteId, sourceId) => addEntityChange({
|
addNoteReorderingEntityChange,
|
||||||
entityName: "note_reordering",
|
|
||||||
entityId: parentNoteId,
|
|
||||||
hash: 'N/A',
|
|
||||||
isErased: false,
|
|
||||||
utcDateChanged: dateUtils.utcNowDateTime()
|
|
||||||
}, sourceId),
|
|
||||||
moveEntityChangeToTop,
|
moveEntityChangeToTop,
|
||||||
addEntityChange,
|
addEntityChange,
|
||||||
fillAllEntityChanges,
|
fillAllEntityChanges,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
|
|||||||
|
|
||||||
for (const parentNote of noteFromCache.parents) {
|
for (const parentNote of noteFromCache.parents) {
|
||||||
if (parentNote.hasLabel("sorted")) {
|
if (parentNote.hasLabel("sorted")) {
|
||||||
treeService.sortNotesAlphabetically(parentNote.noteId);
|
treeService.sortNotesByTitle(parentNote.noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,23 +53,19 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
|||||||
if (entity.type === 'relation' && entity.name === 'template') {
|
if (entity.type === 'relation' && entity.name === 'template') {
|
||||||
const note = repository.getNote(entity.noteId);
|
const note = repository.getNote(entity.noteId);
|
||||||
|
|
||||||
if (!["text", "code"].includes(note.type)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = note.getContent();
|
|
||||||
|
|
||||||
if (content && content.trim().length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templateNote = repository.getNote(entity.value);
|
const templateNote = repository.getNote(entity.value);
|
||||||
|
|
||||||
if (!templateNote) {
|
if (!templateNote) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (templateNote.isStringNote()) {
|
const content = note.getContent();
|
||||||
|
|
||||||
|
if (!["text", "code"].includes(note.type)
|
||||||
|
// if the note has already content we're not going to overwrite it with template's one
|
||||||
|
&& (!content || content.trim().length === 0)
|
||||||
|
&& templateNote.isStringNote()) {
|
||||||
|
|
||||||
const templateNoteContent = templateNote.getContent();
|
const templateNoteContent = templateNote.getContent();
|
||||||
|
|
||||||
if (templateNoteContent) {
|
if (templateNoteContent) {
|
||||||
@@ -81,17 +77,21 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
|||||||
note.save();
|
note.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
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') {
|
else if (entity.type === 'label' && entity.name === 'sorted') {
|
||||||
treeService.sortNotesAlphabetically(entity.noteId);
|
treeService.sortNotesByTitle(entity.noteId);
|
||||||
|
|
||||||
if (entity.isInheritable) {
|
if (entity.isInheritable) {
|
||||||
const note = noteCache.notes[entity.noteId];
|
const note = noteCache.notes[entity.noteId];
|
||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
for (const noteId of note.subtreeNoteIds) {
|
for (const noteId of note.subtreeNoteIds) {
|
||||||
treeService.sortNotesAlphabetically(noteId);
|
treeService.sortNotesByTitle(noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
if (!metaFile) {
|
if (!metaFile) {
|
||||||
// if there's no meta file then the notes are created based on the order in that tar file but that
|
// if there's no meta file then the notes are created based on the order in that tar file but that
|
||||||
// is usually quite random so we sort the notes in the way they would appear in the file manager
|
// is usually quite random so we sort the notes in the way they would appear in the file manager
|
||||||
treeService.sortNotesAlphabetically(noteId, true);
|
treeService.sortNotesByTitle(noteId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
taskContext.increaseProgressCount();
|
taskContext.increaseProgressCount();
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
|
|||||||
delete noteCache.attributes[attributeId];
|
delete noteCache.attributes[attributeId];
|
||||||
|
|
||||||
if (attr) {
|
if (attr) {
|
||||||
delete noteCache.attributeIndex[`${attr.type}-${attr.name.toLowerCase()}`];
|
const key = `${attr.type}-${attr.name.toLowerCase()}`;
|
||||||
|
|
||||||
|
if (key in noteCache.attributeIndex) {
|
||||||
|
noteCache.attributeIndex[key] = noteCache.attributeIndex[key].filter(attr => attr.attributeId !== attributeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (attributeId in noteCache.attributes) {
|
else if (attributeId in noteCache.attributes) {
|
||||||
@@ -149,6 +153,19 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (entityName === 'note_reordering') {
|
||||||
|
const parentNoteIds = new Set();
|
||||||
|
|
||||||
|
for (const branchId in entity) {
|
||||||
|
const branch = noteCache.branches[branchId];
|
||||||
|
|
||||||
|
if (branch) {
|
||||||
|
branch.notePosition = entity[branchId];
|
||||||
|
|
||||||
|
parentNoteIds.add(branch.parentNoteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
|
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function insert(tableName, rec, replace = false) {
|
|||||||
|
|
||||||
const res = execute(query, Object.values(rec));
|
const res = execute(query, Object.values(rec));
|
||||||
|
|
||||||
return res.lastInsertRowid;
|
return res ? res.lastInsertRowid : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replace(tableName, rec) {
|
function replace(tableName, rec) {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
|
function sortNotesByTitle(parentNoteId, foldersFirst = false, reverse = false) {
|
||||||
sql.transactional(() => {
|
sql.transactional(() => {
|
||||||
const notes = sql.getRows(
|
const notes = sql.getRows(
|
||||||
`SELECT branches.branchId, notes.noteId, title, isProtected,
|
`SELECT branches.branchId, notes.noteId, title, isProtected,
|
||||||
@@ -120,7 +120,7 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
|
|||||||
protectedSessionService.decryptNotes(notes);
|
protectedSessionService.decryptNotes(notes);
|
||||||
|
|
||||||
notes.sort((a, b) => {
|
notes.sort((a, b) => {
|
||||||
if (directoriesFirst && ((a.hasChildren && !b.hasChildren) || (!a.hasChildren && b.hasChildren))) {
|
if (foldersFirst && ((a.hasChildren && !b.hasChildren) || (!a.hasChildren && b.hasChildren))) {
|
||||||
// exactly one note of the two is a directory so the sorting will be done based on this status
|
// exactly one note of the two is a directory so the sorting will be done based on this status
|
||||||
return a.hasChildren ? -1 : 1;
|
return a.hasChildren ? -1 : 1;
|
||||||
}
|
}
|
||||||
@@ -129,6 +129,10 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
notes.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
let position = 10;
|
let position = 10;
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
@@ -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
|
* @deprecated - this will be removed in the future
|
||||||
*/
|
*/
|
||||||
@@ -194,6 +225,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getNotes,
|
getNotes,
|
||||||
validateParentChild,
|
validateParentChild,
|
||||||
sortNotesAlphabetically,
|
sortNotesByTitle,
|
||||||
|
sortNotes,
|
||||||
setNoteToParent
|
setNoteToParent
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<%- include('dialogs/move_to.ejs') %>
|
<%- include('dialogs/move_to.ejs') %>
|
||||||
<%- include('dialogs/backend_log.ejs') %>
|
<%- include('dialogs/backend_log.ejs') %>
|
||||||
<%- include('dialogs/include_note.ejs') %>
|
<%- include('dialogs/include_note.ejs') %>
|
||||||
|
<%- include('dialogs/sort_child_notes.ejs') %>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.baseApiUrl = 'api/';
|
window.baseApiUrl = 'api/';
|
||||||
|
|||||||
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>
|
||||||
Reference in New Issue
Block a user