Merge pull request #8493 from umbraco/v8/feature/block-list-settings-validation
Block list editor - settings validation
This commit is contained in:
+312
-334
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
+58
-24
@@ -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";
|
||||
|
||||
+38
-19
@@ -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) {
|
||||
|
||||
|
||||
+61
-71
@@ -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"
|
||||
|
||||
+18
-19
@@ -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>
|
||||
|
||||
+5
-1
@@ -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;
|
||||
}
|
||||
|
||||
+8
-4
@@ -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>
|
||||
|
||||
+2
-3
@@ -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 = {
|
||||
|
||||
+3
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+4
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user