kopia lustrzana https://github.com/FacilMap/facilmap
Implement edit history
rodzic
9ceb14f485
commit
88483d142e
|
@ -0,0 +1,27 @@
|
|||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">×</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>
|
|
@ -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);
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -55,6 +55,10 @@
|
|||
});
|
||||
};
|
||||
|
||||
scope.showHistory = function() {
|
||||
map.historyUi.openHistoryDialog();
|
||||
};
|
||||
|
||||
var ret = {
|
||||
div: $($templateCache.get("map/toolbox/toolbox.html"))
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -46,5 +46,6 @@ require("./marker")(Database);
|
|||
require("./line")(Database);
|
||||
require("./view")(Database);
|
||||
require("./type")(Database);
|
||||
require("./history")(Database);
|
||||
|
||||
module.exports = Database;
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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" }))
|
||||
|
|
Ładowanie…
Reference in New Issue