Compare commits

..

17 Commits

Author SHA1 Message Date
zadam
5f258fbbbf release 0.37.5 2019-11-25 22:46:15 +01:00
zadam
bf9ad976b9 fixing non-root path issues, #404 2019-11-25 21:44:46 +01:00
zadam
434d8ef48c added extra autofixers for completely missing note_contents or note_revision_contents row 2019-11-23 19:56:52 +01:00
zadam
c8ba07a4ae fix migration script in case of not fully consistent database (missing note_contents for note). closes #717 2019-11-23 11:13:57 +01:00
zadam
38e7649ac3 release 0.37.4 2019-11-22 22:38:03 +01:00
zadam
7a2c7edd7e allow multiple instances of @in operator, closes #716 2019-11-22 21:17:46 +01:00
zadam
cfb850acb2 download fixes for the sub-domain web deployment 2019-11-22 20:35:17 +01:00
zadam
a16aaf7a81 fix setup on non-root paths #404 2019-11-22 20:24:49 +01:00
zadam
522f71cb91 fix tree clipboard 2019-11-20 19:24:23 +01:00
zadam
d357943ebb release 0.37.3 2019-11-19 23:05:54 +01:00
zadam
07043fb177 switch search in subtree to ctrl+shift+s to stay consistent with ctrl+s 2019-11-19 23:04:43 +01:00
zadam
1f8d382b1f added "search in subtree" context menu, #534 2019-11-19 21:11:20 +01:00
zadam
61e8cbbcba add log for content hash failures 2019-11-19 19:07:14 +01:00
zadam
86c5dd6494 fix recent changes, closes #713 2019-11-19 19:02:16 +01:00
zadam
c5acb7fc9b release 0.37.2 2019-11-18 23:04:09 +01:00
zadam
834e1f7253 fix activating tab when app was closed last time with "new tab" 2019-11-17 10:24:06 +01:00
zadam
1a87190f43 added IN operator to search, closes #534 2019-11-17 09:07:35 +01:00
27 changed files with 181 additions and 59 deletions

View File

