mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 08:16:40 +01:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7307ca385f | ||
|
|
c1fd9825aa | ||
|
|
9de7d3fc53 | ||
|
|
3c5db844ba | ||
|
|
e7330c1104 | ||
|
|
ec4586b164 | ||
|
|
91e5f24798 | ||
|
|
38723e0189 | ||
|
|
8c88ce6f65 | ||
|
|
4d22959e28 | ||
|
|
50a28d8c51 | ||
|
|
e25b633ec4 | ||
|
|
75bd395669 | ||
|
|
5e353a5612 | ||
|
|
6b359b7796 | ||
|
|
13f9d037dc | ||
|
|
1911d64c1c | ||
|
|
d4c3f1b3f2 | ||
|
|
3db84daf94 |
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.42.2",
|
||||
"version": "0.42.5",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -3345,9 +3345,9 @@
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0.tgz",
|
||||
"integrity": "sha512-JsaSQNPh+XDYkLj8APtVKTtvpb86KIG57W5OOss4TNrn8L3isC9LsCITwfnVmGIXHhvX6oY/weCtN5hAAytjVg==",
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.2.tgz",
|
||||
"integrity": "sha512-+a3KegLvQXVjC3b6yBWwZmtWp3tHf9ut27yORAWHO9JRFtKfNf88fi1UvTPJSW8R0sUH7ZEdzN6A95T22KGtlA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@electron/get": "^1.0.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.42.3",
|
||||
"version": "0.42.6",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -78,7 +78,7 @@
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "9.0.0",
|
||||
"electron": "9.0.2",
|
||||
"electron-builder": "22.6.0",
|
||||
"electron-packager": "14.2.1",
|
||||
"electron-rebuild": "1.10.1",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
const anonymizationService = require('./services/anonymization');
|
||||
const backupService = require('./services/backup');
|
||||
|
||||
anonymizationService.anonymize().then(filePath => {
|
||||
console.log("Anonymized file has been saved to:", filePath);
|
||||
backupService.anonymize().then(resp => {
|
||||
if (resp.success) {
|
||||
console.log("Anonymization failed.");
|
||||
}
|
||||
else {
|
||||
console.log("Anonymized file has been saved to: " + resp.anonymizedFilePath);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -541,12 +541,13 @@ class Note extends Entity {
|
||||
/**
|
||||
* @return {Promise<Attribute>}
|
||||
*/
|
||||
async addAttribute(type, name, value = "") {
|
||||
async addAttribute(type, name, value = "", isInheritable = false) {
|
||||
const attr = new Attribute({
|
||||
noteId: this.noteId,
|
||||
type: type,
|
||||
name: name,
|
||||
value: value
|
||||
value: value,
|
||||
isInheritable: isInheritable
|
||||
});
|
||||
|
||||
await attr.save();
|
||||
@@ -556,12 +557,12 @@ class Note extends Entity {
|
||||
return attr;
|
||||
}
|
||||
|
||||
async addLabel(name, value = "") {
|
||||
return await this.addAttribute(LABEL, name, value);
|
||||
async addLabel(name, value = "", isInheritable = false) {
|
||||
return await this.addAttribute(LABEL, name, value, isInheritable);
|
||||
}
|
||||
|
||||
async addRelation(name, targetNoteId) {
|
||||
return await this.addAttribute(RELATION, name, targetNoteId);
|
||||
async addRelation(name, targetNoteId, isInheritable = false) {
|
||||
return await this.addAttribute(RELATION, name, targetNoteId, isInheritable);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,13 +39,14 @@ export async function showDialog(noteIds) {
|
||||
}
|
||||
|
||||
async function cloneNotesTo(notePath) {
|
||||
const targetNoteId = treeService.getNoteIdFromNotePath(notePath);
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
const targetBranchId = await treeCache.getBranchId(parentNoteId, noteId);
|
||||
|
||||
for (const cloneNoteId of clonedNoteIds) {
|
||||
await branchService.cloneNoteTo(cloneNoteId, targetNoteId, $clonePrefix.val());
|
||||
await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val());
|
||||
|
||||
const clonedNote = await treeCache.getNote(cloneNoteId);
|
||||
const targetNote = await treeCache.getNote(targetNoteId);
|
||||
const targetNote = await treeCache.getBranch(targetBranchId).getNote();
|
||||
|
||||
toastService.showMessage(`Note "${clonedNote.title}" has been cloned into ${targetNote.title}`);
|
||||
}
|
||||
@@ -64,4 +65,4 @@ $form.on('submit', () => {
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,10 +32,11 @@ export async function showDialog(branchIds) {
|
||||
noteAutocompleteService.showRecentNotes($noteAutoComplete);
|
||||
}
|
||||
|
||||
async function moveNotesTo(parentNoteId) {
|
||||
await branchService.moveToParentNote(movedBranchIds, parentNoteId);
|
||||
async function moveNotesTo(parentBranchId) {
|
||||
await branchService.moveToParentNote(movedBranchIds, parentBranchId);
|
||||
|
||||
const parentNote = await treeCache.getNote(parentNoteId);
|
||||
const parentBranch = treeCache.getBranch(parentBranchId);
|
||||
const parentNote = await parentBranch.getNote();
|
||||
|
||||
toastService.showMessage(`Selected notes have been moved into ${parentNote.title}`);
|
||||
}
|
||||
@@ -46,13 +47,12 @@ $form.on('submit', () => {
|
||||
if (notePath) {
|
||||
$dialog.modal('hide');
|
||||
|
||||
const noteId = treeService.getNoteIdFromNotePath(notePath);
|
||||
|
||||
moveNotesTo(noteId);
|
||||
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
|
||||
treeCache.getBranchId(parentNoteId, noteId).then(branchId => moveNotesTo(branchId));
|
||||
}
|
||||
else {
|
||||
console.error("No path to move to.");
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,12 +17,18 @@ const TPL = `
|
||||
|
||||
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
|
||||
|
||||
<h4>Debugging</h4>
|
||||
<h4>Anonymize database</h4>
|
||||
|
||||
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and some non-sensitive metadata)
|
||||
for sharing online for debugging purposes without fear of leaking your personal data.</p>
|
||||
|
||||
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
|
||||
|
||||
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
|
||||
for sharing online for debugging purposes without fear of leaking your personal data.</p>
|
||||
<h4>Backup database</h4>
|
||||
|
||||
<p>Trilium has automatic backup (daily, weekly, monthly), but you can also trigger backup manually here.</p>
|
||||
|
||||
<button id="backup-database-button" class="btn">Backup database now</button><br/><br/>
|
||||
|
||||
<h4>Vacuum database</h4>
|
||||
|
||||
@@ -37,6 +43,7 @@ export default class AdvancedOptions {
|
||||
this.$forceFullSyncButton = $("#force-full-sync-button");
|
||||
this.$fillSyncRowsButton = $("#fill-sync-rows-button");
|
||||
this.$anonymizeButton = $("#anonymize-button");
|
||||
this.$backupDatabaseButton = $("#backup-database-button");
|
||||
this.$vacuumDatabaseButton = $("#vacuum-database-button");
|
||||
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
|
||||
|
||||
@@ -53,21 +60,32 @@ export default class AdvancedOptions {
|
||||
});
|
||||
|
||||
this.$anonymizeButton.on('click', async () => {
|
||||
await server.post('anonymization/anonymize');
|
||||
const resp = await server.post('database/anonymize');
|
||||
|
||||
toastService.showMessage("Created anonymized database");
|
||||
if (!resp.success) {
|
||||
toastService.showError("Could not create anonymized database, check backend logs for details");
|
||||
}
|
||||
else {
|
||||
toastService.showMessage(`Created anonymized database in ${resp.anonymizedFilePath}`, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
this.$backupDatabaseButton.on('click', async () => {
|
||||
const {backupFile} = await server.post('database/backup-database');
|
||||
|
||||
toastService.showMessage("Database has been backed up to " + backupFile, 10000);
|
||||
});
|
||||
|
||||
this.$vacuumDatabaseButton.on('click', async () => {
|
||||
await server.post('cleanup/vacuum-database');
|
||||
await server.post('database/vacuum-database');
|
||||
|
||||
toastService.showMessage("Database has been vacuumed");
|
||||
});
|
||||
|
||||
this.$findAndFixConsistencyIssuesButton.on('click', async () => {
|
||||
await server.post('cleanup/find-and-fix-consistency-issues');
|
||||
await server.post('database/find-and-fix-consistency-issues');
|
||||
|
||||
toastService.showMessage("Consistency issues should be fixed.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToParentNote(branchIdsToMove, newParentNoteId) {
|
||||
async function moveToParentNote(branchIdsToMove, newParentBranchId) {
|
||||
branchIdsToMove = filterRootNote(branchIdsToMove);
|
||||
|
||||
for (const branchIdToMove of branchIdsToMove) {
|
||||
@@ -56,7 +56,7 @@ async function moveToParentNote(branchIdsToMove, newParentNoteId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentNoteId}`);
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
@@ -198,8 +198,8 @@ ws.subscribeToMessages(async message => {
|
||||
}
|
||||
});
|
||||
|
||||
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
|
||||
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
|
||||
async function cloneNoteTo(childNoteId, parentBranchId, prefix) {
|
||||
const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, {
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
@@ -225,4 +225,4 @@ export default {
|
||||
moveNodeUpInHierarchy,
|
||||
cloneNoteAfter,
|
||||
cloneNoteTo
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,13 +33,13 @@ async function pasteAfter(afterBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function pasteInto(parentNoteId) {
|
||||
async function pasteInto(parentBranchId) {
|
||||
if (isClipboardEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clipboardMode === 'cut') {
|
||||
await branchService.moveToParentNote(clipboardBranchIds, parentNoteId);
|
||||
await branchService.moveToParentNote(clipboardBranchIds, parentBranchId);
|
||||
|
||||
clipboardBranchIds = [];
|
||||
clipboardMode = null;
|
||||
@@ -50,7 +50,7 @@ async function pasteInto(parentNoteId) {
|
||||
for (const clipboardBranch of clipboardBranches) {
|
||||
const clipboardNote = await clipboardBranch.getNote();
|
||||
|
||||
await branchService.cloneNoteTo(clipboardNote.noteId, parentNoteId);
|
||||
await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId);
|
||||
}
|
||||
|
||||
// copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places
|
||||
@@ -89,4 +89,4 @@ export default {
|
||||
cut,
|
||||
copy,
|
||||
isClipboardEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ let protectedSessionDeferred = null;
|
||||
|
||||
async function leaveProtectedSession() {
|
||||
if (protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
utils.reloadApp();
|
||||
protectedSessionHolder.resetProtectedSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,4 +113,4 @@ export default {
|
||||
enterProtectedSession,
|
||||
leaveProtectedSession,
|
||||
setupProtectedSession
|
||||
};
|
||||
};
|
||||
|
||||
@@ -238,7 +238,7 @@ export default class TabManager extends Component {
|
||||
}
|
||||
|
||||
this.tabsUpdate.scheduleUpdate();
|
||||
|
||||
|
||||
this.setCurrentNotePathToHash();
|
||||
}
|
||||
|
||||
@@ -249,6 +249,9 @@ export default class TabManager extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// close dangling autocompletes after closing the tab
|
||||
$(".aa-input").autocomplete("close");
|
||||
|
||||
await this.triggerEvent('beforeTabRemove', {tabId});
|
||||
|
||||
if (this.tabContexts.length <= 1) {
|
||||
@@ -267,9 +270,6 @@ export default class TabManager extends Component {
|
||||
|
||||
this.children = this.children.filter(tc => tc.tabId !== tabId);
|
||||
|
||||
// remove dangling autocompletes after closing the tab
|
||||
$(".algolia-autocomplete").remove();
|
||||
|
||||
this.triggerEvent('tabRemoved', {tabId});
|
||||
|
||||
this.tabsUpdate.scheduleUpdate();
|
||||
@@ -346,4 +346,4 @@ export default class TabManager extends Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,10 @@ async function consumeSyncData() {
|
||||
utils.reloadApp();
|
||||
}
|
||||
|
||||
for (const syncRow of nonProcessedSyncRows) {
|
||||
processedSyncIds.add(syncRow.id);
|
||||
}
|
||||
|
||||
lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncRows[allSyncRows.length - 1].id);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,72 @@ const TPL = `
|
||||
width: 320px;
|
||||
border-radius: 10px 0 10px 10px;
|
||||
}
|
||||
|
||||
ul.fancytree-container {
|
||||
outline: none !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.fancytree-custom-icon {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.fancytree-title {
|
||||
color: inherit !important;
|
||||
background: inherit !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
filter: drop-shadow(2px 2px 2px var(--main-text-color));
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
content: " *"
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
|
||||
.ui-fancytree > li > ul {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active .fancytree-title {
|
||||
font-weight: bold;
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
|
||||
color: var(--active-item-text-color) !important;
|
||||
background-color: var(--active-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-selected .fancytree-title {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
span.fancytree-node:hover span.fancytree-title {
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-node.archived {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class="btn btn-sm icon-button bx bx-cog tree-settings-button" title="Tree settings"></button>
|
||||
@@ -206,6 +272,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
const treeData = [await this.prepareRootNode()];
|
||||
|
||||
this.$tree.fancytree({
|
||||
titlesTabbable: true,
|
||||
autoScroll: true,
|
||||
keyboard: false, // we takover keyboard handling in the hotkeys plugin
|
||||
extensions: utils.isMobile() ? ["dnd5", "clones"] : ["hotkeys", "dnd5", "clones"],
|
||||
@@ -265,6 +332,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
const notes = this.getSelectedOrActiveNodes(node).map(node => ({
|
||||
noteId: node.data.noteId,
|
||||
branchId: node.data.branchId,
|
||||
title: node.title
|
||||
}));
|
||||
|
||||
@@ -304,17 +372,28 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
});
|
||||
}
|
||||
else {
|
||||
const jsonStr = dataTransfer.getData("text");
|
||||
let notes = null;
|
||||
|
||||
try {
|
||||
notes = JSON.parse(jsonStr);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Cannot parse ${jsonStr} into notes for drop`);
|
||||
return;
|
||||
}
|
||||
|
||||
// This function MUST be defined to enable dropping of items on the tree.
|
||||
// data.hitMode is 'before', 'after', or 'over'.
|
||||
|
||||
const selectedBranchIds = this.getSelectedOrActiveNodes().map(node => node.data.branchId);
|
||||
const selectedBranchIds = notes.map(note => note.branchId);
|
||||
|
||||
if (data.hitMode === "before") {
|
||||
branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId);
|
||||
} else if (data.hitMode === "after") {
|
||||
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
|
||||
} else if (data.hitMode === "over") {
|
||||
branchService.moveToParentNote(selectedBranchIds, node.data.noteId);
|
||||
branchService.moveToParentNote(selectedBranchIds, node.data.branchId);
|
||||
} else {
|
||||
throw new Error("Unknown hitMode=" + data.hitMode);
|
||||
}
|
||||
@@ -571,13 +650,18 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
/** @return {FancytreeNode[]} */
|
||||
getSelectedOrActiveNodes(node = null) {
|
||||
const notes = this.getSelectedNodes(true);
|
||||
const nodes = this.getSelectedNodes(true);
|
||||
|
||||
if (notes.length === 0) {
|
||||
notes.push(node ? node : this.getActiveNode());
|
||||
// the node you start dragging should be included even if not selected
|
||||
if (node && !nodes.find(n => n.key === node.key)) {
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
return notes;
|
||||
if (nodes.length === 0) {
|
||||
nodes.push(this.getActiveNode());
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async setExpandedStatusForSubtree(node, isExpanded) {
|
||||
@@ -634,17 +718,17 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
const activeContext = appContext.tabManager.getActiveTabContext();
|
||||
|
||||
if (activeContext && activeContext.notePath) {
|
||||
this.tree.setFocus();
|
||||
this.tree.setFocus(true);
|
||||
|
||||
const node = await this.expandToNote(activeContext.notePath);
|
||||
|
||||
await node.makeVisible({scrollIntoView: true});
|
||||
node.setFocus();
|
||||
node.setFocus(true);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return {FancytreeNode} */
|
||||
async getNodeFromPath(notePath, expand = false, expandOpts = {}) {
|
||||
async getNodeFromPath(notePath, expand = false) {
|
||||
utils.assertArguments(notePath);
|
||||
|
||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
@@ -673,7 +757,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
if (expand) {
|
||||
await parentNode.setExpanded(true, expandOpts);
|
||||
await parentNode.setExpanded(true);
|
||||
|
||||
// although previous line should set the expanded status, it seems to happen asynchronously
|
||||
// so we need to make sure it is set properly before calling updateNode which uses this flag
|
||||
const branch = treeCache.getBranch(parentNode.data.branchId);
|
||||
branch.isExpanded = true;
|
||||
}
|
||||
|
||||
this.updateNode(parentNode);
|
||||
@@ -713,8 +802,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
/** @return {FancytreeNode} */
|
||||
async expandToNote(notePath, expandOpts) {
|
||||
return this.getNodeFromPath(notePath, true, expandOpts);
|
||||
async expandToNote(notePath) {
|
||||
return this.getNodeFromPath(notePath, true);
|
||||
}
|
||||
|
||||
updateNode(node) {
|
||||
@@ -729,6 +818,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
node.icon = this.getIcon(note, isFolder);
|
||||
node.extraClasses = this.getExtraClasses(note);
|
||||
node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
|
||||
|
||||
if (node.isExpanded() !== branch.isExpanded) {
|
||||
node.setExpanded(branch.isExpanded, {noEvents: true});
|
||||
}
|
||||
|
||||
node.renderTitle();
|
||||
}
|
||||
|
||||
@@ -804,6 +898,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
const activeNode = this.getActiveNode();
|
||||
const activeNodeFocused = activeNode ? activeNode.hasFocus() : false;
|
||||
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
|
||||
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
|
||||
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
|
||||
@@ -933,12 +1028,16 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
node = await this.expandToNote(nextNotePath);
|
||||
|
||||
if (node) {
|
||||
this.tree.setFocus();
|
||||
node.setFocus(true);
|
||||
|
||||
await appContext.tabManager.getActiveTabContext().setNote(nextNotePath);
|
||||
}
|
||||
}
|
||||
|
||||
const newActiveNode = this.getActiveNode();
|
||||
|
||||
// return focus if the previously active node was also focused
|
||||
if (newActiveNode && activeNodeFocused) {
|
||||
newActiveNode.setFocus(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1062,7 +1161,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
const toNode = node.getPrevSibling();
|
||||
|
||||
if (toNode !== null) {
|
||||
branchService.moveToParentNote([node.data.branchId], toNode.data.noteId);
|
||||
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1147,7 +1246,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
pasteNotesFromClipboardCommand({node}) {
|
||||
clipboard.pasteInto(node.data.noteId);
|
||||
clipboard.pasteInto(node.data.branchId);
|
||||
}
|
||||
|
||||
pasteNotesAfterFromClipboard({node}) {
|
||||
|
||||
@@ -23,6 +23,7 @@ const mentionSetup = {
|
||||
row.text = row.name = row.noteTitle;
|
||||
row.id = '@' + row.text;
|
||||
row.link = '#' + row.path;
|
||||
row.notePath = row.path;
|
||||
}
|
||||
|
||||
res(rows);
|
||||
@@ -256,4 +257,4 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
this.textEditor.model.insertContent(imageElement, this.textEditor.model.document.selection);
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,23 @@ import TypeWidget from "./type_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-file note-detail-printable">
|
||||
<style>
|
||||
.file-table td {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="file-table">
|
||||
<tr>
|
||||
<th nowrap>Note ID:</th>
|
||||
<th>Note ID:</th>
|
||||
<td class="file-note-id"></td>
|
||||
<th nowrap>Original file name:</th>
|
||||
<th>Original file name:</th>
|
||||
<td class="file-filename"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th nowrap>File type:</th>
|
||||
<th>File type:</th>
|
||||
<td class="file-filetype"></td>
|
||||
<th nowrap>File size:</th>
|
||||
<th>File size:</th>
|
||||
<td class="file-filesize"></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -94,7 +100,7 @@ export default class FileTypeWidget extends TypeWidget {
|
||||
toastService.showError("Upload of a new file revision failed.");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
@@ -130,4 +136,4 @@ export default class FileTypeWidget extends TypeWidget {
|
||||
getFileUrl() {
|
||||
return utils.getUrlForDownload("api/notes/" + this.noteId + "/download");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,71 +113,6 @@ span.fancytree-node.muted { opacity: 0.6; }
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
ul.fancytree-container {
|
||||
outline: none !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.fancytree-custom-icon {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.fancytree-title {
|
||||
color: inherit !important;
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
span.fancytree-node.protected > span.fancytree-custom-icon {
|
||||
filter: drop-shadow(2px 2px 2px var(--main-text-color));
|
||||
}
|
||||
|
||||
span.fancytree-node.multiple-parents .fancytree-title::after {
|
||||
content: " *"
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
|
||||
.ui-fancytree > li > ul {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active .fancytree-title {
|
||||
font-weight: bold;
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
|
||||
color: var(--active-item-text-color) !important;
|
||||
background-color: var(--active-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-selected .fancytree-title {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--main-background-color) !important; /* invisible border */
|
||||
border-radius: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
span.fancytree-node:hover span.fancytree-title {
|
||||
border-color: var(--main-border-color) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-node.archived {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ui-autocomplete {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
@@ -873,4 +808,4 @@ body {
|
||||
|
||||
.hidden-int, .hidden-ext {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const anonymization = require('../../services/anonymization');
|
||||
|
||||
async function anonymize() {
|
||||
await anonymization.anonymize();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
anonymize
|
||||
};
|
||||
@@ -14,25 +14,33 @@ const TaskContext = require('../../services/task_context');
|
||||
*/
|
||||
|
||||
async function moveBranchToParent(req) {
|
||||
const {branchId, parentNoteId} = req.params;
|
||||
const {branchId, parentBranchId} = req.params;
|
||||
|
||||
const parentBranch = await repository.getBranch(parentBranchId);
|
||||
const branchToMove = await repository.getBranch(branchId);
|
||||
|
||||
if (branchToMove.parentNoteId === parentNoteId) {
|
||||
if (!parentBranch || !branchToMove) {
|
||||
return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`];
|
||||
}
|
||||
|
||||
if (branchToMove.parentNoteId === parentBranch.noteId) {
|
||||
return { success: true }; // no-op
|
||||
}
|
||||
|
||||
const validationResult = await treeService.validateParentChild(parentNoteId, branchToMove.noteId, branchId);
|
||||
const validationResult = await treeService.validateParentChild(parentBranch.noteId, branchToMove.noteId, branchId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return [200, validationResult];
|
||||
}
|
||||
|
||||
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentNoteId]);
|
||||
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentBranch.noteId]);
|
||||
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
|
||||
|
||||
const newBranch = branchToMove.createClone(parentNoteId, newNotePos);
|
||||
newBranch.isExpanded = true;
|
||||
// expanding so that the new placement of the branch is immediately visible
|
||||
parentBranch.isExpanded = true;
|
||||
await parentBranch.save();
|
||||
|
||||
const newBranch = branchToMove.createClone(parentBranch.noteId, newNotePos);
|
||||
await newBranch.save();
|
||||
|
||||
branchToMove.isDeleted = true;
|
||||
@@ -117,6 +125,7 @@ async function setExpanded(req) {
|
||||
if (branchId !== 'root') {
|
||||
await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
|
||||
// we don't sync expanded label
|
||||
// also this does not trigger updates to the frontend, this would trigger too many reloads
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,4 +187,4 @@ module.exports = {
|
||||
setExpandedForSubtree,
|
||||
deleteBranch,
|
||||
setPrefix
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
const cloningService = require('../../services/cloning');
|
||||
|
||||
async function cloneNoteToParent(req) {
|
||||
const {noteId, parentNoteId} = req.params;
|
||||
const {noteId, parentBranchId} = req.params;
|
||||
const {prefix} = req.body;
|
||||
|
||||
return await cloningService.cloneNoteToParent(noteId, parentNoteId, prefix);
|
||||
return await cloningService.cloneNoteToParent(noteId, parentBranchId, prefix);
|
||||
}
|
||||
|
||||
async function cloneNoteAfter(req) {
|
||||
@@ -18,4 +18,4 @@ async function cloneNoteAfter(req) {
|
||||
module.exports = {
|
||||
cloneNoteToParent,
|
||||
cloneNoteAfter
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,8 +2,19 @@
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const log = require('../../services/log');
|
||||
const backupService = require('../../services/backup');
|
||||
const consistencyChecksService = require('../../services/consistency_checks');
|
||||
|
||||
async function anonymize() {
|
||||
return await backupService.anonymize();
|
||||
}
|
||||
|
||||
async function backupDatabase() {
|
||||
return {
|
||||
backupFile: await backupService.backupNow("now")
|
||||
};
|
||||
}
|
||||
|
||||
async function vacuumDatabase() {
|
||||
await sql.execute("VACUUM");
|
||||
|
||||
@@ -15,6 +26,8 @@ async function findAndFixConsistencyIssues() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
backupDatabase,
|
||||
vacuumDatabase,
|
||||
findAndFixConsistencyIssues
|
||||
};
|
||||
findAndFixConsistencyIssues,
|
||||
anonymize
|
||||
};
|
||||
@@ -16,7 +16,7 @@ const ApiToken = require('../../entities/api_token');
|
||||
|
||||
async function loginSync(req) {
|
||||
if (!await sqlInit.schemaExists()) {
|
||||
return [400, { message: "DB schema does not exist, can't sync." }];
|
||||
return [500, { message: "DB schema does not exist, can't sync." }];
|
||||
}
|
||||
|
||||
const timestampStr = req.body.timestamp;
|
||||
@@ -27,7 +27,7 @@ async function loginSync(req) {
|
||||
|
||||
// login token is valid for 5 minutes
|
||||
if (Math.abs(timestamp.getTime() - now.getTime()) > 5 * 60 * 1000) {
|
||||
return [400, { message: 'Auth request time is out of sync, please check that both client and server have correct time.' }];
|
||||
return [401, { message: 'Auth request time is out of sync, please check that both client and server have correct time.' }];
|
||||
}
|
||||
|
||||
const syncVersion = req.body.syncVersion;
|
||||
@@ -102,4 +102,4 @@ module.exports = {
|
||||
loginSync,
|
||||
loginToProtectedSession,
|
||||
token
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,8 +24,7 @@ const exportRoute = require('./api/export');
|
||||
const importRoute = require('./api/import');
|
||||
const setupApiRoute = require('./api/setup');
|
||||
const sqlRoute = require('./api/sql');
|
||||
const anonymizationRoute = require('./api/anonymization');
|
||||
const cleanupRoute = require('./api/cleanup');
|
||||
const databaseRoute = require('./api/database');
|
||||
const imageRoute = require('./api/image');
|
||||
const attributesRoute = require('./api/attributes');
|
||||
const scriptRoute = require('./api/script');
|
||||
@@ -123,7 +122,7 @@ function register(app) {
|
||||
apiRoute(POST, '/api/tree/load', treeApiRoute.load);
|
||||
apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix);
|
||||
|
||||
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentNoteId', branchesApiRoute.moveBranchToParent);
|
||||
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent);
|
||||
apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote);
|
||||
apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote);
|
||||
apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
|
||||
@@ -152,7 +151,7 @@ function register(app) {
|
||||
|
||||
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
|
||||
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentBranchId', cloningApiRoute.cloneNoteToParent);
|
||||
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
|
||||
|
||||
route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||
@@ -220,12 +219,15 @@ function register(app) {
|
||||
|
||||
apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema);
|
||||
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
|
||||
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
|
||||
route(POST, '/api/database/anonymize', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
|
||||
|
||||
// backup requires execution outside of transaction
|
||||
route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);
|
||||
|
||||
// VACUUM requires execution outside of transaction
|
||||
route(POST, '/api/cleanup/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.vacuumDatabase, apiResultHandler, false);
|
||||
route(POST, '/api/database/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false);
|
||||
|
||||
route(POST, '/api/cleanup/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.findAndFixConsistencyIssues, apiResultHandler, false);
|
||||
route(POST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false);
|
||||
|
||||
apiRoute(POST, '/api/script/exec', scriptRoute.exec);
|
||||
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
|
||||
@@ -267,4 +269,4 @@ function register(app) {
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const dataDir = require('./data_dir');
|
||||
const dateUtils = require('./date_utils');
|
||||
const fs = require('fs-extra');
|
||||
const sqlite = require('sqlite');
|
||||
|
||||
async function anonymize() {
|
||||
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
|
||||
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
|
||||
}
|
||||
|
||||
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
|
||||
|
||||
fs.copySync(dataDir.DOCUMENT_PATH, anonymizedFile);
|
||||
|
||||
const db = await sqlite.open(anonymizedFile, {Promise});
|
||||
|
||||
await db.run("UPDATE notes SET title = 'title'");
|
||||
await db.run("UPDATE note_contents SET content = 'text'");
|
||||
await db.run("UPDATE note_revisions SET title = 'title'");
|
||||
await db.run("UPDATE note_revision_contents SET content = 'title'");
|
||||
await db.run("UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'");
|
||||
await db.run("UPDATE attributes SET name = 'name' WHERE type = 'relation'");
|
||||
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
|
||||
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
|
||||
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
|
||||
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);
|
||||
await db.run("VACUUM");
|
||||
|
||||
await db.close();
|
||||
|
||||
return anonymizedFile;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
anonymize
|
||||
};
|
||||
@@ -8,6 +8,8 @@ const log = require('./log');
|
||||
const sqlInit = require('./sql_init');
|
||||
const syncMutexService = require('./sync_mutex');
|
||||
const cls = require('./cls');
|
||||
const sqlite = require('sqlite');
|
||||
const sqlite3 = require('sqlite3');
|
||||
|
||||
async function regularBackup() {
|
||||
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
|
||||
@@ -28,28 +30,103 @@ async function periodBackup(optionName, fileName, periodInSeconds) {
|
||||
}
|
||||
}
|
||||
|
||||
const COPY_ATTEMPT_COUNT = 50;
|
||||
|
||||
async function copyFile(backupFile) {
|
||||
const sql = require('./sql');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(backupFile);
|
||||
} catch (e) {
|
||||
} // unlink throws exception if the file did not exist
|
||||
|
||||
let success = false;
|
||||
let attemptCount = 0
|
||||
|
||||
for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) {
|
||||
try {
|
||||
await sql.executeNoWrap(`VACUUM INTO '${backupFile}'`);
|
||||
|
||||
success = true;
|
||||
} catch (e) {
|
||||
log.info(`Copy DB attempt ${attemptCount + 1} failed with "${e.message}", retrying...`);
|
||||
}
|
||||
// we re-try since VACUUM is very picky and it can't run if there's any other query currently running
|
||||
// which is difficult to guarantee so we just re-try
|
||||
}
|
||||
|
||||
return attemptCount !== COPY_ATTEMPT_COUNT;
|
||||
}
|
||||
|
||||
async function backupNow(name) {
|
||||
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
|
||||
await syncMutexService.doExclusively(async () => {
|
||||
return await syncMutexService.doExclusively(async () => {
|
||||
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
|
||||
|
||||
fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
|
||||
const success = await copyFile(backupFile);
|
||||
|
||||
log.info("Created backup at " + backupFile);
|
||||
if (success) {
|
||||
log.info("Created backup at " + backupFile);
|
||||
}
|
||||
else {
|
||||
log.error(`Creating backup ${backupFile} failed`);
|
||||
}
|
||||
|
||||
return backupFile;
|
||||
});
|
||||
}
|
||||
|
||||
async function anonymize() {
|
||||
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
|
||||
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
|
||||
}
|
||||
|
||||
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
|
||||
|
||||
const success = await copyFile(anonymizedFile);
|
||||
|
||||
if (!success) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const db = await sqlite.open({
|
||||
filename: anonymizedFile,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
|
||||
await db.run("UPDATE api_tokens SET token = 'API token value'");
|
||||
await db.run("UPDATE notes SET title = 'title'");
|
||||
await db.run("UPDATE note_contents SET content = 'text'");
|
||||
await db.run("UPDATE note_revisions SET title = 'title'");
|
||||
await db.run("UPDATE note_revision_contents SET content = 'title'");
|
||||
await db.run("UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'");
|
||||
await db.run("UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name != 'template'");
|
||||
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
|
||||
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
|
||||
('documentId', 'documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
|
||||
'passwordVerificationSalt', 'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')`);
|
||||
await db.run("VACUUM");
|
||||
|
||||
await db.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
anonymizedFilePath: anonymizedFile
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
|
||||
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
|
||||
}
|
||||
|
||||
sqlInit.dbReady.then(() => {
|
||||
setInterval(cls.wrap(regularBackup), 60 * 60 * 1000);
|
||||
setInterval(cls.wrap(regularBackup), 4 * 60 * 60 * 1000);
|
||||
|
||||
// kickoff backup immediately
|
||||
setTimeout(cls.wrap(regularBackup), 1000);
|
||||
// kickoff first backup soon after start up
|
||||
setTimeout(cls.wrap(regularBackup), 5 * 60 * 1000);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
backupNow
|
||||
};
|
||||
backupNow,
|
||||
anonymize
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2020-05-20T08:54:55+02:00", buildRevision: "04c573e212db06e1dd60c74707e40f6912c85aab" };
|
||||
module.exports = { buildDate:"2020-06-03T14:30:07+02:00", buildRevision: "c1fd9825aa6087b5061cdede5dba3f7f9dc62c31" };
|
||||
|
||||
@@ -9,12 +9,14 @@ const Branch = require('../entities/branch');
|
||||
const TaskContext = require("./task_context.js");
|
||||
const utils = require('./utils');
|
||||
|
||||
async function cloneNoteToParent(noteId, parentNoteId, prefix) {
|
||||
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentNoteId)) {
|
||||
async function cloneNoteToParent(noteId, parentBranchId, prefix) {
|
||||
const parentBranch = await repository.getBranch(parentBranchId);
|
||||
|
||||
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentBranch.noteId)) {
|
||||
return { success: false, message: 'Note is deleted.' };
|
||||
}
|
||||
|
||||
const validationResult = await treeService.validateParentChild(parentNoteId, noteId);
|
||||
const validationResult = await treeService.validateParentChild(parentBranch.noteId, noteId);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return validationResult;
|
||||
@@ -22,12 +24,13 @@ async function cloneNoteToParent(noteId, parentNoteId, prefix) {
|
||||
|
||||
const branch = await new Branch({
|
||||
noteId: noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
parentNoteId: parentBranch.noteId,
|
||||
prefix: prefix,
|
||||
isExpanded: 0
|
||||
}).save();
|
||||
|
||||
await sql.execute("UPDATE branches SET isExpanded = 1 WHERE noteId = ?", [parentNoteId]);
|
||||
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
|
||||
await parentBranch.save();
|
||||
|
||||
return { success: true, branchId: branch.branchId };
|
||||
}
|
||||
@@ -111,4 +114,4 @@ module.exports = {
|
||||
ensureNoteIsAbsentFromParent,
|
||||
toggleNoteInParent,
|
||||
cloneNoteAfter
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
{
|
||||
actionName: "scrollToActiveNote",
|
||||
defaultShortcuts: ["CommandOrControl+."],
|
||||
scope: "window" // FIXME - how do we find what note tree should be updated?
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
actionName: "searchNotes",
|
||||
@@ -55,7 +55,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
actionName: "collapseTree",
|
||||
defaultShortcuts: ["Alt+C"],
|
||||
description: "Collapses the complete note tree",
|
||||
scope: "note-tree"
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
actionName: "collapseSubtree",
|
||||
@@ -425,4 +425,4 @@ async function getKeyboardActions() {
|
||||
module.exports = {
|
||||
DEFAULT_KEYBOARD_ACTIONS,
|
||||
getKeyboardActions
|
||||
};
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ async function createNewNote(params) {
|
||||
const parentNote = await repository.getNote(params.parentNoteId);
|
||||
|
||||
if (!parentNote) {
|
||||
throw new Error(`Parent note ${params.parentNoteId} not found.`);
|
||||
throw new Error(`Parent note "${params.parentNoteId}" not found.`);
|
||||
}
|
||||
|
||||
if (!params.title || params.title.trim().length === 0) {
|
||||
@@ -662,7 +662,9 @@ async function scanForLinks(note) {
|
||||
const content = await note.getContent();
|
||||
const newContent = await saveLinks(note, content);
|
||||
|
||||
await note.setContent(newContent);
|
||||
if (content !== newContent) {
|
||||
await note.setContent(newContent);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Could not scan for links note ${note.noteId}: ${e.message} ${e.stack}`);
|
||||
|
||||
@@ -153,6 +153,10 @@ async function execute(query, params = []) {
|
||||
return await wrap(async db => db.run(query, ...params), query);
|
||||
}
|
||||
|
||||
async function executeNoWrap(query, params = []) {
|
||||
await dbConnection.run(query, ...params);
|
||||
}
|
||||
|
||||
async function executeMany(query, params) {
|
||||
// essentially just alias
|
||||
await getManyRows(query, params);
|
||||
@@ -264,6 +268,7 @@ module.exports = {
|
||||
getMap,
|
||||
getColumn,
|
||||
execute,
|
||||
executeNoWrap,
|
||||
executeMany,
|
||||
executeScript,
|
||||
transactional,
|
||||
|
||||
Reference in New Issue
Block a user