mirror of
https://github.com/zadam/trilium.git
synced 2025-10-30 01:36:24 +01:00
Compare commits
25 Commits
v0.37.1-be
...
v0.37.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b30291601 | ||
|
|
5193f073e9 | ||
|
|
6c7d8a9667 | ||
|
|
5e9bedd903 | ||
|
|
e712990c03 | ||
|
|
91487b338a | ||
|
|
3ff24d53e5 | ||
|
|
94c904fb40 | ||
|
|
5f258fbbbf | ||
|
|
bf9ad976b9 | ||
|
|
434d8ef48c | ||
|
|
c8ba07a4ae | ||
|
|
38e7649ac3 | ||
|
|
7a2c7edd7e | ||
|
|
cfb850acb2 | ||
|
|
a16aaf7a81 | ||
|
|
522f71cb91 | ||
|
|
d357943ebb | ||
|
|
07043fb177 | ||
|
|
1f8d382b1f | ||
|
|
61e8cbbcba | ||
|
|
86c5dd6494 | ||
|
|
c5acb7fc9b | ||
|
|
834e1f7253 | ||
|
|
1a87190f43 |
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<dataSource name="document.db">
|
<dataSource name="document.db">
|
||||||
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.16">
|
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.17">
|
||||||
<root id="1">
|
<root id="1">
|
||||||
<ServerVersion>3.25.1</ServerVersion>
|
<ServerVersion>3.25.1</ServerVersion>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ SELECT noteId, title, -1, isProtected, type, mime, hash, isDeleted, isErased, da
|
|||||||
DROP TABLE notes;
|
DROP TABLE notes;
|
||||||
ALTER TABLE notes_mig RENAME TO notes;
|
ALTER TABLE notes_mig RENAME TO notes;
|
||||||
|
|
||||||
UPDATE notes SET contentLength = (SELECT COALESCE(LENGTH(content), 0) FROM note_contents WHERE note_contents.noteId = notes.noteId);
|
UPDATE notes SET contentLength = COALESCE((SELECT COALESCE(LENGTH(content), 0) FROM note_contents WHERE note_contents.noteId = notes.noteId), -1);
|
||||||
|
|
||||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
|
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (`isDeleted`);
|
||||||
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
|
CREATE INDEX `IDX_notes_title` ON `notes` (`title`);
|
||||||
|
|||||||
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
@@ -498,7 +498,7 @@
|
|||||||
t._started = false;
|
t._started = false;
|
||||||
onRenderStop();
|
onRenderStop();
|
||||||
} else {
|
} else {
|
||||||
setImmediate(step);
|
requestIdleCallback(step, { timeout: 10 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"version": "0.36.5",
|
"version": "0.37.5",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"productName": "Trilium Notes",
|
"productName": "Trilium Notes",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.37.1-beta",
|
"version": "0.37.6",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "electron.js",
|
"main": "electron.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -788,6 +788,7 @@ class Note extends Entity {
|
|||||||
delete pojo.isContentAvailable;
|
delete pojo.isContentAvailable;
|
||||||
delete pojo.__attributeCache;
|
delete pojo.__attributeCache;
|
||||||
delete pojo.content;
|
delete pojo.content;
|
||||||
|
/** zero references to contentHash, probably can be removed */
|
||||||
delete pojo.contentHash;
|
delete pojo.contentHash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ $form.on('submit', () => {
|
|||||||
function exportBranch(branchId, type, format, version) {
|
function exportBranch(branchId, type, format, version) {
|
||||||
taskId = utils.randomString(10);
|
taskId = utils.randomString(10);
|
||||||
|
|
||||||
const url = utils.getHost() + `/api/notes/${branchId}/export/${type}/${format}/${version}/${taskId}`;
|
const url = utils.getUrlForDownload(`api/notes/${branchId}/export/${type}/${format}/${version}/${taskId}`);
|
||||||
|
|
||||||
utils.download(url);
|
utils.download(url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ async function setContentPane() {
|
|||||||
const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
|
const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
|
||||||
|
|
||||||
$downloadButton.on('click', () => {
|
$downloadButton.on('click', () => {
|
||||||
utils.download(utils.getHost() + `/api/notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}/download`);
|
const url = utils.getUrlForDownload(`api/notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}/download`);
|
||||||
|
|
||||||
|
utils.download(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
$titleButtons.append($downloadButton);
|
$titleButtons.append($downloadButton);
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default class SidebarOptions {
|
|||||||
this.$sidebarMinWidth.val(options.sidebarMinWidth);
|
this.$sidebarMinWidth.val(options.sidebarMinWidth);
|
||||||
this.$sidebarWidthPercent.val(options.sidebarWidthPercent);
|
this.$sidebarWidthPercent.val(options.sidebarWidthPercent);
|
||||||
|
|
||||||
if (parseInt(options.showSidebarInNewTab)) {
|
if (options.showSidebarInNewTab === 'true') {
|
||||||
this.$showSidebarInNewTab.attr("checked", "checked");
|
this.$showSidebarInNewTab.attr("checked", "checked");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -4,83 +4,97 @@ import cloningService from "./cloning.js";
|
|||||||
import toastService from "./toast.js";
|
import toastService from "./toast.js";
|
||||||
import hoistedNoteService from "./hoisted_note.js";
|
import hoistedNoteService from "./hoisted_note.js";
|
||||||
|
|
||||||
let clipboardIds = [];
|
/*
|
||||||
|
* Clipboard contains node keys which are not stable. If a (part of the) tree is reloaded,
|
||||||
|
* node keys in the clipboard might not exist anymore. Code here should then be ready to deal
|
||||||
|
* with this.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let clipboardNodeKeys = [];
|
||||||
let clipboardMode = null;
|
let clipboardMode = null;
|
||||||
|
|
||||||
async function pasteAfter(node) {
|
async function pasteAfter(afterNode) {
|
||||||
|
if (isClipboardEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (clipboardMode === 'cut') {
|
if (clipboardMode === 'cut') {
|
||||||
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
|
const nodes = clipboardNodeKeys.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
|
||||||
|
|
||||||
await treeChangesService.moveAfterNode(nodes, node);
|
await treeChangesService.moveAfterNode(nodes, afterNode);
|
||||||
|
|
||||||
clipboardIds = [];
|
clipboardNodeKeys = [];
|
||||||
clipboardMode = null;
|
clipboardMode = null;
|
||||||
}
|
}
|
||||||
else if (clipboardMode === 'copy') {
|
else if (clipboardMode === 'copy') {
|
||||||
for (const noteId of clipboardIds) {
|
for (const nodeKey of clipboardNodeKeys) {
|
||||||
await cloningService.cloneNoteAfter(noteId, node.data.branchId);
|
const clipNode = treeUtils.getNodeByKey(nodeKey);
|
||||||
|
|
||||||
|
await cloningService.cloneNoteAfter(clipNode.data.noteId, afterNode.data.branchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
||||||
}
|
}
|
||||||
else if (clipboardIds.length === 0) {
|
|
||||||
// just do nothing
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
toastService.throwError("Unrecognized clipboard mode=" + clipboardMode);
|
toastService.throwError("Unrecognized clipboard mode=" + clipboardMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pasteInto(node) {
|
async function pasteInto(parentNode) {
|
||||||
|
if (isClipboardEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (clipboardMode === 'cut') {
|
if (clipboardMode === 'cut') {
|
||||||
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
|
const nodes = clipboardNodeKeys.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
|
||||||
|
|
||||||
await treeChangesService.moveToNode(nodes, node);
|
await treeChangesService.moveToNode(nodes, parentNode);
|
||||||
|
|
||||||
await node.setExpanded(true);
|
await parentNode.setExpanded(true);
|
||||||
|
|
||||||
clipboardIds = [];
|
clipboardNodeKeys = [];
|
||||||
clipboardMode = null;
|
clipboardMode = null;
|
||||||
}
|
}
|
||||||
else if (clipboardMode === 'copy') {
|
else if (clipboardMode === 'copy') {
|
||||||
for (const noteId of clipboardIds) {
|
for (const nodeKey of clipboardNodeKeys) {
|
||||||
await cloningService.cloneNoteTo(noteId, node.data.noteId);
|
const clipNode = treeUtils.getNodeByKey(nodeKey);
|
||||||
|
|
||||||
|
await cloningService.cloneNoteTo(clipNode.data.noteId, parentNode.data.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await node.setExpanded(true);
|
await parentNode.setExpanded(true);
|
||||||
|
|
||||||
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
||||||
}
|
}
|
||||||
else if (clipboardIds.length === 0) {
|
|
||||||
// just do nothing
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
toastService.throwError("Unrecognized clipboard mode=" + mode);
|
toastService.throwError("Unrecognized clipboard mode=" + mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copy(nodes) {
|
function copy(nodes) {
|
||||||
clipboardIds = nodes.map(node => node.data.noteId);
|
clipboardNodeKeys = nodes.map(node => node.key);
|
||||||
clipboardMode = 'copy';
|
clipboardMode = 'copy';
|
||||||
|
|
||||||
toastService.showMessage("Note(s) have been copied into clipboard.");
|
toastService.showMessage("Note(s) have been copied into clipboard.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function cut(nodes) {
|
function cut(nodes) {
|
||||||
clipboardIds = nodes
|
clipboardNodeKeys = nodes
|
||||||
.filter(node => node.data.noteId !== hoistedNoteService.getHoistedNoteNoPromise())
|
.filter(node => node.data.noteId !== hoistedNoteService.getHoistedNoteNoPromise())
|
||||||
.filter(node => node.getParent().data.noteType !== 'search')
|
.filter(node => node.getParent().data.noteType !== 'search')
|
||||||
.map(node => node.data.noteId);
|
.map(node => node.key);
|
||||||
|
|
||||||
if (clipboardIds.length > 0) {
|
if (clipboardNodeKeys.length > 0) {
|
||||||
clipboardMode = 'cut';
|
clipboardMode = 'cut';
|
||||||
|
|
||||||
toastService.showMessage("Note(s) have been cut into clipboard.");
|
toastService.showMessage("Note(s) have been cut into clipboard.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEmpty() {
|
function isClipboardEmpty() {
|
||||||
return clipboardIds.length === 0;
|
clipboardNodeKeys = clipboardNodeKeys.filter(key => !!treeUtils.getNodeByKey(key));
|
||||||
|
|
||||||
|
return clipboardNodeKeys.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -88,5 +102,5 @@ export default {
|
|||||||
pasteInto,
|
pasteInto,
|
||||||
cut,
|
cut,
|
||||||
copy,
|
copy,
|
||||||
isEmpty
|
isClipboardEmpty
|
||||||
}
|
}
|
||||||
@@ -273,7 +273,9 @@ async function filterTabs(noteId) {
|
|||||||
|
|
||||||
async function noteDeleted(noteId) {
|
async function noteDeleted(noteId) {
|
||||||
for (const tc of tabContexts) {
|
for (const tc of tabContexts) {
|
||||||
if (tc.notePath && tc.notePath.split("/").includes(noteId)) {
|
// not removing active even if it contains deleted note since that one will move to another note (handled by deletion logic)
|
||||||
|
// and we would lose tab context state (e.g. sidebar visibility)
|
||||||
|
if (!tc.isActive() && tc.notePath && tc.notePath.split("/").includes(noteId)) {
|
||||||
await tabRow.removeTab(tc.$tab[0]);
|
await tabRow.removeTab(tc.$tab[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,8 +185,7 @@ class NoteDetailBook {
|
|||||||
}
|
}
|
||||||
else if (type === 'file') {
|
else if (type === 'file') {
|
||||||
function getFileUrl() {
|
function getFileUrl() {
|
||||||
// electron needs absolute URL so we extract current host, port, protocol
|
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
|
||||||
return utils.getHost() + "/api/notes/" + note.noteId + "/download";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
|
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
|
||||||
|
|||||||
@@ -87,8 +87,7 @@ class NoteDetailFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFileUrl() {
|
getFileUrl() {
|
||||||
// electron needs absolute URL so we extract current host, port, protocol
|
return utils.getUrlForDownload("api/notes/" + this.ctx.note.noteId + "/download");
|
||||||
return utils.getHost() + "/api/notes/" + this.ctx.note.noteId + "/download";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {}
|
show() {}
|
||||||
|
|||||||
@@ -98,8 +98,7 @@ class NoteDetailImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFileUrl() {
|
getFileUrl() {
|
||||||
// electron needs absolute URL so we extract current host, port, protocol
|
return utils.getUrlForDownload(`api/notes/${this.ctx.note.noteId}/download`);
|
||||||
return utils.getHost() + `/api/notes/${this.ctx.note.noteId}/download`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {}
|
show() {}
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ async function refreshSearch() {
|
|||||||
toastService.showMessage("Saved search note refreshed.");
|
toastService.showMessage("Saved search note refreshed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function searchInSubtree(noteId) {
|
||||||
|
showSearch();
|
||||||
|
|
||||||
|
$searchInput.val(`@in=${noteId} @text*=*`);
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
const hashValue = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
|
const hashValue = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
|
||||||
|
|
||||||
@@ -178,5 +184,6 @@ export default {
|
|||||||
refreshSearch,
|
refreshSearch,
|
||||||
doSearch,
|
doSearch,
|
||||||
init,
|
init,
|
||||||
|
searchInSubtree,
|
||||||
getHelpText: () => helpText
|
getHelpText: () => helpText
|
||||||
};
|
};
|
||||||
@@ -246,11 +246,15 @@ class TabContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCurrentNotePathToHash() {
|
setCurrentNotePathToHash() {
|
||||||
if (this.$tab[0] === this.tabRow.activeTabEl) {
|
if (this.isActive()) {
|
||||||
document.location.hash = (this.notePath || "") + "-" + this.tabId;
|
document.location.hash = (this.notePath || "") + "-" + this.tabId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isActive() {
|
||||||
|
return this.$tab[0] === this.tabRow.activeTabEl;
|
||||||
|
}
|
||||||
|
|
||||||
setupClasses() {
|
setupClasses() {
|
||||||
for (const clazz of Array.from(this.$tab[0].classList)) { // create copy to safely iterate over while removing classes
|
for (const clazz of Array.from(this.$tab[0].classList)) { // create copy to safely iterate over while removing classes
|
||||||
if (clazz !== 'note-tab') {
|
if (clazz !== 'note-tab') {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class TreeCache {
|
|||||||
else {
|
else {
|
||||||
return this.notes[noteId];
|
return this.notes[noteId];
|
||||||
}
|
}
|
||||||
}).filter(note => note !== null);
|
}).filter(note => !!note);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return {Promise<boolean>} */
|
/** @return {Promise<boolean>} */
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import hoistedNoteService from './hoisted_note.js';
|
|||||||
import noteDetailService from './note_detail.js';
|
import noteDetailService from './note_detail.js';
|
||||||
import clipboard from './clipboard.js';
|
import clipboard from './clipboard.js';
|
||||||
import protectedSessionHolder from "./protected_session_holder.js";
|
import protectedSessionHolder from "./protected_session_holder.js";
|
||||||
|
import searchNotesService from "./search_notes.js";
|
||||||
|
|
||||||
class TreeContextMenu {
|
class TreeContextMenu {
|
||||||
constructor(node) {
|
constructor(node) {
|
||||||
@@ -41,7 +42,7 @@ class TreeContextMenu {
|
|||||||
|| (selNodes.length === 1 && selNodes[0] === this.node);
|
|| (selNodes.length === 1 && selNodes[0] === this.node);
|
||||||
|
|
||||||
const notSearch = note.type !== 'search';
|
const notSearch = note.type !== 'search';
|
||||||
const parentNotSearch = parentNote.type !== 'search';
|
const parentNotSearch = !parentNote || parentNote.type !== 'search';
|
||||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -55,6 +56,8 @@ class TreeContextMenu {
|
|||||||
{ title: "Delete <kbd>Delete</kbd>", cmd: "delete", uiIcon: "trash",
|
{ title: "Delete <kbd>Delete</kbd>", cmd: "delete", uiIcon: "trash",
|
||||||
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
||||||
{ title: "----" },
|
{ title: "----" },
|
||||||
|
{ title: "Search in subtree <kbd>Ctrl+Shift+S</kbd>", cmd: "searchInSubtree", uiIcon: "search",
|
||||||
|
enabled: notSearch && noSelectedNotes },
|
||||||
isHoisted ? null : { title: "Hoist note <kbd>Ctrl-H</kbd>", cmd: "hoist", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
isHoisted ? null : { title: "Hoist note <kbd>Ctrl-H</kbd>", cmd: "hoist", uiIcon: "empty", enabled: noSelectedNotes && notSearch },
|
||||||
!isHoisted || !isNotRoot ? null : { title: "Unhoist note <kbd>Ctrl-H</kbd>", cmd: "unhoist", uiIcon: "arrow-up" },
|
!isHoisted || !isNotRoot ? null : { title: "Unhoist note <kbd>Ctrl-H</kbd>", cmd: "unhoist", uiIcon: "arrow-up" },
|
||||||
{ title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "empty",
|
{ title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "empty",
|
||||||
@@ -72,11 +75,11 @@ class TreeContextMenu {
|
|||||||
{ title: "Move to ... <kbd>Ctrl+Shift+X</kbd>", cmd: "moveTo", uiIcon: "empty",
|
{ title: "Move to ... <kbd>Ctrl+Shift+X</kbd>", cmd: "moveTo", uiIcon: "empty",
|
||||||
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
enabled: isNotRoot && !isHoisted && parentNotSearch },
|
||||||
{ title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "paste",
|
{ title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "paste",
|
||||||
enabled: !clipboard.isEmpty() && notSearch && noSelectedNotes },
|
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
|
||||||
{ title: "Paste after", cmd: "pasteAfter", uiIcon: "paste",
|
{ title: "Paste after", cmd: "pasteAfter", uiIcon: "paste",
|
||||||
enabled: !clipboard.isEmpty() && isNotRoot && parentNotSearch && noSelectedNotes },
|
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
|
||||||
{ title: "Duplicate note here", cmd: "duplicateNote", uiIcon: "empty",
|
{ title: "Duplicate note here", cmd: "duplicateNote", uiIcon: "empty",
|
||||||
enabled: noSelectedNotes && parentNotSearch && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
|
enabled: noSelectedNotes && parentNotSearch && isNotRoot && !isHoisted && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
|
||||||
{ title: "----" },
|
{ title: "----" },
|
||||||
{ title: "Export", cmd: "export", uiIcon: "empty",
|
{ title: "Export", cmd: "export", uiIcon: "empty",
|
||||||
enabled: notSearch && noSelectedNotes },
|
enabled: notSearch && noSelectedNotes },
|
||||||
@@ -177,6 +180,9 @@ class TreeContextMenu {
|
|||||||
|
|
||||||
treeService.duplicateNote(this.node.data.noteId, branch.parentNoteId);
|
treeService.duplicateNote(this.node.data.noteId, branch.parentNoteId);
|
||||||
}
|
}
|
||||||
|
else if (cmd === "searchInSubtree") {
|
||||||
|
searchNotesService.searchInSubtree(this.node.data.noteId);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
ws.logError("Unknown command: " + cmd);
|
ws.logError("Unknown command: " + cmd);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import treeService from "./tree.js";
|
|||||||
import hoistedNoteService from "./hoisted_note.js";
|
import hoistedNoteService from "./hoisted_note.js";
|
||||||
import clipboard from "./clipboard.js";
|
import clipboard from "./clipboard.js";
|
||||||
import treeCache from "./tree_cache.js";
|
import treeCache from "./tree_cache.js";
|
||||||
|
import searchNoteService from "./search_notes.js";
|
||||||
|
|
||||||
const keyBindings = {
|
const keyBindings = {
|
||||||
"del": node => {
|
"del": node => {
|
||||||
@@ -167,6 +168,11 @@ const keyBindings = {
|
|||||||
"down": node => {
|
"down": node => {
|
||||||
node.navigate($.ui.keyCode.DOWN, true).then(treeService.clearSelectedNodes);
|
node.navigate($.ui.keyCode.DOWN, true).then(treeService.clearSelectedNodes);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
"ctrl+shift+s": node => {
|
||||||
|
searchNoteService.searchInSubtree(node.data.noteId);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -214,6 +214,20 @@ async function clearBrowserCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param url - should be without initial slash!!!
|
||||||
|
*/
|
||||||
|
function getUrlForDownload(url) {
|
||||||
|
if (isElectron()) {
|
||||||
|
// electron needs absolute URL so we extract current host, port, protocol
|
||||||
|
return getHost() + '/' + url;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// web server can be deployed on subdomain so we need to use relative path
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
reloadApp,
|
reloadApp,
|
||||||
parseDate,
|
parseDate,
|
||||||
@@ -230,7 +244,6 @@ export default {
|
|||||||
escapeHtml,
|
escapeHtml,
|
||||||
stopWatch,
|
stopWatch,
|
||||||
formatLabel,
|
formatLabel,
|
||||||
getHost,
|
|
||||||
download,
|
download,
|
||||||
toObject,
|
toObject,
|
||||||
randomString,
|
randomString,
|
||||||
@@ -245,5 +258,6 @@ export default {
|
|||||||
getMimeTypeClass,
|
getMimeTypeClass,
|
||||||
closeActiveDialog,
|
closeActiveDialog,
|
||||||
isHtmlEmpty,
|
isHtmlEmpty,
|
||||||
clearBrowserCache
|
clearBrowserCache,
|
||||||
|
getUrlForDownload
|
||||||
};
|
};
|
||||||
@@ -127,11 +127,13 @@ async function consumeSyncData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
|
const loc = window.location;
|
||||||
|
const webSocketUri = (loc.protocol === "https:" ? "wss:" : "ws:")
|
||||||
|
+ "//" + loc.host + loc.pathname;
|
||||||
|
|
||||||
// use wss for secure messaging
|
// use wss for secure messaging
|
||||||
const ws = new WebSocket(protocol + "://" + location.host);
|
const ws = new WebSocket(webSocketUri);
|
||||||
ws.onopen = () => console.debug(utils.now(), "Connected to server with WebSocket");
|
ws.onopen = () => console.debug(utils.now(), `Connected to server ${webSocketUri} with WebSocket`);
|
||||||
ws.onmessage = handleMessage;
|
ws.onmessage = handleMessage;
|
||||||
// we're not handling ws.onclose here because reconnection is done in sendPing()
|
// we're not handling ws.onclose here because reconnection is done in sendPing()
|
||||||
|
|
||||||
|
|||||||
@@ -76,12 +76,12 @@ function SetupModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// not using server.js because it loads too many dependencies
|
// not using server.js because it loads too many dependencies
|
||||||
$.post('/api/setup/new-document', {
|
$.post('api/setup/new-document', {
|
||||||
username: username,
|
username: username,
|
||||||
password: password1,
|
password: password1,
|
||||||
theme: theme
|
theme: theme
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
window.location.replace("/");
|
window.location.replace("./");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (this.setupType() === 'sync-from-server') {
|
else if (this.setupType() === 'sync-from-server') {
|
||||||
@@ -128,10 +128,10 @@ function SetupModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkOutstandingSyncs() {
|
async function checkOutstandingSyncs() {
|
||||||
const { stats, initialized } = await $.get('/api/sync/stats');
|
const { stats, initialized } = await $.get('api/sync/stats');
|
||||||
|
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
window.location.replace("/");
|
window.location.replace("./");
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
|
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ body {
|
|||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#context-menu-container {
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: auto; /* make it scrollable when exceeding total height of the window */
|
||||||
|
}
|
||||||
|
|
||||||
#context-menu-container, #context-menu-container .dropdown-menu {
|
#context-menu-container, #context-menu-container .dropdown-menu {
|
||||||
padding: 3px 0 0;
|
padding: 3px 0 0;
|
||||||
z-index: 1111;
|
z-index: 1111;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ async function getRecentChanges() {
|
|||||||
FROM
|
FROM
|
||||||
note_revisions
|
note_revisions
|
||||||
JOIN notes USING(noteId)
|
JOIN notes USING(noteId)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
utcDateCreated DESC
|
note_revisions.utcDateCreated DESC
|
||||||
LIMIT 1000
|
LIMIT 1000
|
||||||
)
|
)
|
||||||
UNION ALL SELECT * FROM (
|
UNION ALL SELECT * FROM (
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
module.exports = { buildDate:"2019-11-16T19:09:52+01:00", buildRevision: "1838f097e537eedc958b52ee82093e43ab5b9908" };
|
module.exports = { buildDate:"2019-11-26T22:50:08+01:00", buildRevision: "5193f073e9e55f5440fe2e71fbd2cdfcdb2d2c6b" };
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
|
|||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
if (['isarchived', 'orderby', 'limit'].includes(filter.name.toLowerCase())) {
|
if (['isarchived', 'in', 'orderby', 'limit'].includes(filter.name.toLowerCase())) {
|
||||||
continue; // these are not real filters
|
continue; // these are not real filters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,25 @@ async function findLogicIssues() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await findAndFixIssues(`
|
||||||
|
SELECT notes.noteId
|
||||||
|
FROM notes
|
||||||
|
LEFT JOIN note_contents USING(noteId)
|
||||||
|
WHERE
|
||||||
|
note_contents.noteId IS NULL`,
|
||||||
|
async ({noteId}, autoFix) => {
|
||||||
|
if (autoFix) {
|
||||||
|
const note = await repository.getNote(noteId);
|
||||||
|
// empty string might be wrong choice for some note types (and protected notes) but it's a best guess
|
||||||
|
await note.setContent(note.isErased ? null : '');
|
||||||
|
|
||||||
|
logFix(`Note ${noteId} content was set to empty string since there was no corresponding row`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logError(`Note ${noteId} content row does not exist`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await findAndFixIssues(`
|
await findAndFixIssues(`
|
||||||
SELECT noteId
|
SELECT noteId
|
||||||
FROM notes
|
FROM notes
|
||||||
@@ -321,6 +340,7 @@ async function findLogicIssues() {
|
|||||||
async ({noteId}, autoFix) => {
|
async ({noteId}, autoFix) => {
|
||||||
if (autoFix) {
|
if (autoFix) {
|
||||||
const note = await repository.getNote(noteId);
|
const note = await repository.getNote(noteId);
|
||||||
|
// empty string might be wrong choice for some note types (and protected notes) but it's a best guess
|
||||||
await note.setContent('');
|
await note.setContent('');
|
||||||
|
|
||||||
logFix(`Note ${noteId} content was set to empty string since it was null even though it is not deleted`);
|
logFix(`Note ${noteId} content was set to empty string since it was null even though it is not deleted`);
|
||||||
@@ -360,6 +380,25 @@ async function findLogicIssues() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await findAndFixIssues(`
|
||||||
|
SELECT note_revisions.noteRevisionId
|
||||||
|
FROM note_revisions
|
||||||
|
LEFT JOIN note_revision_contents USING(noteRevisionId)
|
||||||
|
WHERE note_revision_contents.noteRevisionId IS NULL`,
|
||||||
|
async ({noteRevisionId}, autoFix) => {
|
||||||
|
if (autoFix) {
|
||||||
|
const noteRevision = await repository.getNoteRevision(noteRevisionId);
|
||||||
|
await noteRevision.setContent(null);
|
||||||
|
noteRevision.isErased = true;
|
||||||
|
await noteRevision.save();
|
||||||
|
|
||||||
|
logFix(`Note revision content ${noteRevisionId} was created and set to erased since it did not exist.`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logError(`Note revision content ${noteRevisionId} does not exist`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await findAndFixIssues(`
|
await findAndFixIssues(`
|
||||||
SELECT noteRevisionId
|
SELECT noteRevisionId
|
||||||
FROM note_revisions
|
FROM note_revisions
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ async function checkContentHashes(otherHashes) {
|
|||||||
if (hashes[key] !== otherHashes[key]) {
|
if (hashes[key] !== otherHashes[key]) {
|
||||||
allChecksPassed = false;
|
allChecksPassed = false;
|
||||||
|
|
||||||
|
log.info(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${otherHashes[key]}`);
|
||||||
|
|
||||||
if (key !== 'recent_notes') {
|
if (key !== 'recent_notes') {
|
||||||
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
|
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
|
||||||
ws.sendMessageToAllClients({type: 'sync-hash-check-failed'});
|
ws.sendMessageToAllClients({type: 'sync-hash-check-failed'});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const fileType = require('file-type');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const log = require("../log");
|
const log = require("../log");
|
||||||
const utils = require("../utils");
|
const utils = require("../utils");
|
||||||
|
const sql = require("../sql");
|
||||||
const noteService = require("../notes");
|
const noteService = require("../notes");
|
||||||
const imageService = require("../image");
|
const imageService = require("../image");
|
||||||
const protectedSessionService = require('../protected_session');
|
const protectedSessionService = require('../protected_session');
|
||||||
@@ -11,7 +12,7 @@ const protectedSessionService = require('../protected_session');
|
|||||||
function parseDate(text) {
|
function parseDate(text) {
|
||||||
// insert - and : to make it ISO format
|
// insert - and : to make it ISO format
|
||||||
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2)
|
text = text.substr(0, 4) + "-" + text.substr(4, 2) + "-" + text.substr(6, 2)
|
||||||
+ "T" + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + "Z";
|
+ " " + text.substr(9, 2) + ":" + text.substr(11, 2) + ":" + text.substr(13, 2) + ".000Z";
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
@@ -150,7 +151,7 @@ async function importEnex(taskContext, file, parentNote) {
|
|||||||
} else if (currentTag === 'created') {
|
} else if (currentTag === 'created') {
|
||||||
note.utcDateCreated = parseDate(text);
|
note.utcDateCreated = parseDate(text);
|
||||||
} else if (currentTag === 'updated') {
|
} else if (currentTag === 'updated') {
|
||||||
// updated is currently ignored since utcDateModified is updated automatically with each save
|
note.utcDateModified = parseDate(text);
|
||||||
} else if (currentTag === 'tag') {
|
} else if (currentTag === 'tag') {
|
||||||
note.attributes.push({
|
note.attributes.push({
|
||||||
type: 'label',
|
type: 'label',
|
||||||
@@ -187,9 +188,27 @@ async function importEnex(taskContext, file, parentNote) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function updateDates(noteId, utcDateCreated, utcDateModified) {
|
||||||
|
// it's difficult to force custom dateCreated and dateModified to Note entity so we do it post-creation with SQL
|
||||||
|
await sql.execute(`
|
||||||
|
UPDATE notes
|
||||||
|
SET dateCreated = ?,
|
||||||
|
utcDateCreated = ?,
|
||||||
|
dateModified = ?,
|
||||||
|
utcDateModified = ?
|
||||||
|
WHERE noteId = ?`,
|
||||||
|
[utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, noteId]);
|
||||||
|
|
||||||
|
await sql.execute(`
|
||||||
|
UPDATE note_contents
|
||||||
|
SET utcDateModified = ?
|
||||||
|
WHERE noteId = ?`,
|
||||||
|
[utcDateModified, noteId]);
|
||||||
|
}
|
||||||
|
|
||||||
async function saveNote() {
|
async function saveNote() {
|
||||||
// make a copy because stream continues with the next async call and note gets overwritten
|
// make a copy because stream continues with the next async call and note gets overwritten
|
||||||
let {title, content, attributes, resources, utcDateCreated} = note;
|
let {title, content, attributes, resources, utcDateCreated, utcDateModified} = note;
|
||||||
|
|
||||||
content = extractContent(content);
|
content = extractContent(content);
|
||||||
|
|
||||||
@@ -201,6 +220,10 @@ async function importEnex(taskContext, file, parentNote) {
|
|||||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||||
})).note;
|
})).note;
|
||||||
|
|
||||||
|
utcDateCreated = utcDateCreated || noteEntity.utcDateCreated;
|
||||||
|
// sometime date modified is not present in ENEX, then use date created
|
||||||
|
utcDateModified = utcDateModified || utcDateCreated;
|
||||||
|
|
||||||
taskContext.increaseProgressCount();
|
taskContext.increaseProgressCount();
|
||||||
|
|
||||||
let noteContent = await noteEntity.getContent();
|
let noteContent = await noteEntity.getContent();
|
||||||
@@ -224,6 +247,8 @@ async function importEnex(taskContext, file, parentNote) {
|
|||||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||||
})).note;
|
})).note;
|
||||||
|
|
||||||
|
await updateDates(resourceNote.noteId, utcDateCreated, utcDateModified);
|
||||||
|
|
||||||
taskContext.increaseProgressCount();
|
taskContext.increaseProgressCount();
|
||||||
|
|
||||||
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
|
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
|
||||||
@@ -235,7 +260,9 @@ async function importEnex(taskContext, file, parentNote) {
|
|||||||
try {
|
try {
|
||||||
const originalName = "image." + resource.mime.substr(6);
|
const originalName = "image." + resource.mime.substr(6);
|
||||||
|
|
||||||
const {url} = await imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
|
const {url, note: imageNote} = await imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
|
||||||
|
|
||||||
|
await updateDates(imageNote.noteId, utcDateCreated, utcDateModified);
|
||||||
|
|
||||||
const imageLink = `<img src="${url}">`;
|
const imageLink = `<img src="${url}">`;
|
||||||
|
|
||||||
@@ -257,6 +284,10 @@ async function importEnex(taskContext, file, parentNote) {
|
|||||||
|
|
||||||
// save updated content with links to files/images
|
// save updated content with links to files/images
|
||||||
await noteEntity.setContent(noteContent);
|
await noteEntity.setContent(noteContent);
|
||||||
|
|
||||||
|
await noteService.scanForLinks(noteEntity.noteId);
|
||||||
|
|
||||||
|
await updateDates(noteEntity.noteId, utcDateCreated, utcDateModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
saxStream.on("closetag", async tag => {
|
saxStream.on("closetag", async tag => {
|
||||||
|
|||||||
@@ -255,6 +255,25 @@ function isArchived(noteId) {
|
|||||||
return isNotePathArchived(notePath);
|
return isNotePathArchived(notePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} noteId
|
||||||
|
* @param {string} ancestorNoteId
|
||||||
|
* @return {boolean} - true if given noteId has ancestorNoteId in any of its paths (even archived)
|
||||||
|
*/
|
||||||
|
function isInAncestor(noteId, ancestorNoteId) {
|
||||||
|
if (ancestorNoteId === noteId) { // special case
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parentNoteId of childToParent[noteId] || []) {
|
||||||
|
if (isInAncestor(parentNoteId, ancestorNoteId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function getNoteTitleFromPath(notePath) {
|
function getNoteTitleFromPath(notePath) {
|
||||||
const pathArr = notePath.split("/");
|
const pathArr = notePath.split("/");
|
||||||
|
|
||||||
@@ -529,6 +548,7 @@ module.exports = {
|
|||||||
getNoteTitleFromPath,
|
getNoteTitleFromPath,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
isArchived,
|
isArchived,
|
||||||
|
isInAncestor,
|
||||||
load,
|
load,
|
||||||
findSimilarNotes
|
findSimilarNotes
|
||||||
};
|
};
|
||||||
@@ -37,7 +37,7 @@ function isProtectedSessionAvailable() {
|
|||||||
function decryptNotes(notes) {
|
function decryptNotes(notes) {
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (note.isProtected) {
|
if (note.isProtected) {
|
||||||
note.title = decrypt(note.title);
|
note.title = decryptString(note.title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,20 @@ async function searchForNoteIds(searchString) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInFilters = filters.filter(filter => filter.name.toLowerCase() === 'in');
|
||||||
|
|
||||||
|
for (const isInFilter of isInFilters) {
|
||||||
|
if (isInFilter.operator === '=') {
|
||||||
|
noteIds = noteIds.filter(noteId => noteCacheService.isInAncestor(noteId, isInFilter.value));
|
||||||
|
}
|
||||||
|
else if (isInFilter.operator === '!=') {
|
||||||
|
noteIds = noteIds.filter(noteId => !noteCacheService.isInAncestor(noteId, isInFilter.value));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Unrecognized isIn operator ${isInFilter.operator}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const limitFilter = filters.find(filter => filter.name.toLowerCase() === 'limit');
|
const limitFilter = filters.find(filter => filter.name.toLowerCase() === 'limit');
|
||||||
|
|
||||||
if (limitFilter) {
|
if (limitFilter) {
|
||||||
|
|||||||
@@ -138,7 +138,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const baseApiUrl = 'api/';
|
|
||||||
const glob = {
|
const glob = {
|
||||||
sourceId: ''
|
sourceId: ''
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user