@@ -20,7 +20,7 @@ SELECT noteId, title, -1, isProtected, type, mime, hash, isDeleted, isErased, da
DROP TABLE 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_title` ON `notes` (`title`);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -498,7 +498,7 @@
t._started = false;
onRenderStop();
} else {
setImmediate(step);
requestIdleCallback(step, { timeout: 10 });
}
}

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.36.5",
"version": "0.37.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.37.1-beta",
"version": "0.37.5",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {

View File

@@ -74,7 +74,7 @@ $form.on('submit', () => {
function exportBranch(branchId, type, format, version) {
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);
}

View File

@@ -102,7 +102,9 @@ async function setContentPane() {
const $downloadButton = $('<button class="btn btn-sm btn-primary" type="button">Download</button>');
$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);

View File

@@ -4,83 +4,97 @@ import cloningService from "./cloning.js";
import toastService from "./toast.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;
async function pasteAfter(node) {
async function pasteAfter(afterNode) {
if (isClipboardEmpty()) {
return;
}
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;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloningService.cloneNoteAfter(noteId, node.data.branchId);
for (const nodeKey of clipboardNodeKeys) {
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
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
toastService.throwError("Unrecognized clipboard mode=" + clipboardMode);
}
}
async function pasteInto(node) {
async function pasteInto(parentNode) {
if (isClipboardEmpty()) {
return;
}
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;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloningService.cloneNoteTo(noteId, node.data.noteId);
for (const nodeKey of clipboardNodeKeys) {
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
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
toastService.throwError("Unrecognized clipboard mode=" + mode);
}
}
function copy(nodes) {
clipboardIds = nodes.map(node => node.data.noteId);
clipboardNodeKeys = nodes.map(node => node.key);
clipboardMode = 'copy';
toastService.showMessage("Note(s) have been copied into clipboard.");
}
function cut(nodes) {
clipboardIds = nodes
clipboardNodeKeys = nodes
.filter(node => node.data.noteId !== hoistedNoteService.getHoistedNoteNoPromise())
.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';
toastService.showMessage("Note(s) have been cut into clipboard.");
}
}
function isEmpty() {
return clipboardIds.length === 0;
function isClipboardEmpty() {
clipboardNodeKeys = clipboardNodeKeys.filter(key => !!treeUtils.getNodeByKey(key));
return clipboardNodeKeys.length === 0;
}
export default {
@@ -88,5 +102,5 @@ export default {
pasteInto,
cut,
copy,
isEmpty
isClipboardEmpty
}

View File

@@ -185,8 +185,7 @@ class NoteDetailBook {
}
else if (type === 'file') {
function getFileUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return utils.getHost() + "/api/notes/" + note.noteId + "/download";
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
}
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');

View File

@@ -87,8 +87,7 @@ class NoteDetailFile {
}
getFileUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return utils.getHost() + "/api/notes/" + this.ctx.note.noteId + "/download";
return utils.getUrlForDownload("api/notes/" + this.ctx.note.noteId + "/download");
}
show() {}

View File

@@ -98,8 +98,7 @@ class NoteDetailImage {
}
getFileUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return utils.getHost() + `/api/notes/${this.ctx.note.noteId}/download`;
return utils.getUrlForDownload(`api/notes/${this.ctx.note.noteId}/download`);
}
show() {}

View File

@@ -142,6 +142,12 @@ async function refreshSearch() {
toastService.showMessage("Saved search note refreshed.");
}
function searchInSubtree(noteId) {
showSearch();
$searchInput.val(`@in=${noteId} @text*=*`);
}
function init() {
const hashValue = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
@@ -178,5 +184,6 @@ export default {
refreshSearch,
doSearch,
init,
searchInSubtree,
getHelpText: () => helpText
};

View File

@@ -148,7 +148,7 @@ class TreeCache {
else {
return this.notes[noteId];
}
}).filter(note => note !== null);
}).filter(note => !!note);
}
/** @return {Promise<boolean>} */

View File

@@ -9,6 +9,7 @@ import hoistedNoteService from './hoisted_note.js';
import noteDetailService from './note_detail.js';
import clipboard from './clipboard.js';
import protectedSessionHolder from "./protected_session_holder.js";
import searchNotesService from "./search_notes.js";
class TreeContextMenu {
constructor(node) {
@@ -55,6 +56,8 @@ class TreeContextMenu {
{ title: "Delete <kbd>Delete</kbd>", cmd: "delete", uiIcon: "trash",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ 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 || !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",
@@ -72,9 +75,9 @@ class TreeContextMenu {
{ title: "Move to ... <kbd>Ctrl+Shift+X</kbd>", cmd: "moveTo", uiIcon: "empty",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ 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",
enabled: !clipboard.isEmpty() && isNotRoot && parentNotSearch && noSelectedNotes },
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: "Duplicate note here", cmd: "duplicateNote", uiIcon: "empty",
enabled: noSelectedNotes && parentNotSearch && (!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) },
{ title: "----" },
@@ -177,6 +180,9 @@ class TreeContextMenu {
treeService.duplicateNote(this.node.data.noteId, branch.parentNoteId);
}
else if (cmd === "searchInSubtree") {
searchNotesService.searchInSubtree(this.node.data.noteId);
}
else {
ws.logError("Unknown command: " + cmd);
}

View File

@@ -4,6 +4,7 @@ import treeService from "./tree.js";
import hoistedNoteService from "./hoisted_note.js";
import clipboard from "./clipboard.js";
import treeCache from "./tree_cache.js";
import searchNoteService from "./search_notes.js";
const keyBindings = {
"del": node => {
@@ -167,6 +168,11 @@ const keyBindings = {
"down": node => {
node.navigate($.ui.keyCode.DOWN, true).then(treeService.clearSelectedNodes);
return false;
},
"ctrl+shift+s": node => {
searchNoteService.searchInSubtree(node.data.noteId);
return false;
}
};

View File

@@ -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 {
reloadApp,
parseDate,
@@ -230,7 +244,6 @@ export default {
escapeHtml,
stopWatch,
formatLabel,
getHost,
download,
toObject,
randomString,
@@ -245,5 +258,6 @@ export default {
getMimeTypeClass,
closeActiveDialog,
isHtmlEmpty,
clearBrowserCache
clearBrowserCache,
getUrlForDownload
};

View File

@@ -127,11 +127,13 @@ async function consumeSyncData() {
}
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
const ws = new WebSocket(protocol + "://" + location.host);
ws.onopen = () => console.debug(utils.now(), "Connected to server with WebSocket");
const ws = new WebSocket(webSocketUri);
ws.onopen = () => console.debug(utils.now(), `Connected to server ${webSocketUri} with WebSocket`);
ws.onmessage = handleMessage;
// we're not handling ws.onclose here because reconnection is done in sendPing()

View File

@@ -76,12 +76,12 @@ function SetupModel() {
}
// not using server.js because it loads too many dependencies
$.post('/api/setup/new-document', {
$.post('api/setup/new-document', {
username: username,
password: password1,
theme: theme
}).then(() => {
window.location.replace("/");
window.location.replace("./");
});
}
else if (this.setupType() === 'sync-from-server') {
@@ -128,10 +128,10 @@ function SetupModel() {
}
async function checkOutstandingSyncs() {
const { stats, initialized } = await $.get('/api/sync/stats');
const { stats, initialized } = await $.get('api/sync/stats');
if (initialized) {
window.location.replace("/");
window.location.replace("./");
}
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;

View File

@@ -17,8 +17,8 @@ async function getRecentChanges() {
FROM
note_revisions
JOIN notes USING(noteId)
ORDER BY
utcDateCreated DESC
ORDER BY
note_revisions.utcDateCreated DESC
LIMIT 1000
)
UNION ALL SELECT * FROM (

View File

@@ -1 +1 @@
module.exports = { buildDate:"2019-11-16T19:09:52+01:00", buildRevision: "1838f097e537eedc958b52ee82093e43ab5b9908" };
module.exports = { buildDate:"2019-11-25T22:46:15+01:00", buildRevision: "bf9ad976b9bf340db3d47db72ddf0857a04de178" };

View File

@@ -67,7 +67,7 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
const params = [];
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
}

View File

@@ -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(`
SELECT noteId
FROM notes
@@ -321,6 +340,7 @@ async function findLogicIssues() {
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('');
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(`
SELECT noteRevisionId
FROM note_revisions

View File

@@ -56,6 +56,8 @@ async function checkContentHashes(otherHashes) {
if (hashes[key] !== otherHashes[key]) {
allChecksPassed = false;
log.info(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${otherHashes[key]}`);
if (key !== 'recent_notes') {
// 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'});

View File

@@ -255,6 +255,25 @@ function isArchived(noteId) {
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) {
const pathArr = notePath.split("/");
@@ -529,6 +548,7 @@ module.exports = {
getNoteTitleFromPath,
isAvailable,
isArchived,
isInAncestor,
load,
findSimilarNotes
};

View File

@@ -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');
if (limitFilter) {

View File

@@ -138,7 +138,6 @@
</div>
<script type="text/javascript">
const baseApiUrl = 'api/';
const glob = {
sourceId: ''
};