Implement edit history

pull/54/merge
Candid Dauth 2016-10-31 15:11:36 +03:00
rodzic 9ceb14f485
commit 88483d142e
14 zmienionych plików z 355 dodań i 25 usunięć

Wyświetl plik

@ -0,0 +1,27 @@
<div class="modal-header">
<button type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-title">Edit History</h3>
</div>
<div class="modal-body">
<div uib-alert class="alert-danger" ng-if="error">{{error.message || error}}</div>
<p><em>Here you can inspect and revert the last 50 changes to the map.</em></p>
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Date</th>
<th>Action</th>
<th>Restore</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in history | fmOrderBy:'-time'">
<td>{{item.time}}</td>
<td>{{item.labels.description}}</td>
<td><button ng-show="item.labels.button" type="button" class="btn btn-default" ng-click="confirm(item.labels.confirm) && revert(item)">{{item.labels.button}}</button></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-default" ng-click="$close()">Close</button>
</div>

Wyświetl plik

@ -0,0 +1,109 @@
(function(fm, $, ng, undefined) {
fm.app.factory("fmMapHistory", function($uibModal, fmUtils) {
return function(map) {
var fmMapHistory = {
openHistoryDialog: function() {
var scope = map.socket.$new();
var dialog = $uibModal.open({
templateUrl: "map/history/history.html",
scope: scope,
controller: "fmMapHistoryDialogCtrl",
size: "lg",
resolve: {
map: function() { return map; }
}
});
map.socket.listenToHistory().catch(function(err) {
scope.error = err;
});
var unsubscribe = map.socket.stopListeningToHistory.bind(map.socket);
dialog.result.then(unsubscribe, unsubscribe);
scope.$watch("history", function() {
for(var i in scope.history) {
scope.history[i].labels = fmMapHistory._getLabelsForItem(scope.history[i]);
}
}, true);
},
_existsNow: function(item) {
// Look through the history of this particular object and see if the last entry indicates that the object exists now
var ret = null;
var time = 0;
for(var i in map.socket.history) {
var item2 = map.socket.history[i];
var time2 = new Date(item2.time).getTime();
if(item2.type == item.type && item2.objectId == item.objectId && time2 > time) {
ret = (item2.action != "delete");
time = time2;
}
}
return ret;
},
_getLabelsForItem: function(item) {
if(item.type == "Pad") {
return {
description: "Changed pad settings",
button: "Revert",
confirm: "Do you really want to restore the old version of the pad settings?"
};
}
var nameStrBefore = item.objectBefore && item.objectBefore.name ? "“" + item.objectBefore.name + "”" : "";
var nameStrAfter = item.objectAfter && item.objectAfter.name ? "“" + item.objectAfter.name + "”" : "";
var existsNow = fmMapHistory._existsNow(item);
var ret = {
description: {
create: "Created",
update: "Changed",
delete: "Deleted"
}[item.action] + " " + item.type + " " + item.objectId + " " + (nameStrBefore && nameStrAfter && nameStrBefore != nameStrAfter ? nameStrBefore + " (new name: " + nameStrAfter + ")" : (nameStrBefore || nameStrAfter)),
};
if(item.action == "create") {
if(existsNow) {
ret.button = "Revert (delete)";
ret.confirm = "delete";
}
} else if(existsNow) {
ret.button = "Revert";
ret.confirm = "restore the old version of";
} else {
ret.button = "Restore";
ret.confirm = "restore";
}
if(ret.confirm)
ret.confirm = "Do you really want to " + ret.confirm + " " + ((nameStrBefore || nameStrAfter) ? "the " + item.type + " " + (nameStrBefore || nameStrAfter) : "this " + item.type);
if(item.objectBefore && item.objectAfter)
ret.diff = fmUtils.getObjectDiff(item.objectBefore, item.objectAfter);
return ret;
}
};
return fmMapHistory;
};
});
fm.app.controller("fmMapHistoryDialogCtrl", function($scope, map) {
$scope.revert = function(item) {
$scope.error = null;
map.socket.revertHistoryEntry({ id: item.id }).catch(function(err) {
$scope.error = err;
});
};
});
})(FacilMap, jQuery, angular);

Wyświetl plik

