more fine grained handling of conflicts without having to reload whole page most of the time

This commit is contained in:
azivner 2017-11-01 22:36:26 -04:00
parent 962c078bbc
commit 1cf247f164
7 changed files with 172 additions and 111 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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
});
});

View File

@ -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()
});
});

View File

@ -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,

View File

@ -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
};