Merge pull request #8493 from umbraco/v8/feature/block-list-settings-validation

Block list editor - settings validation
This commit is contained in:
Warren Buckley
2020-07-29 11:11:07 +01:00
committed by GitHub
26 changed files with 820 additions and 628 deletions
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -27,7 +27,7 @@
"angular-ui-sortable": "0.19.0",
"animejs": "2.2.0",
"bootstrap-social": "5.1.1",
"chart.js": "^2.8.0",
"chart.js": "^2.9.3",
"clipboard": "2.0.4",
"diff": "3.5.0",
"flatpickr": "4.5.2",
@@ -39,7 +39,7 @@
"moment": "2.22.2",
"ng-file-upload": "12.2.13",
"nouislider": "14.4.0",
"npm": "^6.14.0",
"npm": "^6.14.7",
"signalr": "2.4.0",
"spectrum-colorpicker": "1.8.0",
"tinymce": "4.9.10",
@@ -57,7 +57,7 @@
"gulp-angular-embed-templates": "^2.3.0",
"gulp-babel": "8.0.0",
"gulp-clean-css": "4.2.0",
"gulp-cli": "^2.2.0",
"gulp-cli": "^2.3.0",
"gulp-concat": "2.6.1",
"gulp-eslint": "6.0.0",
"gulp-imagemin": "6.1.1",
@@ -71,7 +71,6 @@
"gulp-wrap": "0.15.0",
"gulp-wrap-js": "0.4.1",
"jasmine-core": "3.5.0",
"jasmine-promise-matchers": "^2.6.0",
"karma": "4.4.1",
"karma-chrome-launcher": "^3.1.0",
"karma-jasmine": "2.0.1",
@@ -1,5 +1,17 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:valServerMatch
* @restrict A
* @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data
* contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against
* a property validation key. The attribute value can be in multiple value types:
* - STRING = The property validation key to have an exact match on. If matched, then the form will have a valServerMatch validator applied.
* - OBJECT = A dictionary where the key is the match type: "contains", "prefix", "suffix" and the value is either:
* - ARRAY = A list of property validation keys to match on. If any are matched then the form will have a valServerMatch validator applied.
* - OBJECT = A dictionary where the key is the validator error name applied to the form and the value is the STRING of the property validation key to match on
**/
function valServerMatch(serverValidationManager) {
return {
require: ['form', '^^umbProperty', '?^^umbVariantContent'],
restrict: "A",
@@ -40,41 +52,63 @@ function valServerMatch(serverValidationManager) {
var unsubscribe = [];
//subscribe to the server validation changes
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
if (!isValid) {
formCtrl.$setValidity('valServerMatch', false);
}
else {
formCtrl.$setValidity('valServerMatch', true);
function bindCallback(validationKey, matchVal, matchType) {
if (Utilities.isString(matchVal)) {
matchVal = [matchVal]; // normalize to an array since the value can also natively be an array
}
// match for each string in the array
matchVal.forEach(m => {
unsubscribe.push(serverValidationManager.subscribe(
m,
currentCulture,
"",
// the callback
function (isValid, propertyErrors, allErrors) {
if (!isValid) {
formCtrl.$setValidity(validationKey, false);
}
else {
formCtrl.$setValidity(validationKey, true);
}
},
currentSegment,
matchType ? { matchType: matchType } : null // specify the match type
));
});
}
if (Utilities.isObject(scope.valServerMatch)) {
var allowedKeys = ["contains", "prefix", "suffix"];
Object.keys(scope.valServerMatch).forEach(k => {
if (allowedKeys.indexOf(k) === -1) {
Object.keys(scope.valServerMatch).forEach(matchType => {
if (allowedKeys.indexOf(matchType) === -1) {
throw "valServerMatch dictionary keys must be one of " + allowedKeys.join();
}
unsubscribe.push(serverValidationManager.subscribe(
scope.valServerMatch[k],
currentCulture,
"",
serverValidationManagerCallback,
currentSegment,
{ matchType: k } // specify the match type
));
var matchVal = scope.valServerMatch[matchType];
if (Utilities.isObject(matchVal)) {
// as an object, the key will be the validation error instead of the default "valServerMatch"
Object.keys(matchVal).forEach(valKey => {
// matchVal[valKey] can be an ARRAY or a STRING
bindCallback(valKey, matchVal[valKey], matchType);
});
}
else {
// matchVal can be an ARRAY or a STRING
bindCallback("valServerMatch", matchVal, matchType);
}
});
}
else if (Utilities.isString(scope.valServerMatch)) {
unsubscribe.push(serverValidationManager.subscribe(
scope.valServerMatch,
currentCulture,
"",
serverValidationManagerCallback,
currentSegment));
// a STRING match which will be an exact match on the string supplied as the property validation key
bindCallback("valServerMatch", scope.valServerMatch, null);
}
else {
throw "valServerMatch value must be a string or a dictionary";
@@ -1,36 +1,49 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:valSubView
* @restrict A
* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data.
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
* @ngdoc directive
* @name umbraco.directives.directive:valSubView
* @restrict A
* @description Used to show validation warnings for a editor sub view (used in conjunction with:
* umb-editor-sub-view or umb-editor-sub-views) to indicate that the section content has validation errors in its data.
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
* When applied to
**/
(function () {
'use strict';
// Since this is a directive applied as an attribute, the value of that attribtue is the 'model' object property
// of the current inherited scope that the hasError/errorClass properties will apply to.
// This directive cannot have it's own scope because it's an attribute applied to another scoped directive.
// Due to backwards compatibility we can't really change this, ideally this would have it's own scope/properties.
function valSubViewDirective() {
function controller($scope, $element) {
function controller($scope, $element, $attrs) {
var model = $scope.model; // this is the default and required for backwards compat
if ($attrs && $attrs.valSubView) {
// get the property to use
model = $scope[$attrs.valSubView];
}
//expose api
return {
valStatusChanged: function (args) {
// TODO: Verify this is correct, does $scope.model ever exist?
if ($scope.model) {
if (model) {
if (!args.form.$valid) {
var subViewContent = $element.find(".ng-invalid");
if (subViewContent.length > 0) {
$scope.model.hasError = true;
$scope.model.errorClass = args.showValidation ? 'show-validation' : null;
model.hasError = true;
model.errorClass = args.showValidation ? 'show-validation' : null;
} else {
$scope.model.hasError = false;
$scope.model.errorClass = null;
model.hasError = false;
model.errorClass = null;
}
}
else {
$scope.model.hasError = false;
$scope.model.errorClass = null;
model.hasError = false;
model.errorClass = null;
}
}
}
@@ -40,12 +53,18 @@
function link(scope, el, attr, ctrl) {
//if there are no containing form or valFormManager controllers, then we do nothing
if (!ctrl || !Utilities.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) {
if (!ctrl[1]) {
return;
}
var model = scope.model; // this is the default and required for backwards compat
if (attr && attr.valSubView) {
// get the property to use
model = scope[attr.valSubView];
}
var valFormManager = ctrl[1];
scope.model.hasError = false;
model.hasError = false;
//listen for form validation changes
valFormManager.onValidationStatusChanged(function (evt, args) {
@@ -54,14 +73,14 @@
var subViewContent = el.find(".ng-invalid");
if (subViewContent.length > 0) {
scope.model.hasError = true;
model.hasError = true;
} else {
scope.model.hasError = false;
model.hasError = false;
}
}
else {
scope.model.hasError = false;
model.hasError = false;
}
});
@@ -305,6 +305,7 @@ angular.module('umbraco.mocks').
allowedActions: ["U", "H", "A"],
contentTypeAlias: "testAlias",
contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB",
apps: [],
variants: [
{
name: "",
@@ -14,7 +14,7 @@
'use strict';
function blockEditorModelObjectFactory($interpolate, udiService, contentResource) {
function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService) {
/**
* Simple mapping from property model content entry to editing model,
@@ -162,6 +162,7 @@
}
}
}
/**
* Used to create a prop watcher for the settings in the property editor data model.
*/
@@ -200,6 +201,29 @@
}
}
function createDataEntry(elementTypeKey, dataItems) {
var data = {
contentTypeKey: elementTypeKey,
udi: udiService.create("element")
};
dataItems.push(data);
return data.udi;
}
function getDataByUdi(udi, dataItems) {
return dataItems.find(entry => entry.udi === udi) || null;
}
/**
* Set the udi and key property for the content item
* @param {any} contentData
* @param {any} udi
*/
function ensureUdiAndKey(contentData, udi) {
contentData.udi = udi;
// Change the content.key to the GUID part of the udi, else it's just random which we don't want, it must be consistent
contentData.key = udiService.getKey(udi);
}
/**
* Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property.
@@ -209,7 +233,15 @@
"Umbraco.UploadField",
"Umbraco.ImageCropper"
];
function replaceUnsupportedProperties(scaffold) {
/**
* Formats the content apps and ensures unsupported property's have the notsupported view (returns a promise)
* @param {any} scaffold
*/
function formatScaffoldData(scaffold) {
// deal with not supported props
scaffold.variants.forEach((variant) => {
variant.tabs.forEach((tab) => {
tab.properties.forEach((property) => {
@@ -219,7 +251,42 @@
});
});
});
return scaffold;
// could be empty in tests
if (!scaffold.apps) {
console.warn("No content apps found in scaffold");
return $q.resolve(scaffold);
}
// replace view of content app
var contentApp = scaffold.apps.find(entry => entry.alias === "umbContent");
if (contentApp) {
contentApp.view = "views/common/infiniteeditors/blockeditor/blockeditor.content.html";
}
// remove info app
var infoAppIndex = scaffold.apps.findIndex(entry => entry.alias === "umbInfo");
if (infoAppIndex >= 0) {
scaffold.apps.splice(infoAppIndex, 1);
}
// add the settings app
return localizationService.localize("blockEditor_tabBlockSettings").then(
function (settingsName) {
var settingsTab = {
"name": settingsName,
"alias": "settings",
"icon": "icon-settings",
"view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html",
"hasError": false
};
scaffold.apps.push(settingsTab);
return scaffold;
}
);
}
/**
@@ -320,16 +387,24 @@
// removing duplicates.
scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index);
scaffoldKeys.forEach((contentTypeKey => {
var self = this;
scaffoldKeys.forEach(contentTypeKey => {
tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => {
// this.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete.
if (this.scaffolds) {
this.scaffolds.push(replaceUnsupportedProperties(scaffold));
// self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete.
if (self.scaffolds) {
return formatScaffoldData(scaffold).then(s => {
self.scaffolds.push(s);
return s;
});
}
else {
return $q.resolve(scaffold);
}
}));
}));
});
return Promise.all(tasks);
return $q.all(tasks);
},
/**
@@ -415,12 +490,12 @@
*/
getBlockObject: function (layoutEntry) {
var udi = layoutEntry.contentUdi;
var contentUdi = layoutEntry.contentUdi;
var dataModel = this._getDataByUdi(udi);
var dataModel = getDataByUdi(contentUdi, this.value.contentData);
if (dataModel === null) {
console.error("Couldn't find content model of " + udi)
console.error("Couldn't find content model of " + contentUdi)
return null;
}
@@ -428,12 +503,12 @@
var contentScaffold;
if (blockConfiguration === null) {
console.error("The block entry of " + udi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor");
console.error("The block entry of " + contentUdi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor");
}
else {
contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
if (contentScaffold === null) {
console.error("The block entry of " + udi + " is not begin initialized cause its Element Type was not loaded.");
console.error("The block entry of " + contentUdi + " is not begin initialized cause its Element Type was not loaded.");
}
}
@@ -470,9 +545,7 @@
// make basics from scaffold
blockObject.content = Utilities.copy(contentScaffold);
blockObject.content.udi = udi;
// Change the content.key to the GUID part of the udi, else it's just random which we don't want, it should be consistent
blockObject.content.key = udiService.getKey(udi);
ensureUdiAndKey(blockObject.content, contentUdi);
mapToElementModel(blockObject.content, dataModel);
@@ -486,12 +559,12 @@
if (!layoutEntry.settingsUdi) {
// if this block does not have settings data, then create it. This could happen because settings model has been added later than this content was created.
layoutEntry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey);
layoutEntry.settingsUdi = createDataEntry(blockConfiguration.settingsElementTypeKey, this.value.settingsData);
}
var settingsUdi = layoutEntry.settingsUdi;
var settingsData = this._getSettingsByUdi(settingsUdi);
var settingsData = getDataByUdi(settingsUdi, this.value.settingsData);
if (settingsData === null) {
console.error("Couldnt find content settings data of " + settingsUdi)
return null;
@@ -501,7 +574,8 @@
// make basics from scaffold
blockObject.settings = Utilities.copy(settingsScaffold);
blockObject.settings.udi = settingsUdi;
ensureUdiAndKey(blockObject.settings, settingsUdi);
mapToElementModel(blockObject.settings, settingsData);
}
}
@@ -628,11 +702,11 @@
}
var entry = {
contentUdi: this._createDataEntry(contentTypeKey)
contentUdi: createDataEntry(contentTypeKey, this.value.contentData)
}
if (blockConfiguration.settingsElementTypeKey != null) {
entry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey)
entry.settingsUdi = createDataEntry(blockConfiguration.settingsElementTypeKey, this.value.settingsData)
}
return entry;
@@ -656,7 +730,7 @@
return null;
}
var dataModel = this._getDataByUdi(layoutEntry.udi);
var dataModel = getDataByUdi(layoutEntry.udi, this.value.contentData);
if (dataModel === null) {
return null;
}
@@ -679,22 +753,6 @@
}
},
// private
// TODO: Then this can just be a method in the outer scope
_createDataEntry: function (elementTypeKey) {
var content = {
contentTypeKey: elementTypeKey,
udi: udiService.create("element")
};
this.value.contentData.push(content);
return content.udi;
},
// private
// TODO: Then this can just be a method in the outer scope
_getDataByUdi: function (udi) {
return this.value.contentData.find(entry => entry.udi === udi) || null;
},
/**
* @ngdoc method
* @name removeDataByUdi
@@ -710,23 +768,6 @@
}
},
// private
// TODO: Then this can just be a method in the outer scope
_createSettingsEntry: function (elementTypeKey) {
var settings = {
contentTypeKey: elementTypeKey,
udi: udiService.create("element")
};
this.value.settingsData.push(settings);
return settings.udi;
},
// private
// TODO: Then this can just be a method in the outer scope
_getSettingsByUdi: function (udi) {
return this.value.settingsData.find(entry => entry.udi === udi) || null;
},
/**
* @ngdoc method
* @name removeSettingsByUdi
@@ -67,6 +67,9 @@ angular.module('umbraco.services')
// loads the language resource file from the server
initLocalizedResources: function () {
// TODO: This promise handling is super ugly, we should just be returnning the promise from $http and returning inner values.
var deferred = $q.defer();
if (resourceFileLoadStatus === "loaded") {
@@ -179,7 +182,7 @@ angular.module('umbraco.services')
*/
localize: function (value, tokens, fallbackValue) {
return service.initLocalizedResources().then(function (dic) {
return _lookup(value, tokens, dic, fallbackValue);
return _lookup(value, tokens, dic, fallbackValue);
});
},
@@ -7,6 +7,10 @@
* Used to handle server side validation and wires up the UI with the messages. There are 2 types of validation messages, one
* is for user defined properties (called Properties) and the other is for field properties which are attached to the native
* model objects (not user defined). The methods below are named according to these rules: Properties vs Fields.
*
* For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
* https://github.com/umbraco/Umbraco-CMS/pull/8339
*
*/
function serverValidationManager($timeout) {
@@ -1,86 +1,76 @@
angular.module("umbraco")
.controller("Umbraco.Editors.BlockEditorController",
function ($scope, localizationService, formHelper) {
var vm = this;
.controller("Umbraco.Editors.BlockEditorController",
function ($scope, localizationService, formHelper) {
var vm = this;
vm.model = $scope.model;
vm.model = $scope.model;
vm.tabs = [];
localizationService.localizeMany([
vm.model.liveEditing ? "prompt_discardChanges" : "general_close",
vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges"
]).then(function (data) {
vm.closeLabel = data[0];
vm.submitLabel = data[1];
});
vm.model = $scope.model;
vm.tabs = [];
if ($scope.model.content && $scope.model.content.variants) {
localizationService.localizeMany([
vm.model.liveEditing ? "prompt_discardChanges" : "general_close",
vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges"
]).then(function (data) {
vm.closeLabel = data[0];
vm.submitLabel = data[1];
});
var apps = $scope.model.content.apps;
if (vm.model.content && vm.model.content.variants) {
vm.tabs = apps;
var apps = vm.model.content.apps;
// replace view of content app.
var contentApp = apps.find(entry => entry.alias === "umbContent");
if (contentApp) {
contentApp.view = "views/common/infiniteeditors/blockeditor/blockeditor.content.html";
if(vm.model.hideContent) {
apps.splice(apps.indexOf(contentApp), 1);
} else if (vm.model.openSettings !== true) {
contentApp.active = true;
}
}
// remove info app:
var infoAppIndex = apps.findIndex(entry => entry.alias === "umbInfo");
apps.splice(infoAppIndex, 1);
}
if (vm.model.settings && vm.model.settings.variants) {
localizationService.localize("blockEditor_tabBlockSettings").then(
function (settingsName) {
var settingsTab = {
"name": settingsName,
"alias": "settings",
"icon": "icon-settings",
"view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html"
};
vm.tabs.push(settingsTab);
if (vm.model.openSettings) {
settingsTab.active = true;
// configure the content app based on settings
var contentApp = apps.find(entry => entry.alias === "umbContent");
if (contentApp) {
if (vm.model.hideContent) {
apps.splice(apps.indexOf(contentApp), 1);
} else if (vm.model.openSettings !== true) {
contentApp.active = true;
}
}
);
}
vm.submitAndClose = function () {
if (vm.model && vm.model.submit) {
// always keep server validations since this will be a nested editor and server validations are global
if (formHelper.submitForm({ scope: $scope, formCtrl: vm.blockForm, keepServerValidation: true })) {
vm.model.submit(vm.model);
if (vm.model.settings && vm.model.settings.variants) {
var settingsApp = apps.find(entry => entry.alias === "settings");
if (settingsApp) {
if (vm.model.openSettings) {
settingsApp.active = true;
}
}
}
vm.tabs = apps;
}
vm.submitAndClose = function () {
if (vm.model && vm.model.submit) {
// always keep server validations since this will be a nested editor and server validations are global
if (formHelper.submitForm({
scope: $scope,
formCtrl: vm.blockForm,
keepServerValidation: true
})) {
vm.model.submit(vm.model);
}
}
}
}
vm.close = function () {
if (vm.model && vm.model.close) {
// TODO: At this stage there could very well have been server errors that have been cleared
// but if we 'close' we are basically cancelling the value changes which means we'd want to cancel
// all of the server errors just cleared. It would be possible to do that but also quite annoying.
// The rudimentary way would be to:
// * Track all cleared server errors here by subscribing to the prefix validation of controls contained here
// * If this is closed, re-add all of those server validation errors
// A more robust way to do this would be to:
// * Add functionality to the serverValidationManager whereby we can remove validation errors and it will
// maintain a copy of the original errors
// * It would have a 'commit' method to commit the removed errors - which we would call in the formHelper.submitForm when it's successful
// * It would have a 'rollback' method to reset the removed errors - which we would call here
vm.close = function () {
if (vm.model && vm.model.close) {
// TODO: At this stage there could very well have been server errors that have been cleared
// but if we 'close' we are basically cancelling the value changes which means we'd want to cancel
// all of the server errors just cleared. It would be possible to do that but also quite annoying.
// The rudimentary way would be to:
// * Track all cleared server errors here by subscribing to the prefix validation of controls contained here
// * If this is closed, re-add all of those server validation errors
// A more robust way to do this would be to:
// * Add functionality to the serverValidationManager whereby we can remove validation errors and it will
// maintain a copy of the original errors
// * It would have a 'commit' method to commit the removed errors - which we would call in the formHelper.submitForm when it's successful
// * It would have a 'rollback' method to reset the removed errors - which we would call here
// TODO: check if content/settings has changed and ask user if they are sure.
vm.model.close(vm.model);
// TODO: check if content/settings has changed and ask user if they are sure.
vm.model.close(vm.model);
}
}
}
}
);
}
);
@@ -1,6 +1,6 @@
<div class="umb-editor-sub-view"
ng-class="'sub-view-' + model.name"
val-sub-view>
val-sub-view="model">
<div
class="umb-editor-sub-view__content"
@@ -5,7 +5,7 @@
class="umb-editor-sub-view"
ng-repeat="subView in subViews track by subView.alias"
ng-class="'sub-view-' + subView.name"
val-sub-view>
val-sub-view="subView">
<div class="umb-editor-sub-view__content"
ng-show="subView.active === true"
@@ -8,13 +8,11 @@
<div ng-repeat="layout in vm.layout track by layout.$block.key">
<button
type="button"
class="btn-reset umb-block-list__block--create-button"
ng-click="vm.showCreateDialog($index, $event)"
ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl"
ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)"
>
<button type="button"
class="btn-reset umb-block-list__block--create-button"
ng-click="vm.showCreateDialog($index, $event)"
ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl"
ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)">
<div class="__plus" ng-style="{'left':inlineCreateButtonCtrl.plusPosX}">+</div>
</button>
@@ -26,28 +24,29 @@
</div>
</div>
<button
ng-if="vm.loading !== true"
id="{{vm.model.alias}}"
type="button"
class="btn-reset umb-block-list__create-button umb-outline"
ng-class="{ '--disabled': vm.availableBlockTypes.length === 0 }"
ng-click="vm.showCreateDialog(vm.layout.length, $event)"
>
<button ng-if="vm.loading !== true"
id="{{vm.model.alias}}"
type="button"
class="btn-reset umb-block-list__create-button umb-outline"
ng-class="{ '--disabled': vm.availableBlockTypes.length === 0 }"
ng-click="vm.showCreateDialog(vm.layout.length, $event)">
<localize key="grid_addElement"></localize>
</button>
<input type="hidden" name="minCount" ng-model="vm.layout" />
<input type="hidden" name="maxCount" ng-model="vm.layout" />
<input type="hidden" name="minCount" ng-model="vm.layout" val-server="minCount" />
<input type="hidden" name="maxCount" ng-model="vm.layout" val-server="maxCount" />
<div ng-messages="vm.propertyForm.minCount.$error" show-validation-on-submit>
<div class="help text-error" ng-message="minCount">
<localize key="validation_entriesShort" tokens="[vm.validationLimit.min, vm.validationLimit.min - vm.layout.length]" watch-tokens="true">Minimum %0% entries, needs <strong>%1%</strong> more.</localize>
</div>
<span class="help-inline" ng-message="valServer" ng-bind-html="vm.propertyForm.minCount.errorMsg">></span>
</div>
<div ng-if="vm.propertyForm.maxCount.$error === true && vm.layout.length > vm.validationLimit.max">
<div class="help text-error">
<div ng-messages="vm.propertyForm.maxCount.$error" show-validation-on-submit>
<div class="help text-error" ng-message="maxCount">
<localize key="validation_entriesExceed" tokens="[vm.validationLimit.max, vm.layout.length - vm.validationLimit.max]" watch-tokens="true">Maximum %0% entries, <strong>%1%</strong> too many.</localize>
</div>
<span class="help-inline" ng-message="valServer" ng-bind-html="vm.propertyForm.maxCount.errorMsg"></span>
</div>
</div>
@@ -23,13 +23,17 @@
> .umb-block-list__block--actions {
opacity: 0;
transition: opacity 120ms;
.--error {
color: @formErrorBorder !important;
}
}
&:hover,
&:focus,
&:focus-within,
&.--active {
> .umb-block-list__block--actions {
opacity: 1;
}
@@ -1,4 +1,4 @@
<ng-form name="vm.blockRowForm" val-server-match="{ 'contains' : vm.layout.$block.content.key }">
<ng-form name="vm.blockRowForm" val-server-match="{ 'contains' : { 'valServerMatchContent': vm.layout.$block.content.key, 'valServerMatchSettings': vm.layout.$block.settings.key } }">
<div class="umb-block-list__block" ng-class="{'--active':vm.layout.$block.active}">
<umb-block-list-block stylesheet="{{::vm.layout.$block.config.stylesheet}}"
@@ -12,20 +12,24 @@
<div class="umb-block-list__block--actions">
<button type="button" class="btn-reset umb-outline action --settings" localize="title" title="actions_editSettings"
ng-click="vm.blockEditorApi.openSettingsForBlock(vm.layout.$block, vm.index);"
ng-click="vm.blockEditorApi.openSettingsForBlock(vm.layout.$block, vm.index, vm.blockRowForm);"
ng-class="{ '--error': vm.blockRowForm.$error.valServerMatchSettings && vm.valFormManager.isShowingValidation() }"
ng-if="vm.layout.$block.showSettings === true">
<i class="icon icon-settings" aria-hidden="true"></i>
<span class="sr-only">
<localize key="general_settings">Settings</localize>
</span>
</button>
<button type="button" class="btn-reset umb-outline action --copy" localize="title" title="actions_copy" ng-click="vm.blockEditorApi.copyBlock(vm.layout.$block);" ng-if="vm.layout.$block.showCopy === true">
<button type="button" class="btn-reset umb-outline action --copy" localize="title" title="actions_copy"
ng-click="vm.blockEditorApi.copyBlock(vm.layout.$block);"
ng-if="vm.layout.$block.showCopy === true">
<i class="icon icon-documents" aria-hidden="true"></i>
<span class="sr-only">
<localize key="general_copy">Copy</localize>
</span>
</button>
<button type="button" class="btn-reset umb-outline action --delete" localize="title" title="actions_delete" ng-click="vm.blockEditorApi.requestDeleteBlock(vm.layout.$block);">
<button type="button" class="btn-reset umb-outline action --delete" localize="title" title="actions_delete"
ng-click="vm.blockEditorApi.requestDeleteBlock(vm.layout.$block);">
<i class="icon icon-trash" aria-hidden="true"></i>
<span class="sr-only">
<localize key="general_delete">Delete</localize>
@@ -555,9 +555,8 @@
});
}
// TODO: We'll need to pass in a parentForm here too
function openSettingsForBlock(block, blockIndex) {
editBlock(block, true, blockIndex);
function openSettingsForBlock(block, blockIndex, parentForm) {
editBlock(block, true, blockIndex, parentForm);
}
vm.blockEditorApi = {
@@ -18,6 +18,9 @@
blockEditorApi: "<",
layout: "<",
index: "<"
},
require: {
valFormManager: "^^valFormManager"
}
}
);
@@ -11,7 +11,6 @@ module.exports = function (config) {
files: [
// Jasmine plugins
'node_modules/jasmine-promise-matchers/dist/jasmine-promise-matchers.js',
//libraries
'node_modules/jquery/dist/jquery.min.js',
@@ -5,24 +5,34 @@
var settingsKey = "2AF42343-C8A2-400D-BA43-4818C2B3CDC5";
var settingsUdi = "umb://element/2AF42343C8A2400DBA434818C2B3CDC5";
var blockEditorService, contentResource, $rootScope, $scope;
var blockEditorService, contentResource, $rootScope, $scope, $q, localizationService, $timeout;
beforeEach(module('umbraco.services'));
beforeEach(module('umbraco.resources'));
beforeEach(module('umbraco.mocks'));
beforeEach(module('umbraco'));
beforeEach(inject(function ($injector, mocksUtils, _$rootScope_) {
beforeEach(inject(function ($injector, mocksUtils, _$rootScope_, _$q_, _$timeout_) {
mocksUtils.disableAuth();
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$q = _$q_;
$timeout = _$timeout_;
contentResource = $injector.get("contentResource");
spyOn(contentResource, "getScaffoldByKey").and.callFake(
function () {
return Promise.resolve(mocksUtils.getMockVariantContent(1234, contentKey, contentUdi))
var scaffold = mocksUtils.getMockVariantContent(1234, contentKey, contentUdi);
return $q.resolve(scaffold);
}
);
// this seems to be required because of the poor promise implementation in localizationService (see TODO in that service)
localizationService = $injector.get("localizationService");
spyOn(localizationService, "localize").and.callFake(
function () {
return $q.resolve("Localized test text");
}
);
@@ -101,13 +111,19 @@
it('load provides data for itemPicker', function (done) {
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
modelObject.load().then(() => {
var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker();
expect(itemPickerOptions.length).toBe(1);
expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey);
done();
modelObject.load().then(() => {
try {
var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker();
expect(itemPickerOptions.length).toBe(1);
expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey);
done();
} catch (e) {
done.fail(e);
}
});
$rootScope.$digest();
$timeout.flush();
});
it('getLayoutEntry has values', function (done) {
@@ -117,16 +133,22 @@
modelObject.load().then(() => {
var layout = modelObject.getLayout();
try {
var layout = modelObject.getLayout();
expect(layout).not.toBeUndefined();
expect(layout.length).toBe(1);
expect(layout[0]).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0]);
expect(layout[0].udi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].udi);
expect(layout).not.toBeUndefined();
expect(layout.length).toBe(1);
expect(layout[0]).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0]);
expect(layout[0].udi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].udi);
done();
done();
} catch (e) {
done.fail(e);
}
});
$rootScope.$digest();
$timeout.flush();
});
it('getBlockObject has values', function (done) {
@@ -151,6 +173,8 @@
}
});
$rootScope.$digest();
$timeout.flush();
});
@@ -169,18 +193,23 @@
blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue";
$rootScope.$digest();// invoke angularJS Store.
// invoke angularJS Store.
$timeout(function () {
expect(blockObject.data).toEqual(propertyModel.contentData[0]);
expect(blockObject.data.testproperty).toBe("anotherTestValue");
expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue");
expect(blockObject.data).toEqual(propertyModel.contentData[0]);
expect(blockObject.data.testproperty).toBe("anotherTestValue");
expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue");
done();
});
done();
} catch (e) {
done.fail(e);
}
});
$rootScope.$digest();
$timeout.flush();
});
@@ -204,17 +233,22 @@
blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA";
blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D");
$rootScope.$digest();// invoke angularJS Store.
// invoke angularJS Store.
$timeout(function () {
expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA");
expect(propertyModel.contentData[0].testproperty.list.length).toBe(4);
expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA");
expect(propertyModel.contentData[0].testproperty.list.length).toBe(4);
done();
});
done();
} catch (e) {
done.fail(e);
}
}
});
$rootScope.$digest();
$timeout.flush();
});
it('layout is referencing layout of propertyModel', function (done) {
@@ -236,6 +270,8 @@
done();
});
$rootScope.$digest();
$timeout.flush();
});
it('removeDataAndDestroyModel removes data', function (done) {
@@ -270,6 +306,9 @@
done.fail(e);
}
});
$rootScope.$digest();
$timeout.flush();
});
it('getBlockObject of block with settings has values', function (done) {
@@ -291,6 +330,8 @@
done();
});
$rootScope.$digest();
$timeout.flush();
});
@@ -309,21 +350,23 @@
blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue";
blockObject.settings.variants[0].tabs[0].properties[0].value = "anotherTestValueForSettings";
$rootScope.$digest();// invoke angularJS Store.
// invoke angularJS Store.
$timeout(function () {
expect(blockObject.data).toEqual(propertyModel.contentData[0]);
expect(blockObject.data.testproperty).toBe("anotherTestValue");
expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue");
expect(blockObject.data).toEqual(propertyModel.contentData[0]);
expect(blockObject.data.testproperty).toBe("anotherTestValue");
expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue");
expect(blockObject.settingsData).toEqual(propertyModel.settingsData[0]);
expect(blockObject.settingsData.testproperty).toBe("anotherTestValueForSettings");
expect(propertyModel.settingsData[0].testproperty).toBe("anotherTestValueForSettings");
expect(blockObject.settingsData).toEqual(propertyModel.settingsData[0]);
expect(blockObject.settingsData.testproperty).toBe("anotherTestValueForSettings");
expect(propertyModel.settingsData[0].testproperty).toBe("anotherTestValueForSettings");
//
done();
done();
});
});
$rootScope.$digest();
$timeout.flush();
});
@@ -352,20 +395,25 @@
blockObject.settings.variants[0].tabs[0].properties[0].value.list[0] = "settingsValue";
blockObject.settings.variants[0].tabs[0].properties[0].value.list.push("settingsNewValue");
$rootScope.$digest();// invoke angularJS Store.
// invoke angularJS Store.
$timeout(function () {
expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA");
expect(propertyModel.contentData[0].testproperty.list.length).toBe(4);
expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA");
expect(propertyModel.contentData[0].testproperty.list.length).toBe(4);
expect(propertyModel.settingsData[0].testproperty.list[0]).toBe("settingsValue");
expect(propertyModel.settingsData[0].testproperty.list.length).toBe(4);
expect(propertyModel.settingsData[0].testproperty.list[0]).toBe("settingsValue");
expect(propertyModel.settingsData[0].testproperty.list.length).toBe(4);
done();
});
done();
} catch (e) {
done.fail(e);
}
});
$rootScope.$digest();
$timeout.flush();
});
+1 -8
View File
@@ -126,10 +126,6 @@
<Project>{52ac0ba8-a60e-4e36-897b-e8b97a54ed1c}</Project>
<Name>Umbraco.ModelsBuilder.Embedded</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.TestData\Umbraco.TestData.csproj">
<Project>{fb5676ed-7a69-492c-b802-e7b24144c0fc}</Project>
<Name>Umbraco.TestData</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
<Name>Umbraco.Web</Name>
@@ -352,10 +348,7 @@
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>8700</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:8700</IISUrl>
<DevelopmentServerPort>8640</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:8640</IISUrl>
<IISUrl>http://localhost:8700</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>False</UseCustomServer>
<CustomServerUrl>
@@ -2,7 +2,9 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Razor.Parser.SyntaxTree;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
@@ -42,21 +44,24 @@ namespace Umbraco.Web.PropertyEditors
#region Value Editor
protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService);
protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService, Logger);
internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService; // TODO: Not used yet but we'll need it to fill in the FromEditor/ToEditor
private readonly ILogger _logger;
private readonly BlockEditorValues _blockEditorValues;
public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService)
public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, ILogger logger)
: base(attribute)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService);
_logger = logger;
_blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger);
Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService));
Validators.Add(new MinMaxValidator(_blockEditorValues, textService));
}
public IEnumerable<UmbracoEntityReference> GetReferences(object value)
@@ -68,8 +73,8 @@ namespace Umbraco.Web.PropertyEditors
if (blockEditorData == null)
return Enumerable.Empty<UmbracoEntityReference>();
// TODO: What about Settings?
foreach (var row in blockEditorData.BlockValue.ContentData)
// loop through all content and settings data
foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
{
foreach (var prop in row.PropertyValues)
{
@@ -123,38 +128,41 @@ namespace Umbraco.Web.PropertyEditors
{
foreach (var prop in row.PropertyValues)
{
try
// create a temp property with the value
// - force it to be culture invariant as the block editor can't handle culture variant element properties
prop.Value.PropertyType.Variations = ContentVariation.Nothing;
var tempProp = new Property(prop.Value.PropertyType);
tempProp.SetValue(prop.Value.Value);
// convert that temp property, and store the converted value
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null)
{
// create a temp property with the value
// - force it to be culture invariant as the block editor can't handle culture variant element properties
prop.Value.PropertyType.Variations = ContentVariation.Nothing;
var tempProp = new Property(prop.Value.PropertyType);
tempProp.SetValue(prop.Value.Value);
// convert that temp property, and store the converted value
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null)
{
// NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists.
// if the property editor doesn't exist I think everything will break anyways?
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString();
continue;
}
var tempConfig = dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
var valEditor = propEditor.GetValueEditor(tempConfig);
var convValue = valEditor.ToEditor(tempProp, dataTypeService);
// NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists.
// if the property editor doesn't exist I think everything will break anyways?
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue;
row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString();
continue;
}
catch (InvalidOperationException)
var dataType = dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId);
if (dataType == null)
{
// deal with weird situations by ignoring them (no comment)
row.PropertyValues.Remove(prop.Key);
_logger.Warn<BlockEditorPropertyValueEditor>(
"ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
prop.Key, row.Key, property.PropertyType.Alias);
continue;
}
var tempConfig = dataType.Configuration;
var valEditor = propEditor.GetValueEditor(tempConfig);
var convValue = valEditor.ToEditor(tempProp, dataTypeService);
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue;
}
}
@@ -216,6 +224,41 @@ namespace Umbraco.Web.PropertyEditors
#endregion
}
/// <summary>
/// Validates the min/max of the block editor
/// </summary>
private class MinMaxValidator : IValueValidator
{
private readonly BlockEditorValues _blockEditorValues;
private readonly ILocalizedTextService _textService;
public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService)
{
_blockEditorValues = blockEditorValues;
_textService = textService;
}
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
{
var blockConfig = (BlockListConfiguration)dataTypeConfiguration;
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
if ((blockEditorData == null && blockConfig?.ValidationLimit?.Min > 0)
|| (blockEditorData != null && blockEditorData.Layout.Count() < blockConfig?.ValidationLimit?.Min))
{
yield return new ValidationResult(
_textService.Localize("validation/entriesShort", new[] { blockConfig.ValidationLimit.Min.ToString(), (blockConfig.ValidationLimit.Min - blockEditorData.Layout.Count()).ToString() }),
new[] { "minCount" });
}
if (blockEditorData != null && blockEditorData.Layout.Count() > blockConfig?.ValidationLimit?.Max)
{
yield return new ValidationResult(
_textService.Localize("validation/entriesExceed", new[] { blockConfig.ValidationLimit.Max.ToString(), (blockEditorData.Layout.Count() - blockConfig.ValidationLimit.Max).ToString() }),
new[] { "maxCount" });
}
}
}
internal class BlockEditorValidator : ComplexEditorValidator
{
private readonly BlockEditorValues _blockEditorValues;
@@ -230,7 +273,7 @@ namespace Umbraco.Web.PropertyEditors
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
if (blockEditorData != null)
{
foreach (var row in blockEditorData.BlockValue.ContentData)
foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
{
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key);
foreach (var prop in row.PropertyValues)
@@ -251,11 +294,13 @@ namespace Umbraco.Web.PropertyEditors
{
private readonly Lazy<Dictionary<Guid, IContentType>> _contentTypes;
private readonly BlockEditorDataConverter _dataConverter;
private readonly ILogger _logger;
public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService)
public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger)
{
_contentTypes = new Lazy<Dictionary<Guid, IContentType>>(() => contentTypeService.GetAll().ToDictionary(c => c.Key));
_dataConverter = dataConverter;
_logger = logger;
}
private IContentType GetElementType(BlockItemData item)
@@ -318,6 +363,8 @@ namespace Umbraco.Web.PropertyEditors
if (!propertyTypes.TryGetValue(prop.Key, out var propType))
{
block.RawPropertyValues.Remove(prop.Key);
_logger.Warn<BlockEditorValues>("The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}",
prop.Key, block.Key, prop.Key, contentType.Alias);
}
else
{
@@ -25,7 +25,7 @@ namespace Umbraco.Web.PropertyEditors
[DataEditor(
Constants.PropertyEditors.Aliases.NestedContent,
"Nested Content",
"nestedcontent",
"nestedcontent",
ValueType = ValueTypes.Json,
Group = Constants.PropertyEditors.Groups.Lists,
Icon = "icon-thumbnail-list")]
@@ -61,19 +61,21 @@ namespace Umbraco.Web.PropertyEditors
#region Value Editor
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService);
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService, Logger);
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService;
private readonly ILogger _logger;
private readonly NestedContentValues _nestedContentValues;
public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService)
public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, ILogger logger)
: base(attribute)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_logger = logger;
_nestedContentValues = new NestedContentValues(contentTypeService);
Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, textService));
}
@@ -120,10 +122,14 @@ namespace Umbraco.Web.PropertyEditors
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue;
}
catch (InvalidOperationException)
catch (InvalidOperationException ex)
{
// deal with weird situations by ignoring them (no comment)
row.RawPropertyValues.Remove(prop.Key);
_logger.Warn<NestedContentPropertyValueEditor>(
ex,
"ConvertDbToString removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
prop.Key, row.Id, propertyType.Alias);
}
}
}
@@ -185,10 +191,14 @@ namespace Umbraco.Web.PropertyEditors
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue == null ? null : JToken.FromObject(convValue);
}
catch (InvalidOperationException)
catch (InvalidOperationException ex)
{
// deal with weird situations by ignoring them (no comment)
row.RawPropertyValues.Remove(prop.Key);
_logger.Warn<NestedContentPropertyValueEditor>(
ex,
"ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
prop.Key, row.Id, property.PropertyType.Alias);
}
}
}
@@ -7,6 +7,10 @@ namespace Umbraco.Web.PropertyEditors.Validation
/// <summary>
/// A collection of <see cref="ComplexEditorPropertyTypeValidationResult"/> for an element type within complex editor represented by an Element Type
/// </summary>
/// <remarks>
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
/// </remarks>
public class ComplexEditorElementTypeValidationResult : ValidationResult
{
public ComplexEditorElementTypeValidationResult(string elementTypeAlias, Guid blockId)
@@ -8,6 +8,10 @@ namespace Umbraco.Web.PropertyEditors.Validation
/// <summary>
/// A collection of <see cref="ValidationResult"/> for a property type within a complex editor represented by an Element Type
/// </summary>
/// <remarks>
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
/// </remarks>
public class ComplexEditorPropertyTypeValidationResult : ValidationResult
{
public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias)
@@ -8,7 +8,10 @@ namespace Umbraco.Web.PropertyEditors.Validation
/// A collection of <see cref="ComplexEditorElementTypeValidationResult"/> for a complex editor represented by an Element Type
/// </summary>
/// <remarks>
/// For example, each <see cref="ComplexEditorValidationResult"/> represents validation results for a row in Nested Content
/// For example, each <see cref="ComplexEditorValidationResult"/> represents validation results for a row in Nested Content.
///
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
/// </remarks>
public class ComplexEditorValidationResult : ValidationResult
{
@@ -7,7 +7,10 @@ namespace Umbraco.Web.PropertyEditors.Validation
/// Custom <see cref="ValidationResult"/> for content properties
/// </summary>
/// <remarks>
/// This clones the original result and then ensures the nested result if it's the correct type
/// This clones the original result and then ensures the nested result if it's the correct type.
///
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
/// </remarks>
public class ContentPropertyValidationResult : ValidationResult
{
@@ -15,6 +15,9 @@ namespace Umbraco.Web.PropertyEditors.Validation
/// <remarks>
/// This converter is specifically used to convert validation results for content in order to be able to have nested
/// validation results for complex editors.
///
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
/// </remarks>
internal class ValidationResultConverter : JsonConverter
{