@ -9,7 +9,7 @@
};
});
fm.app.factory("fmMap", function(fmUtils, fmSocket, fmMapMessages, fmMapMarkers, $templateCache, $compile, fmMapLines, fmMapTypes, fmMapViews, $rootScope, fmMapPad, fmMapToolbox, $timeout, fmMapLegend, fmMapSearch, fmMapGpx, fmMapAbout, $sce, L, fmMapImport, fmMapHash) {
fm.app.factory("fmMap", function(fmUtils, fmSocket, fmMapMessages, fmMapMarkers, $templateCache, $compile, fmMapLines, fmMapTypes, fmMapViews, $rootScope, fmMapPad, fmMapToolbox, $timeout, fmMapLegend, fmMapSearch, fmMapGpx, fmMapAbout, $sce, L, fmMapImport, fmMapHash, fmMapHistory) {
var maps = { };
var ret = { };
@ -322,6 +322,7 @@
map.importUi = fmMapImport(map);
map.searchUi = fmMapSearch(map);
map.hashUi = fmMapHash(map);
map.historyUi = fmMapHistory(map);
fmMapLegend(map);

Wyświetl plik

@ -38,7 +38,8 @@
<a href="javascript:" id="toolbox-tools-dropdown" uib-dropdown-toggle role="button">Tools <span class="caret"></span></a>
<ul uib-dropdown-menu aria-labelledby="toolbox-layers-dropdown" class="dropdown-menu-right">
<!--<li ng-if="!readonly"><a href="javascript:" ng-click="openDialog('copy-pad-dialog')">Copy pad</a></li>-->
<li><a href="javascript:" ng-click="importFile()">Import File</a></li>
<li><a href="javascript:" ng-click="importFile()">Import file</a></li>
<li><a href="javascript:" ng-click="showHistory()">Show edit history</a></li>
<li><a href="javascript:" ng-click="filter()">Filter</a></li>
<li ng-if="padId"><a href="javascript:" ng-click="exportGpx()">Export GPX</a></li>
<li ng-if="padId"><a href="javascript:" ng-click="showTable()">View as table</a></li>

Wyświetl plik

@ -55,6 +55,10 @@
});
};
scope.showHistory = function() {
map.historyUi.openHistoryDialog();
};
var ret = {
div: $($templateCache.get("map/toolbox/toolbox.html"))
};

Wyświetl plik

@ -15,6 +15,7 @@
lines: { },
views: { },
types: { },
history: { },
filterExpr: null,
filterFunc: fmFilter.compileExpression(null),
@ -75,6 +76,25 @@
};
simulateEvent("filter");
},
listenToHistory: function() {
return fmSocket.emit("listenToHistory").then(function(obj) {
fmSocket.listeningToHistory = true;
receiveMultiple(obj);
});
},
stopListeningToHistory: function() {
fmSocket.listeningToHistory = false;
return fmSocket.emit("stopListeningToHistory");
},
revertHistoryEntry: function(data) {
return fmSocket.emit("revertHistoryEntry", data).then(function(obj) {
fmSocket.history = { };
receiveMultiple(obj);
});
}
});
@ -155,6 +175,7 @@
fmSocket.markers = { };
fmSocket.lines = { };
fmSocket.views = { };
fmSocket.history = { };
},
connect: function() {
@ -165,6 +186,14 @@
if(fmSocket.bbox)
fmSocket.updateBbox(fmSocket.bbox);
if(fmSocket.listeningToHistory) // TODO: Execute after setPadId() returns
fmSocket.listenToHistory().catch(function(err) { console.error("Error listening to history", err); });
},
history: function(data) {
fmSocket.history[data.id] = data;
// TODO: Limit to 50 entries
}
};

Wyświetl plik

@ -702,6 +702,43 @@
};
/**
* Converts an object { entry: { subentry: "value" } } into { "entry.subentry": "value" }
* @param obj {Object}
* @return {Object}
*/
fmUtils.flattenObject = function(obj, _prefix) {
var ret = { };
_prefix = _prefix || "";
for(var i in obj) {
if(typeof obj[i] == "object")
$.extend(ret, fmUtils.flattenObject(obj[i], _prefix + i + "."));
else
ret[_prefix + i] = obj[i];
}
return ret;
};
fmUtils.getObjectDiff = function(obj1, obj2) {
var flat1 = fmUtils.flattenObject(obj1);
var flat2 = fmUtils.flattenObject(obj2);
var ret = [ ];
for(var i in flat1) {
if(flat1[i] != flat2[i] && !(!flat1[i] && !flat2[i]))
ret.push({ index: i, before: flat1[i], after: flat2[i] });
}
for(var i in flat2) {
if(!(i in flat1) && !(!flat1[i] && !flat2[i]))
ret.push({ index: i, before: undefined, after: flat2[i] });
}
return ret;
};
return fmUtils;
});
@ -777,4 +814,10 @@
};
});
fm.app.filter('fmOrderBy', function($filter) {
return function(value, key) {
return $filter('orderBy')(Object.keys(value).map(function(i) { return value[i]; }), key);
};
});
})(FacilMap, jQuery, angular);

