Converts umbProperty to a component, gets nested valPropertyMsg validators clearing (as a prototype), need to check TODOs, test inline editing, etc...

This commit is contained in:
Shannon
2020-07-07 12:50:15 +10:00
parent 7a482a1a9f
commit c4e7929e68
8 changed files with 267 additions and 166 deletions

View File

@@ -3,8 +3,81 @@
* @name umbraco.directives.directive:umbProperty
* @restrict E
**/
angular.module("umbraco.directives")
.directive('umbProperty', function (userService, serverValidationManager, udiService) {
(function () {
'use strict';
angular
.module("umbraco.directives")
.component('umbProperty', {
templateUrl: 'views/components/property/umb-property.html',
controller: UmbPropertyController,
controllerAs: 'vm',
transclude: true,
require: {
parentUmbProperty: '?^^umbProperty'
},
bindings: {
property: "=",
elementUdi: "@",
// optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/)
propertyAlias: "@",
showInherit: "<",
inheritsFrom: "<"
}
});
function UmbPropertyController($scope, userService, serverValidationManager, udiService, angularHelper) {
const vm = this;
vm.$onInit = onInit;
vm.setPropertyError = function (errorMsg) {
vm.property.propertyErrorMessage = errorMsg;
};
vm.propertyActions = [];
vm.setPropertyActions = function (actions) {
vm.propertyActions = actions;
};
// returns the unique Id for the property to be used as the validation key for server side validation logic
vm.getValidationPath = function () {
// the elementUdi will be empty when this is not a nested property
var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias;
vm.elementUdi = ensureUdi(vm.elementUdi);
return serverValidationManager.createPropertyValidationKey(propAlias, vm.elementUdi);
}
vm.getParentValidationPath = function () {
if (!vm.parentUmbProperty) {
return null;
}
return vm.parentUmbProperty.getValidationPath();
}
function onInit() {
vm.controlLabelTitle = null;
if (Umbraco.Sys.ServerVariables.isDebuggingEnabled) {
userService.getCurrentUser().then(function (u) {
if (u.allowedSections.indexOf("settings") !== -1 ? true : false) {
vm.controlLabelTitle = vm.property.alias;
}
});
}
vm.elementUdi = ensureUdi(vm.elementUdi);
if (!vm.parentUmbProperty) {
// not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope
// inheritance is (i.e.infinite editing)
var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "UmbPropertyController");
vm.parentUmbProperty = found ? found.vm : null;
}
}
// if only a guid is passed in, we'll ensure a correct udi structure
function ensureUdi(udi) {
@@ -13,61 +86,6 @@ angular.module("umbraco.directives")
}
return udi;
}
return {
scope: {
property: "=",
elementUdi: "@",
// optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/)
propertyAlias: "@",
showInherit: "<",
inheritsFrom: "<"
},
transclude: true,
restrict: 'E',
replace: true,
templateUrl: 'views/components/property/umb-property.html',
link: function (scope, element, attr, ctrls) {
scope.controlLabelTitle = null;
if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) {
userService.getCurrentUser().then(function (u) {
if(u.allowedSections.indexOf("settings") !== -1 ? true : false) {
scope.controlLabelTitle = scope.property.alias;
}
});
}
scope.elementUdi = ensureUdi(scope.elementUdi);
},
//Define a controller for this directive to expose APIs to other directives
controller: function ($scope) {
var self = this;
//set the API properties/methods
self.property = $scope.property;
self.setPropertyError = function (errorMsg) {
$scope.property.propertyErrorMessage = errorMsg;
};
$scope.propertyActions = [];
self.setPropertyActions = function(actions) {
$scope.propertyActions = actions;
};
// returns the unique Id for the property to be used as the validation key for server side validation logic
self.getValidationPath = function () {
// the elementUdi will be empty when this is not a nested property
var propAlias = $scope.propertyAlias ? $scope.propertyAlias : $scope.property.alias;
$scope.elementUdi = ensureUdi($scope.elementUdi);
return serverValidationManager.createPropertyValidationKey(propAlias, $scope.elementUdi);
}
$scope.getValidationPath = self.getValidationPath;
}
};
});
})();

View File

