mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
more fine grained handling of conflicts without having to reload whole page most of the time
This commit is contained in:
parent
962c078bbc
commit
1cf247f164
@ -113,6 +113,7 @@ function saveNoteToServer(note, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let globalCurrentNote;
|
let globalCurrentNote;
|
||||||
|
let globalCurrentNoteLoadTime;
|
||||||
|
|
||||||
function createNewTopLevelNote() {
|
function createNewTopLevelNote() {
|
||||||
let rootNode = globalTree.fancytree("getRootNode");
|
let rootNode = globalTree.fancytree("getRootNode");
|
||||||
@ -193,6 +194,7 @@ function setNoteBackgroundIfEncrypted(note) {
|
|||||||
function loadNoteToEditor(noteId) {
|
function loadNoteToEditor(noteId) {
|
||||||
$.get(baseApiUrl + 'notes/' + noteId).then(note => {
|
$.get(baseApiUrl + 'notes/' + noteId).then(note => {
|
||||||
globalCurrentNote = note;
|
globalCurrentNote = note;
|
||||||
|
globalCurrentNoteLoadTime = Math.floor(new Date().getTime() / 1000);
|
||||||
|
|
||||||
if (newNoteCreated) {
|
if (newNoteCreated) {
|
||||||
newNoteCreated = false;
|
newNoteCreated = false;
|
||||||
|
@ -1,14 +1,27 @@
|
|||||||
function checkStatus() {
|
function checkStatus() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: baseApiUrl + 'status/' + globalFullLoadTime,
|
url: baseApiUrl + 'status',
|
||||||
type: 'GET',
|
type: 'POST',
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify({
|
||||||
|
treeLoadTime: globalTreeLoadTime,
|
||||||
|
currentNoteId: globalCurrentNote ? globalCurrentNote.detail.note_id : null,
|
||||||
|
currentNoteDateModified: globalCurrentNoteLoadTime
|
||||||
|
}),
|
||||||
success: resp => {
|
success: resp => {
|
||||||
if (resp.changed) {
|
if (resp.changedTree) {
|
||||||
window.location.reload(true);
|
loadTree().then(resp => {
|
||||||
|
console.log("Reloading tree because of background changes");
|
||||||
|
|
||||||
|
globalTree.fancytree('getTree').reload(resp.notes);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
$("#changesToPushCount").html(resp.changesToPushCount);
|
if (resp.changedCurrentNote) {
|
||||||
|
alert("Current note has been changed in different window / computer. Please reload the application and resolve the conflict manually.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$("#changesToPushCount").html(resp.changesToPushCount);
|
||||||
},
|
},
|
||||||
statusCode: {
|
statusCode: {
|
||||||
401: () => {
|
401: () => {
|
||||||
@ -24,4 +37,4 @@ function checkStatus() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(checkStatus, 10 * 1000);
|
setInterval(checkStatus, 5 * 1000);
|
@ -86,16 +86,107 @@ function setExpandedToServer(note_id, is_expanded) {
|
|||||||
let globalEncryptionSalt;
|
let globalEncryptionSalt;
|
||||||
let globalEncryptionSessionTimeout;
|
let globalEncryptionSessionTimeout;
|
||||||
let globalEncryptedDataKey;
|
let globalEncryptedDataKey;
|
||||||
let globalFullLoadTime;
|
let globalTreeLoadTime;
|
||||||
|
|
||||||
$(() => {
|
function initFancyTree(notes, startNoteId) {
|
||||||
$.get(baseApiUrl + 'tree').then(resp => {
|
globalTree.fancytree({
|
||||||
|
autoScroll: true,
|
||||||
|
extensions: ["hotkeys", "filter", "dnd"],
|
||||||
|
source: notes,
|
||||||
|
scrollParent: $("#tree"),
|
||||||
|
activate: (event, data) => {
|
||||||
|
const node = data.node.data;
|
||||||
|
|
||||||
|
saveNoteIfChanged(() => loadNoteToEditor(node.note_id));
|
||||||
|
},
|
||||||
|
expand: (event, data) => {
|
||||||
|
setExpandedToServer(data.node.key, true);
|
||||||
|
},
|
||||||
|
collapse: (event, data) => {
|
||||||
|
setExpandedToServer(data.node.key, false);
|
||||||
|
},
|
||||||
|
init: (event, data) => {
|
||||||
|
if (startNoteId) {
|
||||||
|
data.tree.activateKey(startNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window).resize();
|
||||||
|
},
|
||||||
|
hotkeys: {
|
||||||
|
keydown: keybindings
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
autoApply: true, // Re-apply last filter if lazy data is loaded
|
||||||
|
autoExpand: true, // Expand all branches that contain matches while filtered
|
||||||
|
counter: false, // Show a badge with number of matching child nodes near parent icons
|
||||||
|
fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
|
||||||
|
hideExpandedCounter: true, // Hide counter badge if parent is expanded
|
||||||
|
hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
|
||||||
|
highlight: true, // Highlight matches by wrapping inside <mark> tags
|
||||||
|
leavesOnly: false, // Match end nodes only
|
||||||
|
nodata: true, // Display a 'no data' status node if result is empty
|
||||||
|
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
|
||||||
|
},
|
||||||
|
dnd: dragAndDropSetup,
|
||||||
|
keydown: (event, data) => {
|
||||||
|
const node = data.node;
|
||||||
|
// Eat keyboard events, when a menu is open
|
||||||
|
if ($(".contextMenu:visible").length > 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (event.which) {
|
||||||
|
// Open context menu on [Space] key (simulate right click)
|
||||||
|
case 32: // [Space]
|
||||||
|
$(node.span).trigger("mousedown", {
|
||||||
|
preventDefault: true,
|
||||||
|
button: 2
|
||||||
|
})
|
||||||
|
.trigger("mouseup", {
|
||||||
|
preventDefault: true,
|
||||||
|
pageX: node.span.offsetLeft,
|
||||||
|
pageY: node.span.offsetTop,
|
||||||
|
button: 2
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Handle Ctrl-C, -X and -V
|
||||||
|
// case 67:
|
||||||
|
// if (event.ctrlKey) { // Ctrl-C
|
||||||
|
// copyPaste("copy", node);
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// break;
|
||||||
|
case 86:
|
||||||
|
console.log("CTRL-V");
|
||||||
|
|
||||||
|
if (event.ctrlKey) { // Ctrl-V
|
||||||
|
pasteAfter(node);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 88:
|
||||||
|
console.log("CTRL-X");
|
||||||
|
|
||||||
|
if (event.ctrlKey) { // Ctrl-X
|
||||||
|
cut(node);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
globalTree.contextmenu(contextMenuSetup);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTree() {
|
||||||
|
return $.get(baseApiUrl + 'tree').then(resp => {
|
||||||
const notes = resp.notes;
|
const notes = resp.notes;
|
||||||
let startNoteId = resp.start_note_id;
|
let startNoteId = resp.start_note_id;
|
||||||
globalEncryptionSalt = resp.password_derived_key_salt;
|
globalEncryptionSalt = resp.password_derived_key_salt;
|
||||||
globalEncryptionSessionTimeout = resp.encryption_session_timeout;
|
globalEncryptionSessionTimeout = resp.encryption_session_timeout;
|
||||||
globalEncryptedDataKey = resp.encrypted_data_key;
|
globalEncryptedDataKey = resp.encrypted_data_key;
|
||||||
globalFullLoadTime = resp.full_load_time;
|
globalTreeLoadTime = resp.tree_load_time;
|
||||||
|
|
||||||
// add browser ID header to all AJAX requests
|
// add browser ID header to all AJAX requests
|
||||||
$.ajaxSetup({
|
$.ajaxSetup({
|
||||||
@ -108,94 +199,16 @@ $(() => {
|
|||||||
|
|
||||||
prepareNoteTree(notes);
|
prepareNoteTree(notes);
|
||||||
|
|
||||||
globalTree.fancytree({
|
return {
|
||||||
autoScroll: true,
|
notes: notes,
|
||||||
extensions: ["hotkeys", "filter", "dnd"],
|
startNoteId: startNoteId
|
||||||
source: notes,
|
};
|
||||||
scrollParent: $("#tree"),
|
});
|
||||||
activate: (event, data) => {
|
}
|
||||||
const node = data.node.data;
|
|
||||||
|
|
||||||
saveNoteIfChanged(() => loadNoteToEditor(node.note_id));
|
$(() => {
|
||||||
},
|
loadTree().then(resp => {
|
||||||
expand: (event, data) => {
|
initFancyTree(resp.notes, resp.startNoteId);
|
||||||
setExpandedToServer(data.node.key, true);
|
|
||||||
},
|
|
||||||
collapse: (event, data) => {
|
|
||||||
setExpandedToServer(data.node.key, false);
|
|
||||||
},
|
|
||||||
init: (event, data) => {
|
|
||||||
if (startNoteId) {
|
|
||||||
data.tree.activateKey(startNoteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(window).resize();
|
|
||||||
},
|
|
||||||
hotkeys: {
|
|
||||||
keydown: keybindings
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
autoApply: true, // Re-apply last filter if lazy data is loaded
|
|
||||||
autoExpand: true, // Expand all branches that contain matches while filtered
|
|
||||||
counter: false, // Show a badge with number of matching child nodes near parent icons
|
|
||||||
fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
|
|
||||||
hideExpandedCounter: true, // Hide counter badge if parent is expanded
|
|
||||||
hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
|
|
||||||
highlight: true, // Highlight matches by wrapping inside <mark> tags
|
|
||||||
leavesOnly: false, // Match end nodes only
|
|
||||||
nodata: true, // Display a 'no data' status node if result is empty
|
|
||||||
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
|
|
||||||
},
|
|
||||||
dnd: dragAndDropSetup,
|
|
||||||
keydown: (event, data) => {
|
|
||||||
const node = data.node;
|
|
||||||
// Eat keyboard events, when a menu is open
|
|
||||||
if ($(".contextMenu:visible").length > 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
switch (event.which) {
|
|
||||||
// Open context menu on [Space] key (simulate right click)
|
|
||||||
case 32: // [Space]
|
|
||||||
$(node.span).trigger("mousedown", {
|
|
||||||
preventDefault: true,
|
|
||||||
button: 2
|
|
||||||
})
|
|
||||||
.trigger("mouseup", {
|
|
||||||
preventDefault: true,
|
|
||||||
pageX: node.span.offsetLeft,
|
|
||||||
pageY: node.span.offsetTop,
|
|
||||||
button: 2
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Handle Ctrl-C, -X and -V
|
|
||||||
// case 67:
|
|
||||||
// if (event.ctrlKey) { // Ctrl-C
|
|
||||||
// copyPaste("copy", node);
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// break;
|
|
||||||
case 86:
|
|
||||||
console.log("CTRL-V");
|
|
||||||
|
|
||||||
if (event.ctrlKey) { // Ctrl-V
|
|
||||||
pasteAfter(node);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 88:
|
|
||||||
console.log("CTRL-X");
|
|
||||||
|
|
||||||
if (event.ctrlKey) { // Ctrl-X
|
|
||||||
cut(node);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
globalTree.contextmenu(contextMenuSetup);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,20 +4,34 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const sql = require('../../services/sql');
|
const sql = require('../../services/sql');
|
||||||
const auth = require('../../services/auth');
|
const auth = require('../../services/auth');
|
||||||
|
const sync = require('../../services/sync');
|
||||||
|
const audit_category = require('../../services/audit_category');
|
||||||
|
|
||||||
router.get('/:full_load_time', auth.checkApiAuth, async (req, res, next) => {
|
router.post('', auth.checkApiAuth, async (req, res, next) => {
|
||||||
const fullLoadTime = req.params.full_load_time;
|
const treeLoadTime = req.body.treeLoadTime;
|
||||||
|
const currentNoteId = req.body.currentNoteId;
|
||||||
|
const currentNoteDateModified = req.body.currentNoteDateModified;
|
||||||
|
|
||||||
const browserId = req.get('x-browser-id');
|
const browserId = req.get('x-browser-id');
|
||||||
|
|
||||||
const rowCount = await sql.getSingleValue("SELECT COUNT(*) FROM audit_log WHERE (browser_id IS NULL OR browser_id != ?) " +
|
const noteTreeChangesCount = await sql.getSingleValue("SELECT COUNT(*) FROM audit_log WHERE (browser_id IS NULL OR browser_id != ?) " +
|
||||||
"AND date_modified >= ?", [browserId, fullLoadTime]);
|
"AND date_modified >= ? AND category IN (?, ?, ?)", [browserId, treeLoadTime,
|
||||||
|
audit_category.UPDATE_TITLE, audit_category.CHANGE_PARENT, audit_category.CHANGE_POSITION]);
|
||||||
|
|
||||||
const lastSyncedPush = await sql.getOption('last_synced_push');
|
const currentNoteChangesCount = await sql.getSingleValue("SELECT COUNT(*) FROM audit_log WHERE (browser_id IS NULL OR browser_id != ?) " +
|
||||||
const changesToPushCount = await sql.getSingleValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
|
"AND date_modified >= ? AND note_id = ? AND category IN (?)", [browserId, currentNoteDateModified, currentNoteId,
|
||||||
|
audit_category.UPDATE_CONTENT]);
|
||||||
|
|
||||||
|
let changesToPushCount = 0;
|
||||||
|
|
||||||
|
if (sync.isSyncSetup) {
|
||||||
|
const lastSyncedPush = await sql.getOption('last_synced_push');
|
||||||
|
changesToPushCount = await sql.getSingleValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
|
||||||
|
}
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
'changed': rowCount > 0,
|
'changedTree': noteTreeChangesCount > 0,
|
||||||
|
'changedCurrentNote': currentNoteChangesCount > 0,
|
||||||
'changesToPushCount': changesToPushCount
|
'changesToPushCount': changesToPushCount
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,7 +49,7 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
|||||||
'encrypted_data_key': await sql.getOption('encrypted_data_key'),
|
'encrypted_data_key': await sql.getOption('encrypted_data_key'),
|
||||||
'encryption_session_timeout': await sql.getOption('encryption_session_timeout'),
|
'encryption_session_timeout': await sql.getOption('encryption_session_timeout'),
|
||||||
'browser_id': utils.randomString(12),
|
'browser_id': utils.randomString(12),
|
||||||
'full_load_time': utils.nowTimestamp()
|
'tree_load_time': utils.nowTimestamp()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -102,17 +102,24 @@ async function remove(tableName, noteId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addAudit(category, req=null, noteId=null, changeFrom=null, changeTo=null, comment=null) {
|
async function addAudit(category, req=null, noteId=null, changeFrom=null, changeTo=null, comment=null) {
|
||||||
const now = utils.nowTimestamp();
|
|
||||||
|
|
||||||
const browserId = req == null ? null : req.get('x-browser-id');
|
const browserId = req == null ? null : req.get('x-browser-id');
|
||||||
|
|
||||||
|
await addAuditWithBrowserId(category, browserId, noteId, changeFrom, changeTo, comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSyncAudit(category, sourceId, noteId) {
|
||||||
|
await addAuditWithBrowserId(category, sourceId, noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addAuditWithBrowserId(category, browserId=null, noteId=null, changeFrom=null, changeTo=null, comment=null) {
|
||||||
|
const now = utils.nowTimestamp();
|
||||||
log.info("audit: " + category + ", browserId=" + browserId + ", noteId=" + noteId + ", from=" + changeFrom
|
log.info("audit: " + category + ", browserId=" + browserId + ", noteId=" + noteId + ", from=" + changeFrom
|
||||||
+ ", to=" + changeTo + ", comment=" + comment);
|
+ ", to=" + changeTo + ", comment=" + comment);
|
||||||
|
|
||||||
const id = utils.randomString(14);
|
const id = utils.randomString(14);
|
||||||
|
|
||||||
await execute("INSERT INTO audit_log (id, date_modified, category, browser_id, note_id, change_from, change_to, comment)"
|
await execute("INSERT INTO audit_log (id, date_modified, category, browser_id, note_id, change_from, change_to, comment)"
|
||||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, now, category, browserId, noteId, changeFrom, changeTo, comment]);
|
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, now, category, browserId, noteId, changeFrom, changeTo, comment]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRecentAudits(category, req, noteId) {
|
async function deleteRecentAudits(category, req, noteId) {
|
||||||
@ -191,6 +198,7 @@ module.exports = {
|
|||||||
getOption,
|
getOption,
|
||||||
setOption,
|
setOption,
|
||||||
addAudit,
|
addAudit,
|
||||||
|
addSyncAudit,
|
||||||
deleteRecentAudits,
|
deleteRecentAudits,
|
||||||
remove,
|
remove,
|
||||||
doInTransaction,
|
doInTransaction,
|
||||||
|
@ -7,8 +7,10 @@ const migration = require('./migration');
|
|||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const SOURCE_ID = require('./source_id');
|
const SOURCE_ID = require('./source_id');
|
||||||
|
const audit_category = require('./audit_category');
|
||||||
|
|
||||||
const SYNC_SERVER = config['Sync']['syncServerHost'];
|
const SYNC_SERVER = config['Sync']['syncServerHost'];
|
||||||
|
const isSyncSetup = !!SYNC_SERVER;
|
||||||
|
|
||||||
|
|
||||||
let syncInProgress = false;
|
let syncInProgress = false;
|
||||||
@ -121,6 +123,8 @@ async function pushSync(cookieJar, syncLog) {
|
|||||||
logSyncError("Unrecognized entity type " + sync.entity_name, null, syncLog);
|
logSyncError("Unrecognized entity type " + sync.entity_name, null, syncLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logSync("Pushing changes in " + sync.entity_name + " " + sync.entity_id);
|
||||||
|
|
||||||
await pushEntity(entity, sync.entity_name, cookieJar, syncLog);
|
await pushEntity(entity, sync.entity_name, cookieJar, syncLog);
|
||||||
|
|
||||||
lastSyncedPush = sync.id;
|
lastSyncedPush = sync.id;
|
||||||
@ -232,6 +236,10 @@ async function updateNote(entity, links, sourceId, syncLog) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await sql.addNoteSync(entity.note_id, sourceId);
|
await sql.addNoteSync(entity.note_id, sourceId);
|
||||||
|
|
||||||
|
// we don't distinguish between those for now
|
||||||
|
await sql.addSyncAudit(audit_category.UPDATE_CONTENT, sourceId, entity.note_id);
|
||||||
|
await sql.addSyncAudit(audit_category.UPDATE_TITLE, sourceId, entity.note_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
logSync("Update/sync note " + entity.note_id, syncLog);
|
logSync("Update/sync note " + entity.note_id, syncLog);
|
||||||
@ -249,6 +257,8 @@ async function updateNoteTree(entity, sourceId, syncLog) {
|
|||||||
await sql.replace('notes_tree', entity);
|
await sql.replace('notes_tree', entity);
|
||||||
|
|
||||||
await sql.addNoteTreeSync(entity.note_id, sourceId);
|
await sql.addNoteTreeSync(entity.note_id, sourceId);
|
||||||
|
|
||||||
|
await sql.addSyncAudit(audit_category.UPDATE_TITLE, sourceId, entity.note_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
logSync("Update/sync note tree " + entity.note_id, syncLog);
|
logSync("Update/sync note tree " + entity.note_id, syncLog);
|
||||||
@ -270,7 +280,7 @@ async function updateNoteHistory(entity, sourceId, syncLog) {
|
|||||||
await sql.addNoteHistorySync(entity.note_history_id, sourceId);
|
await sql.addNoteHistorySync(entity.note_history_id, sourceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
logSync("Update/sync note history " + entity.note_id, syncLog);
|
logSync("Update/sync note history " + entity.note_history_id, syncLog);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logSync("Sync conflict in note history for " + entity.note_id + ", from=" + entity.date_modified_from + ", to=" + entity.date_modified_to, syncLog);
|
logSync("Sync conflict in note history for " + entity.note_id + ", from=" + entity.date_modified_from + ", to=" + entity.date_modified_to, syncLog);
|
||||||
@ -293,5 +303,6 @@ module.exports = {
|
|||||||
sync,
|
sync,
|
||||||
updateNote,
|
updateNote,
|
||||||
updateNoteTree,
|
updateNoteTree,
|
||||||
updateNoteHistory
|
updateNoteHistory,
|
||||||
|
isSyncSetup
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user