Wyświetl plik

@ -46,5 +46,6 @@ require("./marker")(Database);
require("./line")(Database);
require("./view")(Database);
require("./type")(Database);
require("./history")(Database);
module.exports = Database;

Wyświetl plik

@ -171,6 +171,7 @@ module.exports = function(Database) {
_createPadObject(type, padId, data) {
var includeData = [ "Marker", "Line" ].includes(type);
var makeHistory = [ "Marker", "Line", "View", "Type" ].includes(type);
return utils.promiseAuto({
create: () => {
@ -186,16 +187,25 @@ module.exports = function(Database) {
if(data.data != null)
return this._setObjectData(type, create.id, data.data);
}
},
history: (create, data) => {
if(makeHistory)
return this.addHistoryEntry(padId, { type: type, action: "create", objectId: create.id, objectAfter: create });
}
}).then(res => res.create);
},
_updatePadObject(type, padId, objId, data) {
_updatePadObject(type, padId, objId, data, _noHistory) {
var includeData = [ "Marker", "Line" ].includes(type);
var makeHistory = !_noHistory && [ "Marker", "Line", "View", "Type" ].includes(type);
return utils.promiseAuto({
oldData: () => {
if(makeHistory)
return this._getPadObject(type, padId, objId);
},
update: () => {
update: (oldData) => {
return this._conn.model(type).update(data, { where: { id: objId, padId: padId } }).then((res) => {
if(res[0] == 0)
throw new Error(type + " " + objId + " of pad " + padId + "could not be found.");
@ -213,12 +223,18 @@ module.exports = function(Database) {
return newData.setDataValue("data", newData.data); // For JSON.stringify()
});
}
},
history: (oldData, newData, updateData) => {
if(makeHistory)
return this.addHistoryEntry(padId, { type: type, action: "update", objectId: objId, objectBefore: oldData, objectAfter: newData });
}
}).then(res => res.newData);
},
_deletePadObject(type, padId, objId) {
var includeData = [ "Marker", "Line" ].includes(type);
var makeHistory = [ "Marker", "Line", "View", "Type" ].includes(type);
return utils.promiseAuto({
oldData: () => {
@ -232,6 +248,11 @@ module.exports = function(Database) {
destroy: (oldData, destroyData) => {
return oldData.destroy();
},
history: (destroy, oldData) => {
if(makeHistory)
return this.addHistoryEntry(padId, { type: type, action: "delete", objectId: objId, objectBefore: oldData });
}
}).then(res => res.oldData);
},

Wyświetl plik

@ -6,18 +6,33 @@ module.exports = function(Database) {
Database.prototype._init.push(function() {
this._conn.define("History", {
time: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW },
type: { type: Sequelize.ENUM("marker", "line", "view", "type", "pad"), allowNull: false },
type: { type: Sequelize.ENUM("Marker", "Line", "View", "Type", "Pad"), allowNull: false },
action: { type: Sequelize.ENUM("create", "update", "delete"), allowNull: false },
objectId: { type: Sequelize.INTEGER(), allowNull: true }, // Is null when type is pad
objectBefore: {
type: Sequelize.TEXT,
allowNull: true,
get: function() {
return JSON.parse(this.getDataValue("objectBefore"));
var obj = this.getDataValue("objectBefore");
return obj == null ? null : JSON.parse(obj);
},
set: function(v) {
this.setDataValue("objectBefore", JSON.stringify(v));
this.setDataValue("objectBefore", v == null ? null : JSON.stringify(v));
}
},
objectAfter: {
type: Sequelize.TEXT,
allowNull: true,
get: function() {
var obj = this.getDataValue("objectAfter");
return obj == null ? null : JSON.parse(obj);
},
set: function(v) {
this.setDataValue("objectAfter", v == null ? null : JSON.stringify(v));
}
}
}, {
freezeTableName: true // Do not call it Histories
});
});
@ -31,14 +46,76 @@ module.exports = function(Database) {
utils.extend(Database.prototype, {
HISTORY_ENTRIES: 50,
addHistoryEntry(padId, data) {
return this._createPadObject("History", padId, data).then((historyEntry) => {
this.emit("addHistoryEntry", historyEntry);
return utils.promiseAuto({
oldEntryIds: () => {
return this._conn.model("History").findAll({
where: { padId: padId },
order: "time DESC",
offset: this.HISTORY_ENTRIES-1,
attributes: [ "id" ]
}).then(ids => ids.map(it => it.id));
},
destroyOld: (oldEntryIds) => {
if(oldEntryIds && oldEntryIds.length > 0) {
return this._conn.model("History").destroy({ where: { padId: padId, id: oldEntryIds } });
}
},
addEntry: (oldEntryIds) => {
var dataClone = JSON.parse(JSON.stringify(data));
if(data.type != "Pad") {
if(dataClone.objectBefore) {
delete dataClone.objectBefore.id;
delete dataClone.objectBefore.padId;
}
if(dataClone.objectAfter) {
delete dataClone.objectAfter.id;
delete dataClone.objectAfter.padId;
}
}
return this._createPadObject("History", padId, dataClone);
}
}).then(res => {
this.emit("addHistoryEntry", padId, res.addEntry);
return res.addEntry;
});
},
getHistory(padId) {
return this._getPadObjects("History", padId);
return this._getPadObjects("History", padId, { order: "time DESC" });
},
revertHistoryEntry(padId, id) {
return this._getPadObject("History", padId, id).then((entry) => {
if(entry.type == "Pad")
return this.updatePadData(padId, entry.objectBefore);
return utils.promiseAuto({
existsNow: () => {
return this._padObjectExists(entry.type, padId, entry.objectId);
},
restore: (existsNow) => {
var objectBefore = JSON.parse(JSON.stringify(entry.objectBefore));
if(entry.action == "create")
return existsNow && this["delete" + entry.type].call(this, padId, entry.objectId, objectBefore);
else if(existsNow)
return this["update" + entry.type].call(this, padId, entry.objectId, objectBefore);
else {
return this["create" + entry.type].call(this, padId, objectBefore).then((newObj) => {
return this._conn.model("History").update({ objectId: newObj.id }, { where: { padId: padId, type: entry.type, objectId: entry.objectId } }).then(() => {
this.emit("historyChange", padId);
});
});
}
}
}).then(res => null);
});
}
});
};

Wyświetl plik

@ -173,7 +173,7 @@ module.exports = function(Database) {
var dataCopy = utils.extend({ }, data);
delete dataCopy.trackPoints; // They came if mode is track
return this._updatePadObject("Line", padId, lineId, dataCopy);
return this._updatePadObject("Line", padId, lineId, dataCopy, doNotUpdateStyles);
},
updateStyle: (newLine) => {

Wyświetl plik

@ -62,7 +62,7 @@ module.exports = function(Database) {
updateMarker(padId, markerId, data, doNotUpdateStyles) {
return utils.promiseAuto({
update: this._updatePadObject("Marker", padId, markerId, data),
update: this._updatePadObject("Marker", padId, markerId, data, doNotUpdateStyles),
updateStyles: (update) => {
if(!doNotUpdateStyles)
return this._updateObjectStyles(update, false);

Wyświetl plik

@ -110,13 +110,14 @@ module.exports = function(Database) {
newData: (update) => this.getPadData(data.id || padId),
/*history: (oldData, update) => {
history: (oldData, newData) => {
return this.addHistoryEntry(data.id || padId, {
type: "pad",
type: "Pad",
action: "update",
objectBefore: oldData
objectBefore: oldData,
objectAfter: newData
});
}*/
}
}).then((res) => {
this.emit("padData", padId, res.newData);

Wyświetl plik

@ -427,7 +427,7 @@ utils.extend(SocketConnection.prototype, {
});
},
/*listenToHistory: function() {
listenToHistory: function() {
return Promise.resolve().then(() => {
if(this.padId == null)
throw "No pad ID set.";
@ -435,7 +435,8 @@ utils.extend(SocketConnection.prototype, {
if(this.historyListener)
throw "Already listening to history.";
this.historyListener = this.registerDatabaseHandler("addHistoryEntry", function(data) {
this.historyListener = this.registerDatabaseHandler("addHistoryEntry", (padId, data) => {
if(padId == this.padId)
this.socket.emit("history", data);
});
@ -446,14 +447,29 @@ utils.extend(SocketConnection.prototype, {
},
stopListeningToHistory: function() {
return Promise.resolve().then(() => {
if(!this.historyListener)
throw "Not listening to history.";
this.historyListener(); // Unregister db listener
this.historyListener = null;
},
revertHistoryEntry: function(data) {
var listening = !!this.historyListener;
return Promise.resolve().then(() => {
if(!utils.stripObject(data, { id: "number" }))
throw "Invalid parameters.";
if(listening)
this.socketHandlers.stopListeningToHistory.call(this);
return this.database.revertHistoryEntry(this.padId, data.id);
}).then(() => {
if(listening)
return this.socketHandlers.listenToHistory.call(this);
});
}*/
}
/*copyPad : function(data, callback) {
if(!utils.stripObject(data, { toId: "string" }))