@@ -12,18 +12,25 @@
* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will
* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly.
**/
function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService) {
function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) {
var SHOW_VALIDATION_CLASS_NAME = "show-validation";
var SAVING_EVENT_NAME = "formSubmitting";
var SAVED_EVENT_NAME = "formSubmitted";
return {
require: ["form", "^^?valFormManager", "^^?valSubView"],
restrict: "A",
controller: function($scope) {
function notify(scope) {
scope.$broadcast("valStatusChanged", { form: scope.formCtrl });
}
function ValFormManagerController($scope) {
//This exposes an API for direct use with this directive
// We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and
// because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is
// an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager
// in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing)
$scope.valFormManager = this;
var unsubscribe = [];
var self = this;
@@ -37,13 +44,46 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
this.showValidation = $scope.showValidation === true;
this.notify = function () {
notify($scope);
}
this.isValid = function () {
return !$scope.formCtrl.$invalid;
}
//Ensure to remove the event handlers when this instance is destroyted
$scope.$on('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();
}
});
},
}
/**
* Find's the valFormManager in the scope/DOM hierarchy
* @param {any} scope
* @param {any} ctrls
* @param {any} index
*/
function getAncestorValFormManager(scope, ctrls, index) {
// first check the normal directive inheritance which relies on DOM inheritance
var found = ctrls[index];
if (found) {
return found;
}
// not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope
// inheritance is (i.e.infinite editing)
var found = angularHelper.traverseScopeChain(scope, s => s && s.valFormManager && s.valFormManager.constructor.name === "ValFormManagerController");
return found ? found.valFormManager : null;
}
return {
require: ["form", "^^?valFormManager", "^^?valSubView"],
restrict: "A",
controller: ValFormManagerController,
link: function (scope, element, attr, ctrls) {
function notifySubView() {
@@ -52,8 +92,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
}
}
var formCtrl = ctrls[0];
var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null;
var formCtrl = scope.formCtrl = ctrls[0];
var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1);
var subView = ctrls.length > 1 ? ctrls[2] : null;
var labels = {};
@@ -98,7 +138,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
//this is the value we watch to notify of any validation changes on the form
return sum;
}, function (e) {
scope.$broadcast("valStatusChanged", { form: formCtrl });
notify(scope);
notifySubView();

View File

@@ -8,11 +8,13 @@
* We will listen for server side validation changes
* and when an error is detected for this property we'll show the error message.
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
* We don't set the validity of this validator to false when client side validation fails, only when server side
* validation fails however we do respond to the client side validation changes to display error and adjust UI state.
**/
function valPropertyMsg(serverValidationManager, localizationService) {
function valPropertyMsg(serverValidationManager, localizationService, angularHelper) {
return {
require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'],
require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent', '?^^valPropertyMsg'],
replace: true,
restrict: "E",
template: "<div ng-show=\"errorMsg != ''\" class='alert alert-error property-error' >{{errorMsg}}</div>",
@@ -93,7 +95,14 @@ function valPropertyMsg(serverValidationManager, localizationService) {
// we need to re-validate it for the server side validator so the user can resubmit
// the form. Of course normal client-side validators will continue to execute.
function startWatch() {
// TODO: Can we watch on something other than the value?? This doesn't work for complex editors especially once that have a
// viewmodel/model setup the value that this is watching doesn't actually get updated by a sub-editor in all cases.
// we can probably watch the formCtrl view value? But then we also don't want this to watch complex values that have sub editors anyways
// since that might end up clearing the whole chain of valPropertyMsg when a sub value is changed (in some cases, not with the block editor).
//if there's not already a watch
if (!watcher) {
watcher = scope.$watch("currentProperty.value",
function (newValue, oldValue) {
@@ -116,11 +125,10 @@ function valPropertyMsg(serverValidationManager, localizationService) {
if (errCount === 0
|| (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg))
|| (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) {
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
resetError();
}
else if (showValidation && scope.errorMsg === "") {
formCtrl.$setValidity('valPropertyMsg', false);
formCtrl.$setValidity('valPropertyMsg', false, formCtrl);
scope.errorMsg = getErrorMsg();
}
}, true);
@@ -135,6 +143,35 @@ function valPropertyMsg(serverValidationManager, localizationService) {
}
}
function resetError() {
var hadError = hasError;
hasError = false;
formCtrl.$setValidity('valPropertyMsg', true, formCtrl);
scope.errorMsg = "";
stopWatch();
// if we had an error, then check on the current valFormManager to see if it's
// now valid, if it is it means that the containing form (i.e. the form rendering)
// properties for an element/content type) is now valid which means we can clear
// the parent's valPropertyMsg if there is one. This will only occur with complex editors
// where we have nested umb-property components.
if (hadError) {
scope.$evalAsync(function () {
if (valFormManager.isValid()) {
// TODO: I think we might have to use formCtrl.$$parentForm here since when using inline editing like
// nestedcontent style, there won't be a 'parent' valFormManager, or will there? i'm unsure
var parentValidationKey = umbPropCtrl.getParentValidationPath();
if (parentValidationKey) {
var parentForm = formCtrl.$$parentForm;
serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment);
}
}
});
}
}
function checkValidationStatus() {
if (formCtrl.$invalid) {
//first we need to check if the valPropertyMsg validity is invalid
@@ -148,9 +185,8 @@ function valPropertyMsg(serverValidationManager, localizationService) {
// errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared
if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) {
hasError = false;
showValidation = false;
scope.errorMsg = "";
resetError();
// if there's no value, the controls can be reset, which clears the error state on formCtrl
for (let control of formCtrl.$getControls()) {
@@ -167,13 +203,11 @@ function valPropertyMsg(serverValidationManager, localizationService) {
}
}
else {
hasError = false;
scope.errorMsg = "";
resetError();
}
}
else {
hasError = false;
scope.errorMsg = "";
resetError();
}
}
@@ -203,17 +237,14 @@ function valPropertyMsg(serverValidationManager, localizationService) {
startWatch();
}
else if (!hasError) {
scope.errorMsg = "";
stopWatch();
resetError();
}
}));
//listen for the forms saved event
unsubscribe.push(scope.$on("formSubmitted", function (ev, args) {
showValidation = false;
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
stopWatch();
resetError();
}));
//listen for server validation changes
@@ -224,7 +255,7 @@ function valPropertyMsg(serverValidationManager, localizationService) {
// indicate that a content property is invalid at the property level since developers may not actually implement
// the correct field validation in their property editors.
if (scope.currentProperty) { //this can be null if no property was assigned
if (scope.currentProperty) { //this can be null if no property was assigned, TODO: I don't believe it can? If it was null we'd get errors above
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
hasError = !isValid;
@@ -232,14 +263,11 @@ function valPropertyMsg(serverValidationManager, localizationService) {
//set the error message to the server message
scope.errorMsg = propertyErrors[0].errorMsg ? propertyErrors[0].errorMsg : labels.propertyHasErrors;
//flag that the current validator is invalid
formCtrl.$setValidity('valPropertyMsg', false);
formCtrl.$setValidity('valPropertyMsg', false, formCtrl);
startWatch();
}
else {
scope.errorMsg = "";
//flag that the current validator is valid
formCtrl.$setValidity('valPropertyMsg', true);
stopWatch();
resetError();
}
}
@@ -250,7 +278,6 @@ function valPropertyMsg(serverValidationManager, localizationService) {
currentSegment
)
);
}
//when the scope is disposed we need to unsubscribe

View File

@@ -81,6 +81,7 @@ function valServer(serverValidationManager) {
if (modelCtrl.$invalid) {
modelCtrl.$setValidity('valServer', true);
console.log("valServer cleared (watch)");
//clear the server validation entry
serverValidationManager.removePropertyError(getPropertyValidationKey(), currentCulture, fieldName, currentSegment);
@@ -101,12 +102,14 @@ function valServer(serverValidationManager) {
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
if (!isValid) {
modelCtrl.$setValidity('valServer', false);
console.log("valServer error");
//assign an error msg property to the current validator
modelCtrl.errorMsg = propertyErrors[0].errorMsg;
startWatch();
}
else {
modelCtrl.$setValidity('valServer', true);
console.log("valServer cleared");
//reset the error message
modelCtrl.errorMsg = "";
stopWatch();

View File

@@ -16,10 +16,10 @@ function serverValidationManager($timeout, udiService) {
var items = [];
/** calls the callback specified with the errors specified, used internally */
function executeCallback(errorsForCallback, callback, culture, segment) {
function executeCallback(errorsForCallback, callback, culture, segment, isValid) {
callback.apply(instance, [
false, // pass in a value indicating it is invalid
isValid, // pass in a value indicating it is invalid
errorsForCallback, // pass in the errors for this item
items, // pass in all errors in total
culture, // pass the culture that we are listing for.
@@ -99,21 +99,21 @@ function serverValidationManager($timeout, udiService) {
//its a field error callback
var fieldErrors = getFieldErrors(cb.fieldName);
if (fieldErrors.length > 0) {
executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment);
executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, false);
}
}
else if (cb.propertyAlias != null) {
//its a property error
var propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName);
if (propErrors.length > 0) {
executeCallback(propErrors, cb.callback, cb.culture, cb.segment);
executeCallback(propErrors, cb.callback, cb.culture, cb.segment, false);
}
}
else {
//its a variant error
var variantErrors = getVariantErrors(cb.culture, cb.segment);
if (variantErrors.length > 0) {
executeCallback(variantErrors, cb.callback, cb.culture, cb.segment);
executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, false);
}
}
}
@@ -247,7 +247,7 @@ function serverValidationManager($timeout, udiService) {
var cbs = getFieldCallbacks(fieldName);
//call each callback for this error
for (var cb in cbs) {
executeCallback(errorsForCallback, cbs[cb].callback, null, null);
executeCallback(errorsForCallback, cbs[cb].callback, null, null, false);
}
}
@@ -309,14 +309,14 @@ function serverValidationManager($timeout, udiService) {
var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment);
//call each callback for this error
for (var cb in cbs) {
executeCallback(errorsForCallback, cbs[cb].callback, culture, segment);
executeCallback(errorsForCallback, cbs[cb].callback, culture, segment, false);
}
//execute variant specific callbacks here too when a propery error is added
var variantCbs = getVariantCallbacks(culture, segment);
//call each callback for this error
for (var cb in variantCbs) {
executeCallback(errorsForCallback, variantCbs[cb].callback, culture, segment);
executeCallback(errorsForCallback, variantCbs[cb].callback, culture, segment, false);
}
}
@@ -679,7 +679,18 @@ function serverValidationManager($timeout, udiService) {
//remove the item
items = _.reject(items, function (item) {
return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
var found = (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
if (found) {
//find all errors for this item
var errorsForCallback = getPropertyErrors(propertyAlias, culture, segment, fieldName);
//we should now call all of the call backs registered for this error
var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment);
//call each callback for this error to tell them it is now valid
for (var cb in cbs) {
executeCallback(errorsForCallback, cbs[cb].callback, culture, segment, true);
}
}
return found;
});
},

View File

@@ -151,7 +151,7 @@ h6.-black {
}
}
.umb-property:last-of-type .umb-control-group {
umb-property:last-of-type .umb-control-group {
&::after {
margin-top: 0px;
height: 0;

View File

@@ -1,31 +1,32 @@
<div class="umb-property">
<ng-form name="propertyForm">
<div class="control-group umb-control-group" ng-class="{hidelabel:property.hideLabel, 'umb-control-group__listview': property.alias === '_umb_containerView'}">
<div class="control-group umb-control-group"
ng-class="{hidelabel:vm.property.hideLabel, 'umb-control-group__listview': vm.property.alias === '_umb_containerView'}">
<pre>{{ getValidationPath() }}</pre>
<pre>{{ vm.getValidationPath() }}</pre>
<val-property-msg></val-property-msg>
<div class="umb-el-wrap">
<div class="control-header" ng-hide="property.hideLabel === true">
<small ng-if="showInherit" class="db" style="padding-top: 0; margin-bottom: 5px;">
<localize key="contentTypeEditor_inheritedFrom"></localize> {{inheritsFrom}}
<div class="control-header" ng-hide="vm.property.hideLabel === true">
<small ng-if="vm.showInherit" class="db" style="padding-top: 0; margin-bottom: 5px;">
<localize key="contentTypeEditor_inheritedFrom"></localize> {{vm.inheritsFrom}}
</small>
<label class="control-label" for="{{property.alias}}" ng-attr-title="{{controlLabelTitle}}">
<label class="control-label" for="{{vm.property.alias}}" ng-attr-title="{{vm.controlLabelTitle}}">
{{property.label}}
{{vm.property.label}}
<span ng-if="property.validation.mandatory">
<span ng-if="vm.property.validation.mandatory">
<strong class="umb-control-required">*</strong>
</span>
</label>
<umb-property-actions actions="propertyActions"></umb-property-actions>
<umb-property-actions actions="vm.propertyActions"></umb-property-actions>
<small class="control-description" ng-if="property.description" ng-bind-html="property.description | preserveNewLineInHtml"></small>
<small class="control-description" ng-if="vm.property.description" ng-bind-html="vm.property.description | preserveNewLineInHtml"></small>
</div>
<div class="controls" ng-transclude>

View File

@@ -208,7 +208,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
if (dto.EditData == null)
{
if (Debugger.IsAttached)
throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding.");
throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding.");
Current.Logger.Warn<DatabaseDataSource>("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id);
}
else
@@ -235,7 +235,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
if (dto.PubData == null)
{
if (Debugger.IsAttached)
throw new Exception("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding.");
throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding.");
Current.Logger.Warn<DatabaseDataSource>("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id);
}
else
@@ -274,7 +274,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto)
{
if (dto.EditData == null)
throw new Exception("No data for media " + dto.Id);
throw new InvalidOperationException("No data for media " + dto.Id);
var nested = DeserializeNestedData(dto.EditData);