mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
13 Commits
v0.37.1-be
...
v0.37.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38e7649ac3 | ||
|
|
7a2c7edd7e | ||
|
|
cfb850acb2 | ||
|
|
a16aaf7a81 | ||
|
|
522f71cb91 | ||
|
|
d357943ebb | ||
|
|
07043fb177 | ||
|
|
1f8d382b1f | ||
|
|
61e8cbbcba | ||
|
|
86c5dd6494 | ||
|
|
c5acb7fc9b | ||
|
|
834e1f7253 | ||
|
|
1a87190f43 |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.36.5",
|
||||
"version": "0.37.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.37.1-beta",
|
||||
"version": "0.37.4",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>');
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -148,7 +148,7 @@ class TreeCache {
|
||||
else {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
}).filter(note => note !== null);
|
||||
}).filter(note => !!note);
|
||||
}
|
||||
|
||||
/** @return {Promise<boolean>} */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2019-11-16T19:09:52+01:00", buildRevision: "1838f097e537eedc958b52ee82093e43ab5b9908" };
|
||||
module.exports = { buildDate:"2019-11-22T22:38:03+01:00", buildRevision: "7a2c7edd7e9b975bf64f732629e711379baecf48" };
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -138,7 +138,6 @@
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const baseApiUrl = 'api/';
|
||||
const glob = {
|
||||
sourceId: ''
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user