Merge objects when object gets changed while editing it

pull/108/head
Candid Dauth 2018-09-27 01:18:56 +02:00
rodzic 302a52f0a9
commit 064f9039b3
10 zmienionych plików z 243 dodań i 178 usunięć

Wyświetl plik

@ -44,6 +44,6 @@
</ul>
</div>
<button type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">Cancel</button>
<button type="submit" class="btn btn-primary" ng-click="save()" ng-disabled="saving">Save</button>
<button type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">{{isModified ? 'Cancel' : 'Close'}}</button>
<button ng-show="isModified" type="submit" class="btn btn-primary" ng-click="save()" ng-disabled="saving">Save</button>
</div>

Wyświetl plik

@ -212,24 +212,15 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
setTimeout(drawElevationPlot, 0);
},
editLine: function(line) {
var scope = $rootScope.$new();
scope.client = map.client;
var dialog = $uibModal.open({
$uibModal.open({
template: require("./edit-line.html"),
scope: scope,
controller: "fmMapLineEditCtrl",
size: "lg",
resolve: {
map: function() { return map; }
map: () => (map),
line: () => (line)
}
});
var preserve = fmUtils.preserveObject(scope, "client.lines["+fmUtils.quoteJavaScript(line.id)+"]", "line", function() {
dialog.dismiss();
});
dialog.result.then(preserve.leave.bind(preserve), preserve.revert.bind(preserve));
},
addLine: function(type) {
map.client.getLineTemplate({ typeId: type.id }).then(function(line) {
@ -378,7 +369,25 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
};
});
fm.app.controller("fmMapLineEditCtrl", function($scope, map) {
fm.app.controller("fmMapLineEditCtrl", function($scope, map, line, fmUtils) {
$scope.line = ng.copy(line);
$scope.client = map.client;
$scope.$watch(() => (map.client.lines[line.id]), (newLine, oldLine) => {
if(newLine == null)
$scope.$dismiss();
else {
fmUtils.mergeObject(oldLine, newLine, $scope.line);
updateModified();
}
}, true);
$scope.$watch("line", updateModified, true);
function updateModified() {
$scope.isModified = !ng.equals($scope.line, map.client.lines[line.id]);
}
$scope.canControl = function(what) {
return map.typesUi.canControl($scope.client.types[$scope.line.typeId], what);
};

Wyświetl plik

@ -47,6 +47,6 @@
</ul>
</div>
<button type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">Cancel</button>
<button type="button" class="btn btn-primary" ng-click="save()" ng-disabled="saving">Save</button>
<button type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">{{isModified ? 'Cancel' : 'Close'}}</button>
<button ng-show="isModified" type="button" class="btn btn-primary" ng-click="save()" ng-disabled="saving">Save</button>
</div>

Wyświetl plik

@ -136,24 +136,15 @@ fm.app.factory("fmMapMarkers", function($uibModal, fmUtils, $compile, $timeout,
});
},
editMarker: function(marker) {
var scope = $rootScope.$new();
scope.client = map.client;
var dialog = $uibModal.open({
$uibModal.open({
template: require("./edit-marker.html"),
scope: scope,
controller: "fmMapMarkerEditCtrl",
size: "lg",
resolve: {
map: function() { return map; }
map: () => (map),
marker: () => (marker)
}
});
var preserve = fmUtils.preserveObject(scope, "client.markers["+fmUtils.quoteJavaScript(marker.id)+"]", "marker", function() {
dialog.dismiss();
});
dialog.result.then(preserve.leave.bind(preserve), preserve.revert.bind(preserve));
},
moveMarker: function(marker) {
if(!markersById[marker.id])
@ -228,7 +219,25 @@ fm.app.factory("fmMapMarkers", function($uibModal, fmUtils, $compile, $timeout,
};
});
fm.app.controller("fmMapMarkerEditCtrl", function($scope, map) {
fm.app.controller("fmMapMarkerEditCtrl", function($scope, map, marker, fmUtils) {
$scope.marker = ng.copy(marker);
$scope.client = map.client;
$scope.$watch(() => (map.client.markers[marker.id]), (newMarker, oldMarker) => {
if(newMarker == null)
$scope.$dismiss();
else {
fmUtils.mergeObject(oldMarker, newMarker, $scope.marker);
updateModified();
}
}, true);
$scope.$watch("marker", updateModified, true);
function updateModified() {
$scope.isModified = !ng.equals($scope.marker, map.client.markers[marker.id]);
}
$scope.canControl = function(what) {
return map.typesUi.canControl($scope.client.types[$scope.marker.typeId], what);
};
@ -245,9 +254,5 @@ fm.app.controller("fmMapMarkerEditCtrl", function($scope, map) {
});
};
$scope.$watchGroup([ "marker.colour", "marker.size", "marker.symbol", "marker.shape" ], function() {
map.markersUi._addMarker($scope.marker);
});
});

Wyświetl plik

@ -11,9 +11,9 @@
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">{{urlPrefix}}</span>
<input id="admin-link-input" ng-model="adminId" class="form-control" />
<input id="admin-link-input" ng-model="padData.adminId" class="form-control" />
<span class="input-group-btn" ng-if="!create">
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + adminId)">Copy</button>
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + padData.adminId)">Copy</button>
</span>
</div>
<span class="help-block" ng-if="adminError">{{adminError}}</span>
@ -26,9 +26,9 @@
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">{{urlPrefix}}</span>
<input id="pad-link-input" ng-model="writeId" class="form-control" />
<input id="pad-link-input" ng-model="padData.writeId" class="form-control" />
<span class="input-group-btn" ng-if="!create">
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + writeId)">Copy</button>
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + padData.writeId)">Copy</button>
</span>
</div>
<span class="help-block" ng-if="writeError">{{writeError}}</span>
@ -41,9 +41,9 @@
<div class="col-sm-9">
<div class="input-group">
<span class="input-group-addon">{{urlPrefix}}</span>
<input id="pad-rolink-input" ng-model="readId" class="form-control" />
<input id="pad-rolink-input" ng-model="padData.id" class="form-control" />
<span class="input-group-btn" ng-if="!create">
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + readId)">Copy</button>
<button type="button" class="btn btn-default" ng-click="copy(urlPrefix + padData.id)">Copy</button>
</span>
</div>
<span class="help-block" ng-if="readError">{{readError}}</span>
@ -93,6 +93,6 @@
</form>
</div>
<div class="modal-footer">
<button ng-if="!noCancel" type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">Cancel</button>
<button type="submit" class="btn btn-primary" ng-click="save()" ng-disabled="writeError || readError || saving">{{create ? 'Create' : 'Save'}}</button>
<button ng-if="!noCancel" type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">{{create || isModified ? 'Cancel' : 'Close'}}</button>
<button ng-show="create || isModified" type="submit" class="btn btn-primary" ng-click="save()" ng-disabled="writeError || readError || saving">{{create ? 'Create' : 'Save'}}</button>
</div>

Wyświetl plik

@ -1,5 +1,6 @@
import fm from '../../app';
import $ from 'jquery';
import ng from 'angular';
fm.app.factory("fmMapPad", function($uibModal, fmUtils, $rootScope) {
return function(map) {
@ -8,12 +9,8 @@ fm.app.factory("fmMapPad", function($uibModal, fmUtils, $rootScope) {
ret.editPadSettings(true, proposedAdminId, noCancel);
},
editPadSettings : function(create, proposedAdminId, noCancel) {
var scope = $rootScope.$new();
scope.client = map.client;
var dialog = $uibModal.open({
$uibModal.open({
template: require("./pad-settings.html"),
scope: scope,
controller: "fmMapPadSettingsCtrl",
size: "lg",
resolve: {
@ -25,14 +22,6 @@ fm.app.factory("fmMapPad", function($uibModal, fmUtils, $rootScope) {
keyboard: !noCancel,
backdrop: noCancel ? "static" : true
});
if(!create) {
var preserve = fmUtils.preserveObject(scope, "client.padData", "padData", function() {
dialog.dismiss();
});
dialog.result.then(preserve.leave.bind(preserve), preserve.revert.bind(preserve));
}
}
};
@ -63,26 +52,28 @@ fm.app.controller("fmMapPadSettingsCtrl", function($scope, map, create, proposed
$scope.noCancel = noCancel;
if(create) {
$scope.adminId = (proposedAdminId || fmUtils.generateRandomPadId(16));
$scope.writeId = fmUtils.generateRandomPadId(14);
$scope.readId = fmUtils.generateRandomPadId(12);
$scope.padData = {
padName: "New FacilMap",
searchEngines: false,
description: "",
clusterMarkers: false
clusterMarkers: false,
adminId: (proposedAdminId || fmUtils.generateRandomPadId(16)),
writeId: fmUtils.generateRandomPadId(14),
id: fmUtils.generateRandomPadId(12)
};
} else {
// We don't want to edit those in padData directly, as that would change the URL while we type
$scope.$watch("padData.adminId", (adminId) => {
$scope.adminId = adminId;
});
$scope.$watch("padData.writeId", (writeId) => {
$scope.writeId = writeId;
});
$scope.$watch("padData.id", (readId) => {
$scope.readId = readId;
});
$scope.padData = ng.copy(map.client.padData);
$scope.$watch(() => (map.client.padData), (newPadData, oldPadData) => {
fmUtils.mergeObject(oldPadData, newPadData, $scope.padData);
updateModified();
}, true);
$scope.$watch("padData", updateModified, true);
function updateModified() {
$scope.isModified = !ng.equals($scope.padData, map.client.padData);
}
}
function validateId(id) {
@ -92,24 +83,23 @@ fm.app.controller("fmMapPadSettingsCtrl", function($scope, map, create, proposed
return "May not contain a slash.";
}
$scope.$watch("adminId", function(adminId) {
$scope.$watch("padData.adminId", function(adminId) {
$scope.adminError = validateId(adminId);
});
$scope.$watch("writeId", function(writeId) {
$scope.$watch("padData.writeId", function(writeId) {
$scope.writeError = validateId(writeId);
});
$scope.$watch("readId", function(readId) {
$scope.$watch("padData.id", function(readId) {
$scope.readError = validateId(readId);
});
$scope.save = function() {
$scope.saving = true;
let newData = $.extend({}, $scope.padData, {id: $scope.readId, writeId: $scope.writeId, adminId: $scope.adminId});
if(create) {
map.client.createPad(newData).then(function() {
map.client.createPad($scope.padData).then(function() {
map.client.updateBbox(fmUtils.leafletToFmBbox(map.map.getBounds(), map.map.getZoom()));
$scope.$close();
@ -118,7 +108,7 @@ fm.app.controller("fmMapPadSettingsCtrl", function($scope, map, create, proposed
$scope.saving = false;
});
} else {
map.client.editPad(newData).then(function() {
map.client.editPad($scope.padData).then(function() {
$scope.$close();
}).catch(function(err) {
$scope.error = err;

Wyświetl plik

@ -72,6 +72,6 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-click="$dismiss()">Cancel</button>
<button type="submit" class="btn btn-primary" ng-click="save()">OK</button>
<button type="button" class="btn btn-default" ng-click="$dismiss()">{{isModified ? 'Cancel' : 'Close'}}</button>
<button ng-show="isModified" type="submit" class="btn btn-primary" ng-click="save()">OK</button>
</div>

Wyświetl plik

@ -149,6 +149,6 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">Cancel</button>
<button type="submit" class="btn btn-primary" ng-click="save()" ng-disabled="saving">Save</button>
<button type="button" class="btn btn-default" ng-click="$dismiss()" ng-disabled="saving">{{isModified ? 'Cancel' : 'Close'}}</button>
<button ng-show="isModified" type="submit" class="btn btn-primary" ng-click="save()" ng-disabled="saving">Save</button>
</div>

Wyświetl plik

@ -1,15 +1,63 @@
import fm from '../../app';
import ng from 'angular';
fm.app.factory("fmMapTypesUtils", function(fmUtils) {
function getIdxForInsertingField(targetFields, targetField, mergedFields) {
// Check which field comes after the field in the target field list, and return the index of that field in mergedFields
for(let i = targetFields.indexOf(targetField) + 1; i < targetFields.length; i++) {
if(!targetFields[i].oldName)
continue;
let thisIdxInMergedFields = mergedFields.findIndex(field => field.oldName == targetFields[i].oldName);
if(thisIdxInMergedFields != -1)
return thisIdxInMergedFields;
}
return mergedFields.length;
}
function mergeFields(oldFields, newFields, customFields) {
let mergedFields = newFields.map((newField) => {
let oldField = oldFields.find((field) => (field.name == newField.name));
let customField = customFields.find((field) => (field.oldName == newField.name));
if(oldField && !customField) // Field has been removed in customFields
return null;
else if(!customField)
return Object.assign({}, newField, {oldName: newField.name});
let mergedField = ng.copy(customField);
fmUtils.mergeObject(oldField, newField, mergedField);
return mergedField;
}).filter(field => field != null);
// Fields that don't have an oldName have been created, so we have to add them again
for(let customField of customFields.filter(field => !field.oldName))
mergedFields.splice(getIdxForInsertingField(customFields, customField, mergedFields), 0, customField);
return mergedFields;
}
let fmMapTypesUtils = {
mergeTypeObject(oldObject, newObject, targetObject) {
let customFields = ng.copy(targetObject.fields);
fmUtils.mergeObject(oldObject, newObject, targetObject);
targetObject.fields = mergeFields(oldObject.fields, newObject.fields, customFields);
}
};
return fmMapTypesUtils;
});
fm.app.factory("fmMapTypes", function($uibModal, fmUtils, $rootScope) {
return function(map) {
var ret = {
editTypes : function() {
let scope = $rootScope.$new();
scope.client = map.client;
$uibModal.open({
template: require("./edit-types.html"),
scope: scope,
controller: "fmMapTypesEditCtrl",
size: "lg",
resolve: {
@ -18,70 +66,27 @@ fm.app.factory("fmMapTypes", function($uibModal, fmUtils, $rootScope) {
});
},
editType : function(type) {
var scope = $rootScope.$new();
scope.client = map.client;
scope.type = type;
scope.$watch("type.fields", (fields) => {
fields.forEach((field) => {
field.oldName = field.name;
});
});
var dialog = $uibModal.open({
$uibModal.open({
template: require("./edit-type.html"),
scope: scope,
controller: "fmMapTypesEditTypeCtrl",
size: "lg",
resolve: {
map: function() { return map; }
map: () => (map),
type: () => (type)
}
});
var preserve = fmUtils.preserveObject(scope, type.id ? "client.types["+fmUtils.quoteJavaScript(type.id)+"]" : "type", "type", function() {
dialog.dismiss();
});
dialog.result.then(preserve.leave.bind(preserve), preserve.revert.bind(preserve));
},
editTypeDropdown : function(type, field) {
var scope = $rootScope.$new();
scope.type = type;
scope.field = field;
if(field.type == 'checkbox') {
if(!field.options || field.options.length != 2) {
field.options = [
{ value: '' },
{ value: field.name }
]
}
// Convert legacy format
if(field.options[0].value == "0")
field.options[0].value = "";
if(field.options[1].value == "1")
field.options[1].value = field.name;
}
for(let option of (field.options || []))
option.oldValue = option.value;
var dialog = $uibModal.open({
$uibModal.open({
template: require("./edit-type-dropdown.html"),
scope: scope,
controller: "fmMapTypesEditTypeDropdownCtrl",
size: "lg",
resolve: {
map: function() { return map; }
map: () => (map),
type: () => (type),
field: () => (field)
}
});
var preserve = fmUtils.preserveObject(scope, "field", "field", function() {
dialog.dismiss();
});
dialog.result.then(preserve.leave.bind(preserve), preserve.revert.bind(preserve));
},
canControl : function(type, what, ignoreField) {
if(type[what+"Fixed"] && ignoreField !== null)
@ -100,6 +105,7 @@ fm.app.factory("fmMapTypes", function($uibModal, fmUtils, $rootScope) {
});
fm.app.controller('fmMapTypesEditCtrl', function($scope, map) {
$scope.client = map.client;
$scope.saving = {};
$scope.create = function() {
@ -120,7 +126,37 @@ fm.app.controller('fmMapTypesEditCtrl', function($scope, map) {
};
});
fm.app.controller('fmMapTypesEditTypeCtrl', function($scope, map, fmSortableOptions) {
fm.app.controller('fmMapTypesEditTypeCtrl', function($scope, map, fmSortableOptions, type, fmUtils, fmMapTypesUtils) {
$scope.client = map.client;
$scope.type = ng.copy(type);
for(let field of $scope.type.fields) {
field.oldName = field.name;
}
if(type.id != null) {
$scope.$watch(() => (map.client.types[type.id]), (newType, oldType) => {
if(newType == null)
$scope.$dismiss();
else {
fmMapTypesUtils.mergeTypeObject(oldType, newType, $scope.type);
updateModified();
}
}, true);
$scope.$watch("type", updateModified, true);
function updateModified() {
let typeWithOldNames = ng.copy(map.client.types[type.id]);
for(let field of typeWithOldNames.fields)
field.oldName = field.name;
$scope.isModified = !ng.equals($scope.type, typeWithOldNames);
}
} else {
$scope.isModified = true;
}
$scope.sortableOptions = fmSortableOptions;
$scope.editDropdown = function(field) {
@ -159,11 +195,60 @@ fm.app.controller('fmMapTypesEditTypeCtrl', function($scope, map, fmSortableOpti
};
});
fm.app.controller('fmMapTypesEditTypeDropdownCtrl', function($scope, map, fmUtils, fmSortableOptions) {
fm.app.controller('fmMapTypesEditTypeDropdownCtrl', function($scope, map, fmUtils, fmSortableOptions, type, field) {
$scope.type = type;
$scope.field = ng.copy(field);
if(type.id != null && field.oldName) {
$scope.$watch(() => {
let extType = map.client.types[type.id];
return extType && extType.fields.find(thisField => thisField.name == field.oldName);
}, (newField, oldField) => {
if(newField == null)
$scope.$dismiss();
else {
fmUtils.mergeObject(newField, oldField, $scope.field);
updateModified();
}
}, true);
$scope.$watch("field", updateModified, true);
function updateModified() {
let fieldWithOldName = Object.assign(ng.copy(map.client.types[type.id].fields.find(thisField => thisField.name == field.oldName)), {
oldName: $scope.field.oldName,
name: $scope.field.name,
type: $scope.field.type
});
$scope.isModified = !ng.equals($scope.field, fieldWithOldName);
}
} else {
$scope.isModified = true;
}
if(field.type == 'checkbox') {
if(!field.options || field.options.length != 2) {
field.options = [
{ value: '' },
{ value: field.name }
]
}
// Convert legacy format
if(field.options[0].value == "0")
field.options[0].value = "";
if(field.options[1].value == "1")
field.options[1].value = field.name;
}
for(let option of (field.options || []))
option.oldValue = option.value;
$scope.sortableOptions = fmSortableOptions;
$scope.canControl = function(what) {
return map.typesUi.canControl($scope.type, what, $scope.field);
return map.typesUi.canControl($scope.type, what, type.fields.find((field) => (field.oldName ? field.oldName == $scope.field.oldName : field.name == $scope.field.name)));
};
$scope.addOption = function() {
@ -180,6 +265,9 @@ fm.app.controller('fmMapTypesEditTypeDropdownCtrl', function($scope, map, fmUtil
};
$scope.save = function() {
let idx = type.fields.findIndex((field) => (field.oldName ? field.oldName == $scope.field.oldName : field.name == $scope.field.name));
type.fields[idx] = $scope.field;
$scope.$close();
};
});

Wyświetl plik

@ -151,48 +151,6 @@ fm.app.factory("fmUtils", function($parse, fmIcons) {
fmUtils.formatTime = commonFormat.formatTime;
fmUtils.formatRoutingMode = commonRouting.formatRoutingMode;
fmUtils.preserveObject = function(scope, sourceExpr, targetExpr, onDelete) {
var obj,bkp;
function _update(firstTime) {
obj = $parse(sourceExpr)(scope);
if(firstTime && obj == null)
obj = $parse(targetExpr)(scope);
bkp = ng.copy(obj);
if(sourceExpr != targetExpr)
$parse(targetExpr + " = " + sourceExpr)(scope);
}
_update(true);
var unwatch = scope.$watch(sourceExpr, function(newVal) {
_update(false);
if(newVal == null) {
unwatch();
if(onDelete)
onDelete();
}
});
return {
revert : function() {
if(bkp == null)
return;
fmUtils.overwriteObject(bkp, obj);
unwatch();
},
leave : function() {
unwatch();
bkp = null;
}
}
};
fmUtils.leafletToFmBbox = function(bbox, zoom) {
var ret = {
top: bbox.getNorth(),
@ -824,6 +782,21 @@ fm.app.factory("fmUtils", function($parse, fmIcons) {
return map.project(latLng1, zoom).distanceTo(map.project(latLng2, zoom)) < 1;
};
/**
* Performs a 3-way merge. Takes the difference between oldObject and newObject and applies it to targetObject.
* @param oldObject {Object}
* @param newObject {Object}
* @param targetObject {Object}
*/
fmUtils.mergeObject = function(oldObject, newObject, targetObject) {
for(let i of new Set([...Object.keys(newObject), ...Object.keys(targetObject)])) {
if(typeof newObject[i] == "object" && newObject[i] != null && targetObject[i] != null)
fmUtils.mergeObject(oldObject && oldObject[i], newObject[i], targetObject[i]);
else if(oldObject == null || !ng.equals(oldObject[i], newObject[i]))
targetObject[i] = ng.copy(newObject[i]);
}
};
return fmUtils;
});