mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01: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 globalCurrentNoteLoadTime;
|
||||
|
||||
function createNewTopLevelNote() {
|
||||
let rootNode = globalTree.fancytree("getRootNode");
|
||||
@ -193,6 +194,7 @@ function setNoteBackgroundIfEncrypted(note) {
|
||||
function loadNoteToEditor(noteId) {
|
||||
$.get(baseApiUrl + 'notes/' + noteId).then(note => {
|
||||
globalCurrentNote = note;
|
||||
globalCurrentNoteLoadTime = Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
if (newNoteCreated) {
|
||||
newNoteCreated = false;
|
||||
|
@ -1,14 +1,27 @@
|
||||
function checkStatus() {
|
||||
$.ajax({
|
||||
url: baseApiUrl + 'status/' + globalFullLoadTime,
|
||||
type: 'GET',
|
||||
url: baseApiUrl + 'status',
|
||||
type: 'POST',
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({
|
||||
treeLoadTime: globalTreeLoadTime,
|
||||
currentNoteId: globalCurrentNote ? globalCurrentNote.detail.note_id : null,
|
||||
currentNoteDateModified: globalCurrentNoteLoadTime
|
||||
}),
|
||||
success: resp => {
|
||||
if (resp.changed) {
|
||||
window.location.reload(true);
|
||||
if (resp.changedTree) {
|
||||
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: {
|
||||
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 globalEncryptionSessionTimeout;
|
||||
let globalEncryptedDataKey;
|
||||
let globalFullLoadTime;
|
||||
let globalTreeLoadTime;
|
||||
|
||||
$(() => {
|
||||
$.get(baseApiUrl + 'tree').then(resp => {
|
||||
function initFancyTree(notes, startNoteId) {
|
||||
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;
|
||||
let startNoteId = resp.start_note_id;
|
||||
globalEncryptionSalt = resp.password_derived_key_salt;
|
||||
globalEncryptionSessionTimeout = resp.encryption_session_timeout;
|
||||
globalEncryptedDataKey = resp.encrypted_data_key;
|
||||
globalFullLoadTime = resp.full_load_time;
|
||||
globalTreeLoadTime = resp.tree_load_time;
|
||||
|
||||
// add browser ID header to all AJAX requests
|
||||
$.ajaxSetup({
|
||||
@ -108,94 +199,16 @@ $(() => {
|
||||
|
||||
prepareNoteTree(notes);
|
||||
|
||||
globalTree.fancytree({
|
||||
autoScroll: true,
|
||||
extensions: ["hotkeys", "filter", "dnd"],
|
||||
source: notes,
|
||||
scrollParent: $("#tree"),
|
||||
activate: (event, data) => {
|
||||
const node = data.node.data;
|
||||
return {
|
||||
notes: notes,
|
||||
startNoteId: startNoteId
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
$(() => {
|
||||
loadTree().then(resp => {
|
||||
initFancyTree(resp.notes, resp.startNoteId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -4,20 +4,34 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const sql = require('../../services/sql');
|
||||
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) => {
|
||||
const fullLoadTime = req.params.full_load_time;
|
||||
router.post('', auth.checkApiAuth, async (req, res, next) => {
|
||||
const treeLoadTime = req.body.treeLoadTime;
|
||||
const currentNoteId = req.body.currentNoteId;
|
||||
const currentNoteDateModified = req.body.currentNoteDateModified;
|
||||
|
||||
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 != ?) " +
|
||||
"AND date_modified >= ?", [browserId, fullLoadTime]);
|
||||
const noteTreeChangesCount = await sql.getSingleValue("SELECT COUNT(*) FROM audit_log WHERE (browser_id IS NULL OR browser_id != ?) " +
|
||||
"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 changesToPushCount = await sql.getSingleValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
|
||||
const currentNoteChangesCount = await sql.getSingleValue("SELECT COUNT(*) FROM audit_log WHERE (browser_id IS NULL OR browser_id != ?) " +
|
||||
"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({
|
||||
'changed': rowCount > 0,
|
||||
'changedTree': noteTreeChangesCount > 0,
|
||||
'changedCurrentNote': currentNoteChangesCount > 0,
|
||||
'changesToPushCount': changesToPushCount
|
||||
});
|
||||
});
|
||||
|
@ -49,7 +49,7 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
|
||||
'encrypted_data_key': await sql.getOption('encrypted_data_key'),
|
||||
'encryption_session_timeout': await sql.getOption('encryption_session_timeout'),
|
||||
'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) {
|
||||
const now = utils.nowTimestamp();
|
||||
|
||||
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
|
||||
+ ", to=" + changeTo + ", comment=" + comment);
|
||||
|
||||
const id = utils.randomString(14);
|
||||
|
||||
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) {
|
||||
@ -191,6 +198,7 @@ module.exports = {
|
||||
getOption,
|
||||
setOption,
|
||||
addAudit,
|
||||
addSyncAudit,
|
||||
deleteRecentAudits,
|
||||
remove,
|
||||
doInTransaction,
|
||||
|
@ -7,8 +7,10 @@ const migration = require('./migration');
|
||||
const utils = require('./utils');
|
||||
const config = require('./config');
|
||||
const SOURCE_ID = require('./source_id');
|
||||
const audit_category = require('./audit_category');
|
||||
|
||||
const SYNC_SERVER = config['Sync']['syncServerHost'];
|
||||
const isSyncSetup = !!SYNC_SERVER;
|
||||
|
||||
|
||||
let syncInProgress = false;
|
||||
@ -121,6 +123,8 @@ async function pushSync(cookieJar, 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);
|
||||
|
||||
lastSyncedPush = sync.id;
|
||||
@ -232,6 +236,10 @@ async function updateNote(entity, links, sourceId, syncLog) {
|
||||
}
|
||||
|
||||
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);
|
||||
@ -249,6 +257,8 @@ async function updateNoteTree(entity, sourceId, syncLog) {
|
||||
await sql.replace('notes_tree', entity);
|
||||
|
||||
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);
|
||||
@ -270,7 +280,7 @@ async function updateNoteHistory(entity, sourceId, syncLog) {
|
||||
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 {
|
||||
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,
|
||||
updateNote,
|
||||
updateNoteTree,
|
||||
updateNoteHistory
|
||||
updateNoteHistory,
|
||||
isSyncSetup
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user