Merge branch 'v8/8.7' into v8/dev

# Conflicts:
#	src/SolutionInfo.cs
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
#	src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js
#	src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html
#	src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html
#	src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html
#	src/Umbraco.Web.UI.Client/src/views/logviewer/search.html
#	src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js
#	src/Umbraco.Web.UI.Client/src/views/packages/edit.html
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html
This commit is contained in:
Sebastiaan Janssen
2020-09-02 10:14:10 +02:00
108 changed files with 1124 additions and 652 deletions

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// The base class for any strongly typed model for a Block editor implementation
/// </summary>
public abstract class BlockEditorModel
{
protected BlockEditorModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData)
{
ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData));
SettingsData = settingsData ?? new List<IPublishedContent>();
}
public BlockEditorModel()
{
}
/// <summary>
/// The content data items of the Block List editor
/// </summary>
[DataMember(Name = "contentData")]
public IEnumerable<IPublishedElement> ContentData { get; set; } = new List<IPublishedContent>();
/// <summary>
/// The settings data items of the Block List editor
/// </summary>
[DataMember(Name = "settingsData")]
public IEnumerable<IPublishedElement> SettingsData { get; set; } = new List<IPublishedContent>();
}
}

View File

@@ -49,8 +49,14 @@ namespace Umbraco.Core.Models.Blocks
/// </summary>
public class BlockPropertyValue
{
public object Value { get; set; }
public PropertyType PropertyType { get; set; }
public BlockPropertyValue(object value, PropertyType propertyType)
{
Value = value;
PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
}
public object Value { get; }
public PropertyType PropertyType { get; }
}
}
}

View File

@@ -7,10 +7,10 @@ namespace Umbraco.Core.Models.Blocks
/// <summary>
/// Represents a layout item for the Block List editor
/// </summary>
[DataContract(Name = "blockListLayout", Namespace = "")]
public class BlockListLayoutReference : IBlockReference<IPublishedElement>
[DataContract(Name = "block", Namespace = "")]
public class BlockListItem : IBlockReference<IPublishedElement>
{
public BlockListLayoutReference(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
{
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
Content = content ?? throw new ArgumentNullException(nameof(content));
@@ -33,19 +33,13 @@ namespace Umbraco.Core.Models.Blocks
/// <summary>
/// The content data item referenced
/// </summary>
/// <remarks>
/// This is ignored from serialization since it is just a reference to the actual data element
/// </remarks>
[IgnoreDataMember]
[DataMember(Name = "content")]
public IPublishedElement Content { get; }
/// <summary>
/// The settings data item referenced
/// </summary>
/// <remarks>
/// This is ignored from serialization since it is just a reference to the actual data element
/// </remarks>
[IgnoreDataMember]
[DataMember(Name = "settings")]
public IPublishedElement Settings { get; }
}
}

View File

@@ -1,4 +1,7 @@
using System.Collections.Generic;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Umbraco.Core.Models.PublishedContent;
@@ -8,26 +11,54 @@ namespace Umbraco.Core.Models.Blocks
/// The strongly typed model for the Block List editor
/// </summary>
[DataContract(Name = "blockList", Namespace = "")]
public class BlockListModel : BlockEditorModel
public class BlockListModel : IReadOnlyList<BlockListItem>
{
private readonly IReadOnlyList<BlockListItem> _layout = new List<BlockListItem>();
public static BlockListModel Empty { get; } = new BlockListModel();
private BlockListModel()
{
}
public BlockListModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData, IEnumerable<BlockListLayoutReference> layout)
: base(contentData, settingsData)
public BlockListModel(IEnumerable<BlockListItem> layout)
{
Layout = layout;
_layout = layout.ToList();
}
public int Count => _layout.Count;
/// <summary>
/// The layout items of the Block List editor
/// Get the block by index
/// </summary>
[DataMember(Name = "layout")]
public IEnumerable<BlockListLayoutReference> Layout { get; } = new List<BlockListLayoutReference>();
/// <param name="index"></param>
/// <returns></returns>
public BlockListItem this[int index] => _layout[index];
/// <summary>
/// Get the block by content Guid
/// </summary>
/// <param name="contentKey"></param>
/// <returns></returns>
public BlockListItem this[Guid contentKey] => _layout.FirstOrDefault(x => x.Content.Key == contentKey);
/// <summary>
/// Get the block by content element Udi
/// </summary>
/// <param name="contentUdi"></param>
/// <returns></returns>
public BlockListItem this[Udi contentUdi]
{
get
{
if (!(contentUdi is GuidUdi guidUdi)) return null;
return _layout.FirstOrDefault(x => x.Content.Key == guidUdi.Guid);
}
}
public IEnumerator<BlockListItem> GetEnumerator() => _layout.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -134,7 +134,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (objectTypes.Any())
{
sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", objectTypes);
sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", new { objectTypes = objectTypes });
}
return Database.Fetch<string>(sql);

View File

@@ -150,8 +150,7 @@
<Compile Include="Persistence\Repositories\Implement\InstallationRepository.cs" />
<Compile Include="Services\Implement\InstallationService.cs" />
<Compile Include="Migrations\Upgrade\V_8_6_0\AddMainDomLock.cs" />
<Compile Include="Models\Blocks\BlockEditorModel.cs" />
<Compile Include="Models\Blocks\BlockListLayoutReference.cs" />
<Compile Include="Models\Blocks\BlockListItem.cs" />
<Compile Include="Models\Blocks\BlockListModel.cs" />
<Compile Include="Models\UpgradeResult.cs" />
<Compile Include="Persistence\Repositories\Implement\UpgradeCheckRepository.cs" />

View File

@@ -154,15 +154,13 @@ namespace Umbraco.Tests.PropertyEditors
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
Assert.AreEqual(0, converted.Count);
json = string.Empty;
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
Assert.AreEqual(0, converted.Count);
}
[Test]
@@ -177,8 +175,7 @@ namespace Umbraco.Tests.PropertyEditors
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
Assert.AreEqual(0, converted.Count);
json = @"{
layout: {},
@@ -186,8 +183,7 @@ data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
Assert.AreEqual(0, converted.Count);
// Even though there is a layout, there is no data, so the conversion will result in zero elements in total
json = @"
@@ -205,8 +201,7 @@ data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
Assert.AreEqual(0, converted.Count);
// Even though there is a layout and data, the data is invalid (missing required keys) so the conversion will result in zero elements in total
json = @"
@@ -228,8 +223,7 @@ data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
Assert.AreEqual(0, converted.Count);
// Everthing is ok except the udi reference in the layout doesn't match the data so it will be empty
json = @"
@@ -252,8 +246,7 @@ data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(1, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
Assert.AreEqual(0, converted.Count);
}
[Test]
@@ -283,14 +276,12 @@ data: []}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(1, converted.ContentData.Count());
var item0 = converted.ContentData.ElementAt(0);
Assert.AreEqual(1, converted.Count);
var item0 = converted[0].Content;
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key);
Assert.AreEqual("Test1", item0.ContentType.Alias);
Assert.AreEqual(1, converted.Layout.Count());
var layout0 = converted.Layout.ElementAt(0);
Assert.IsNull(layout0.Settings);
Assert.AreEqual(Udi.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.ContentUdi);
Assert.IsNull(converted[0].Settings);
Assert.AreEqual(Udi.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi);
}
[Test]
@@ -348,17 +339,15 @@ data: []}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(3, converted.ContentData.Count());
Assert.AreEqual(3, converted.SettingsData.Count());
Assert.AreEqual(2, converted.Layout.Count());
Assert.AreEqual(2, converted.Count);
var item0 = converted.Layout.ElementAt(0);
var item0 = converted[0];
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Content.Key);
Assert.AreEqual("Test1", item0.Content.ContentType.Alias);
Assert.AreEqual(Guid.Parse("1F613E26CE274898908A561437AF5100"), item0.Settings.Key);
Assert.AreEqual("Setting2", item0.Settings.ContentType.Alias);
var item1 = converted.Layout.ElementAt(1);
var item1 = converted[1];
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Content.Key);
Assert.AreEqual("Test2", item1.Content.ContentType.Alias);
Assert.AreEqual(Guid.Parse("63027539B0DB45E7B70459762D4E83DD"), item1.Settings.Key);
@@ -434,11 +423,9 @@ data: []}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(2, converted.ContentData.Count());
Assert.AreEqual(0, converted.SettingsData.Count());
Assert.AreEqual(1, converted.Layout.Count());
Assert.AreEqual(1, converted.Count);
var item0 = converted.Layout.ElementAt(0);
var item0 = converted[0];
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item0.Content.Key);
Assert.AreEqual("Test2", item0.Content.ContentType.Alias);
Assert.IsNull(item0.Settings);

View File

@@ -38,7 +38,7 @@
"lazyload-js": "1.0.0",
"moment": "2.22.2",
"ng-file-upload": "12.2.13",
"nouislider": "14.6.0",
"nouislider": "14.6.1",
"npm": "^6.14.7",
"signalr": "2.4.0",
"spectrum-colorpicker": "1.8.0",

View File

@@ -168,6 +168,7 @@
vm.inviteStep = 2;
}, function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
formHelper.handleError(err);
vm.invitedUserPasswordModel.buttonState = "error";
});

View File

@@ -588,6 +588,7 @@
eventsService.emit("content.unpublished", { content: $scope.content });
overlayService.close();
}, function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
$scope.page.buttonGroupState = 'error';
handleHttpException(err);
});

View File

@@ -188,6 +188,7 @@ Use this directive to construct a header inside the main editor window.
</ul>
@param {string} name The content name.
@param {boolean=} nameRequired Require name to be defined. (True by default)
@param {array=} tabs Array of tabs. See example above.
@param {array=} navigation Array of sub views. See example above.
@param {boolean=} nameLocked Set to <code>true</code> to lock the name.
@@ -358,6 +359,7 @@ Use this directive to construct a header inside the main editor window.
scope: {
name: "=",
nameLocked: "=",
nameRequired: "=?",
menu: "=",
hideActionsMenu: "<?",
icon: "=",

View File

@@ -159,9 +159,6 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
element.removeClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = false;
notifySubView();
//clear form state as at this point we retrieve new data from the server
//and all validation will have cleared at this point
formCtrl.$setPristine();
}));
var confirmed = false;
@@ -238,6 +235,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
}
});
// TODO: I'm unsure why this exists, i believe this may be a hack for something like tinymce which might automatically
// change a form value on load but we need it to be $pristine?
$timeout(function () {
formCtrl.$setPristine();
}, 1000);

View File

@@ -306,7 +306,15 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
formCtrl.$setValidity('valPropertyMsg', false);
startWatch();
// This check is required in order to be able to reset ourselves and is typically for complex editor
// scenarios where the umb-property itself doesn't contain any ng-model controls which means that the
// above serverValidityResetter technique will not work to clear valPropertyMsg errors.
// In order for this to work we rely on the current form controller's $pristine state. This means that anytime
// the form is submitted whether there are validation errors or not the state must be reset... this is automatically
// taken care of with the formHelper.resetForm method that should be used in all cases. $pristine is required because it's
// a value that is cascaded to all form controls based on the hierarchy of child ng-model controls. This allows us to easily
// know if a value has changed. The alternative is what we used to do which was to put a deep $watch on the entire complex value
// which is hugely inefficient.
if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) {
var propertyValidationPath = umbPropCtrl.getValidationPath();
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment);

View File

@@ -54,6 +54,8 @@ function valServerMatch(serverValidationManager) {
function bindCallback(validationKey, matchVal, matchType) {
if (!matchVal) return;
if (Utilities.isString(matchVal)) {
matchVal = [matchVal]; // normalize to an array since the value can also natively be an array
}

View File

@@ -14,7 +14,7 @@
'use strict';
function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService) {
function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper) {
/**
* Simple mapping from property model content entry to editing model,
@@ -236,7 +236,7 @@
/**
* Formats the content apps and ensures unsupported property's have the notsupported view (returns a promise)
* Formats the content apps and ensures unsupported property's have the notsupported view
* @param {any} scaffold
*/
function formatScaffoldData(scaffold) {
@@ -255,7 +255,7 @@
// could be empty in tests
if (!scaffold.apps) {
console.warn("No content apps found in scaffold");
return $q.resolve(scaffold);
return scaffold;
}
// replace view of content app
@@ -271,10 +271,19 @@
scaffold.apps.splice(infoAppIndex, 1);
}
// add the settings app
return localizationService.localize("blockEditor_tabBlockSettings").then(
function (settingsName) {
return scaffold;
}
/**
* Creates a settings content app, we only want to do this if settings is present on the specific block.
* @param {any} contentModel
*/
function appendSettingsContentApp(contentModel, settingsName) {
if (!contentModel.apps) {
return
}
// add the settings app
var settingsTab = {
"name": settingsName,
"alias": "settings",
@@ -282,11 +291,7 @@
"view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html",
"hasError": false
};
scaffold.apps.push(settingsTab);
return scaffold;
}
);
contentModel.apps.push(settingsTab);
}
/**
@@ -309,6 +314,8 @@
this.__watchers = [];
this.__labels = {};
// ensure basic part of data-structure is in place:
this.value = propertyModelValue;
this.value.layout = this.value.layout || {};
@@ -318,13 +325,25 @@
this.propertyEditorAlias = propertyEditorAlias;
this.blockConfigurations = blockConfigurations;
this.blockConfigurations.forEach(blockConfiguration => {
if (blockConfiguration.view != null && blockConfiguration.view !== "") {
blockConfiguration.view = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.view);
}
if (blockConfiguration.stylesheet != null && blockConfiguration.stylesheet !== "") {
blockConfiguration.stylesheet = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.stylesheet);
}
if (blockConfiguration.thumbnail != null && blockConfiguration.thumbnail !== "") {
blockConfiguration.thumbnail = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.thumbnail);
}
});
this.scaffolds = [];
this.isolatedScope = scopeOfExistance.$new(true);
this.isolatedScope.blockObjects = {};
this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this)));
this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this)));
this.__watchers.push(propertyEditorScope.$on("formSubmittingFinalPhase", this.sync.bind(this)));
};
@@ -344,24 +363,25 @@
// update our values
this.value = propertyModelValue;
this.value.layout = this.value.layout || {};
this.value.data = this.value.data || [];
this.value.contentData = this.value.contentData || [];
this.value.settingsData = this.value.settingsData || [];
// re-create the watchers
this.__watchers = [];
this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this)));
this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this)));
this.__watchers.push(propertyEditorScope.$on("formSubmittingFinalPhase", this.sync.bind(this)));
},
/**
* @ngdoc method
* @name getBlockConfiguration
* @methodOf umbraco.services.blockEditorModelObject
* @description Get block configuration object for a given contentTypeKey.
* @param {string} key contentTypeKey to recive the configuration model for.
* @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations.
* @description Get block configuration object for a given contentElementTypeKey.
* @param {string} key contentElementTypeKey to recive the configuration model for.
* @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentElementTypeKey isnt available in the current block configurations.
*/
getBlockConfiguration: function (key) {
return this.blockConfigurations.find(bc => bc.contentTypeKey === key) || null;
return this.blockConfigurations.find(bc => bc.contentElementTypeKey === key) || null;
},
/**
@@ -373,12 +393,24 @@
* @returns {Promise} A Promise object which resolves when all scaffold models are loaded.
*/
load: function () {
var self = this;
var tasks = [];
tasks.push(localizationService.localize("blockEditor_tabBlockSettings").then(
function (settingsName) {
// self.__labels might not exists anymore, this happens if this instance has been destroyed before the load is complete.
if(self.__labels) {
self.__labels.settingsName = settingsName;
}
}
));
var scaffoldKeys = [];
this.blockConfigurations.forEach(blockConfiguration => {
scaffoldKeys.push(blockConfiguration.contentTypeKey);
scaffoldKeys.push(blockConfiguration.contentElementTypeKey);
if (blockConfiguration.settingsElementTypeKey != null) {
scaffoldKeys.push(blockConfiguration.settingsElementTypeKey);
}
@@ -387,19 +419,11 @@
// removing duplicates.
scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index);
var self = this;
scaffoldKeys.forEach(contentTypeKey => {
tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(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);
self.scaffolds.push(formatScaffoldData(scaffold));
}
}));
});
@@ -415,7 +439,7 @@
* @return {Array} array of strings representing alias.
*/
getAvailableAliasesForBlockContent: function () {
return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias);
return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey).contentTypeAlias);
},
/**
@@ -431,7 +455,7 @@
var blocks = [];
this.blockConfigurations.forEach(blockConfiguration => {
var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
var scaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey);
if (scaffold) {
blocks.push({
blockConfigModel: blockConfiguration,
@@ -503,12 +527,12 @@
var contentScaffold;
if (blockConfiguration === null) {
console.error("The block entry of " + contentUdi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor");
console.error("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor");
}
else {
contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey);
if (contentScaffold === null) {
console.error("The block entry of " + contentUdi + " is not begin initialized cause its Element Type was not loaded.");
console.error("The block of " + contentUdi + " is not begin initialized cause its Element Type was not loaded.");
}
}
@@ -519,7 +543,6 @@
unsupported: true
};
contentScaffold = {};
}
var blockObject = {};
@@ -577,6 +600,9 @@
ensureUdiAndKey(blockObject.settings, settingsUdi);
mapToElementModel(blockObject.settings, settingsData);
// add settings content-app
appendSettingsContentApp(blockObject.content, this.__labels.settingsName);
}
}
@@ -587,8 +613,7 @@
if (this.config.settingsElementTypeKey !== null) {
mapElementValues(settings, this.settings);
}
}
};
blockObject.sync = function () {
if (this.content !== null) {
@@ -597,7 +622,7 @@
if (this.config.settingsElementTypeKey !== null) {
mapToPropertyModel(this.settings, this.settingsData);
}
}
};
// first time instant update of label.
blockObject.label = getBlockLabel(blockObject);
@@ -636,7 +661,6 @@
}
return blockObject;
},
/**
@@ -691,18 +715,18 @@
* @name create
* @methodOf umbraco.services.blockEditorModelObject
* @description Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor.
* @param {string} contentTypeKey the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned.
* @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible.
* @param {string} contentElementTypeKey the contentElementTypeKey of the block you wish to create, if contentElementTypeKey is not avaiable in the block configuration then ´null´ will be returned.
* @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentElementTypeKey is unavaiaible.
*/
create: function (contentTypeKey) {
create: function (contentElementTypeKey) {
var blockConfiguration = this.getBlockConfiguration(contentTypeKey);
var blockConfiguration = this.getBlockConfiguration(contentElementTypeKey);
if (blockConfiguration === null) {
return null;
}
var entry = {
contentUdi: createDataEntry(contentTypeKey, this.value.contentData)
contentUdi: createDataEntry(contentElementTypeKey, this.value.contentData)
}
if (blockConfiguration.settingsElementTypeKey != null) {
@@ -723,14 +747,14 @@
elementTypeDataModel = Utilities.copy(elementTypeDataModel);
var contentTypeKey = elementTypeDataModel.contentTypeKey;
var contentElementTypeKey = elementTypeDataModel.contentTypeKey;
var layoutEntry = this.create(contentTypeKey);
var layoutEntry = this.create(contentElementTypeKey);
if (layoutEntry === null) {
return null;
}
var dataModel = getDataByUdi(layoutEntry.udi, this.value.contentData);
var dataModel = getDataByUdi(layoutEntry.contentUdi, this.value.contentData);
if (dataModel === null) {
return null;
}

View File

@@ -174,7 +174,7 @@ function clipboardService(notificationsService, eventsService, localStorageServi
}
);
var entry = {unique:uniqueKey, type:type, alias:alias, data:prepareEntryForStorage(data, firstLevelClearupMethod), label:displayLabel, icon:displayIcon};
var entry = {unique:uniqueKey, type:type, alias:alias, data:prepareEntryForStorage(data, firstLevelClearupMethod), label:displayLabel, icon:displayIcon, date:Date.now()};
storage.entries.push(entry);
if (saveStorage(storage) === true) {
@@ -216,8 +216,7 @@ function clipboardService(notificationsService, eventsService, localStorageServi
}
);
var entry = {unique:uniqueKey, type:type, aliases:aliases, data:copiedDatas, label:displayLabel, icon:displayIcon};
var entry = {unique:uniqueKey, type:type, aliases:aliases, data:copiedDatas, label:displayLabel, icon:displayIcon, date:Date.now()};
storage.entries.push(entry);
if (saveStorage(storage) === true) {

View File

@@ -117,6 +117,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
return $q.resolve(data);
}, function (err) {
formHelper.resetForm({ scope: args.scope, hasErrors: true });
self.handleSaveError({
showNotifications: args.showNotifications,
softRedirect: args.softRedirect,

View File

@@ -46,7 +46,12 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action });
this.focusOnFirstError(currentForm);
args.scope.$broadcast("postFormSubmitting", { scope: args.scope, action: args.action });
// Some property editors need to perform an action after all property editors have reacted to the formSubmitting.
args.scope.$broadcast("formSubmittingFinalPhase", { scope: args.scope, action: args.action });
// Set the form state to submitted
currentForm.$setSubmitted();
//then check if the form is valid
if (!args.skipValidation) {
@@ -105,14 +110,28 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
* @param {object} args An object containing arguments for form submission
*/
resetForm: function (args) {
var currentForm;
if (!args) {
throw "args cannot be null";
}
if (!args.scope) {
throw "args.scope cannot be null";
}
if (!args.formCtrl) {
//try to get the closest form controller
currentForm = angularHelper.getRequiredCurrentForm(args.scope);
}
else {
currentForm = args.formCtrl;
}
args.scope.$broadcast("formSubmitted", { scope: args.scope });
// Set the form state to pristine
currentForm.$setPristine();
currentForm.$setUntouched();
args.scope.$broadcast(args.hasErrors ? "formSubmittedValidationFailed" : "formSubmitted", { scope: args.scope });
},
showNotifications: function (args) {

View File

@@ -31,6 +31,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe
return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart("~/");
},
/**
* @ngdoc method
* @name umbraco.services.umbRequestHelper#dictionaryToQueryString

View File

@@ -7,6 +7,7 @@
// -------------------------
.alert {
position: relative;
padding: 8px 35px 8px 14px;
margin-bottom: @baseLineHeight;
background-color: @warningBackground;
@@ -98,3 +99,29 @@
.alert-block p + p {
margin-top: 5px;
}
// Property error alerts
// -------------------------
.alert.property-error {
display: inline-block;
font-size: 14px;
padding: 6px 16px 6px 12px;
margin-bottom: 6px;
&::after {
content:'';
position: absolute;
bottom:-6px;
left: 6px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid;
}
&.alert-error::after {
border-top-color: @errorBackground;
}
}

View File

@@ -50,6 +50,19 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-name: umb-variant-switcher__toggle--badge-bounce;
animation-timing-function: ease;
@keyframes umb-variant-switcher__toggle--badge-bounce {
0% { transform: translateY(0); }
20% { transform: translateY(-6px); }
40% { transform: translateY(0); }
55% { transform: translateY(-3px); }
70% { transform: translateY(0); }
100% { transform: translateY(0); }
}
}
}
}
@@ -241,6 +254,19 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-name: umb-variant-switcher__name--badge-bounce;
animation-timing-function: ease;
@keyframes umb-variant-switcher__name--badge-bounce {
0% { transform: translateY(0); }
20% { transform: translateY(-6px); }
40% { transform: translateY(0); }
55% { transform: translateY(-3px); }
70% { transform: translateY(0); }
100% { transform: translateY(0); }
}
}
}
}

View File

@@ -53,6 +53,39 @@
height: 4px;
}
}
// Validation
.show-validation &.-has-error {
color: @red;
&:hover {
color: @red !important;
}
&::before {
background-color: @red;
}
&:not(.is-active) {
.badge {
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-name: umb-sub-views-nav-item--badge-bounce;
animation-timing-function: ease;
@keyframes umb-sub-views-nav-item--badge-bounce {
0% { transform: translateY(0); }
20% { transform: translateY(-6px); }
40% { transform: translateY(0); }
55% { transform: translateY(-3px); }
70% { transform: translateY(0); }
100% { transform: translateY(0); }
}
}
.badge.--error-badge {
display: block;
}
}
}
}
&__action:active,
@@ -101,6 +134,10 @@
height: 12px;
min-width: 12px;
}
&.--error-badge {
display: none;
font-weight: 900;
}
}
&-text {
@@ -182,13 +219,3 @@
}
}
}
// Validation
.show-validation .umb-sub-views-nav-item__action.-has-error,
.show-validation .umb-sub-views-nav-item > a.-has-error {
color: @red;
&::before {
background-color: @red;
}
}

View File

@@ -84,10 +84,8 @@
.umb-nested-content__heading {
line-height: 20px;
position: relative;
margin-top:1px;
padding: 15px 5px;
color:@ui-option-type;
border-radius: 3px 3px 0 0;
&:hover {
color:@ui-option-type-hover;

View File

@@ -272,6 +272,7 @@ label:not([for]) {
/* CONTROL VALIDATION */
.umb-control-required {
color: @controlRequiredColor;
font-weight: 900;
}
.controls-row {

View File

@@ -138,7 +138,9 @@
// additional targetting of the ng-invalid class.
.formFieldState(@textColor: @gray-4, @borderColor: @gray-7, @backgroundColor: @gray-10) {
// Set the text color
.control-label,
> .control-label,
> .umb-el-wrap > .control-label,
> .umb-el-wrap > .control-header > .control-label,
.help-block,
.help-inline {
color: @textColor;

View File

@@ -481,7 +481,7 @@
@formErrorText: @errorBackground;
@formErrorBackground: lighten(@errorBackground, 55%);
@formErrorBorder: darken(spin(@errorBackground, -10), 3%);
@formErrorBorder: @red;
@formSuccessText: @successBackground;
@formSuccessBackground: lighten(@successBackground, 48%);

View File

@@ -1,6 +1,6 @@
angular.module("umbraco")
.controller("Umbraco.Editors.BlockEditorController",
function ($scope, localizationService, formHelper) {
function ($scope, localizationService, formHelper, overlayService) {
var vm = this;
vm.model = $scope.model;
@@ -23,17 +23,14 @@ angular.module("umbraco")
if (contentApp) {
if (vm.model.hideContent) {
apps.splice(apps.indexOf(contentApp), 1);
} else if (vm.model.openSettings !== true) {
contentApp.active = true;
}
contentApp.active = (vm.model.openSettings !== true);
}
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;
}
settingsApp.active = (vm.model.openSettings === true);
}
}
@@ -42,6 +39,7 @@ angular.module("umbraco")
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,
@@ -49,6 +47,9 @@ angular.module("umbraco")
keepServerValidation: true
})) {
vm.model.submit(vm.model);
vm.saveButtonState = "success";
} else {
vm.saveButtonState = "error";
}
}
}
@@ -67,6 +68,29 @@ angular.module("umbraco")
// * 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
if (vm.blockForm.$dirty === true) {
localizationService.localizeMany(["prompt_discardChanges", "blockEditor_blockHasChanges"]).then(function (localizations) {
const confirm = {
title: localizations[0],
view: "default",
content: localizations[1],
submitButtonLabelKey: "general_discard",
submitButtonStyle: "danger",
closeButtonLabelKey: "general_cancel",
submit: function () {
overlayService.close();
vm.model.close(vm.model);
},
close: function () {
overlayService.close();
}
};
overlayService.open(confirm);
});
return;
}
// TODO: check if content/settings has changed and ask user if they are sure.
vm.model.close(vm.model);
}

View File

@@ -6,6 +6,7 @@
<umb-editor-header
name="vm.model.title"
name-required="false"
name-locked="true"
navigation="vm.tabs"
hide-alias="true"
@@ -36,6 +37,7 @@
<umb-button
action="vm.close()"
shortcut="esc"
button-style="link"
label="{{vm.closeLabel}}"
type="button">
@@ -44,7 +46,7 @@
<umb-button
action="vm.submitAndClose()"
button-style="primary"
state="submitButtonState"
state="vm.saveButtonState"
label="{{vm.submitLabel}}"
type="button">
</umb-button>

View File

@@ -69,6 +69,7 @@
<umb-button
action="vm.close()"
shortcut="esc"
button-style="link"
label-key="general_cancel"
type="button">

View File

@@ -22,7 +22,7 @@
};
if ($scope.model.modify) {
angular.extend($scope.model.embed, $scope.model.modify);
Utilities.extend($scope.model.embed, $scope.model.modify);
showPreview();
}
@@ -122,7 +122,6 @@
if ($scope.model.embed.url !== "") {
showPreview();
}
}
function toggleConstrain() {
@@ -142,7 +141,6 @@
}
onInit();
}
angular.module("umbraco").controller("Umbraco.Editors.EmbedController", EmbedController);

View File

@@ -16,7 +16,7 @@
<umb-box-content>
<umb-control-group label="@general_url">
<input id="url" class="umb-property-editor input-block-level" type="text" style="margin-bottom: 10px;" ng-model="model.embed.url" ng-keyup="$event.keyCode == 13 ? vm.showPreview() : null" focus-when="{{true}}" required />
<input type="text" id="url" class="umb-property-editor input-block-level" ng-model="model.embed.url" ng-keyup="$event.keyCode == 13 ? vm.showPreview() : null" focus-when="{{true}}" required />
<umb-button
type="button"
action="vm.showPreview()"

View File

@@ -160,7 +160,7 @@ angular.module("umbraco")
}, 2000);
}, function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
formHelper.handleError(err);
$scope.changePasswordButtonState = "error";

View File

@@ -9,7 +9,7 @@
/* Grid Setup */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: minmax(200px, auto);
grid-auto-rows: minmax(160px, auto);
grid-gap: 20px;
}

View File

@@ -1,6 +1,6 @@
<div class="__showcase" ng-style="{'background-color':vm.blockConfigModel.backgroundColor, 'background-image': vm.blockConfigModel.thumbnail ? 'url('+vm.blockConfigModel.thumbnail+'?upscale=false&width=400)' : 'transparent'}">
<i ng-if="vm.blockConfigModel.thumbnail == null && vm.elementTypeModel.icon" class="__icon {{ vm.elementTypeModel.icon }}" ng-style="{'color':vm.blockConfigModel.iconColor}" aria-hidden="true"></i>
<div class="__showcase" ng-style="{'background-color':vm.blockConfigModel.backgroundColor, 'background-image': vm.styleBackgroundImage}">
<i ng-if="vm.blockConfigModel.thumbnail == null && vm.elementTypeModel.icon" class="__icon {{ vm.elementTypeModel.icon }}" ng-attr-style="{{'color:'+vm.blockConfigModel.iconColor+' !important'}}" aria-hidden="true"></i>
</div>
<div class="__info">
<div class="__name" ng-bind="vm.elementTypeModel.name"></div>

View File

@@ -75,14 +75,14 @@ umb-block-card {
.__info {
width: 100%;
background-color: #fff;
padding-bottom: 6px;
padding-bottom: 11px;// 10 + 1 to compentiate for the -1 substraction in margin-bottom.
.__name {
font-weight: bold;
font-size: 14px;
color: @ui-action-type;
margin-left: 16px;
margin-top: 8px;
margin-top: 10px;
margin-bottom: -1px;
}
.__subname {

View File

@@ -14,9 +14,38 @@
}
});
function BlockCardController() {
function BlockCardController($scope, umbRequestHelper) {
var vm = this;
vm.styleBackgroundImage = "none";
var unwatch = $scope.$watch("vm.blockConfigModel.thumbnail", (newValue, oldValue) => {
if(newValue !== oldValue) {
vm.updateThumbnail();
}
});
vm.$onInit = function () {
vm.updateThumbnail();
}
vm.$onDestroy = function () {
unwatch();
}
vm.updateThumbnail = function () {
if (vm.blockConfigModel.thumbnail == null || vm.blockConfigModel.thumbnail === "") {
vm.styleBackgroundImage = "none";
return;
}
var path = umbRequestHelper.convertVirtualToAbsolutePath(vm.blockConfigModel.thumbnail);
if (path.toLowerCase().endsWith(".svg") === false) {
path += "?upscale=false&width=400";
}
vm.styleBackgroundImage = 'url(\''+path+'\')';
}
}

View File

@@ -44,7 +44,6 @@
<umb-box data-element="node-info-history">
<umb-box-header title="{{historyLabel}}">
<umb-button
ng-hide="node.trashed"
type="button"

View File

@@ -48,8 +48,8 @@
umb-auto-focus
focus-on-filled="true"
val-server-field="Name"
required
aria-required="true"
ng-required="nameRequired != null ? nameRequired : true"
aria-required="{{nameRequired != null ? nameRequired : true}}"
aria-invalid="{{contentForm.headerNameForm.headerName.$invalid ? true : false}}"
autocomplete="off"
maxlength="255"/>

View File

@@ -9,6 +9,7 @@
<i class="icon {{ vm.item.icon }}" aria-hidden="true"></i>
<span class="umb-sub-views-nav-item-text">{{ vm.item.name }}</span>
<div ng-show="vm.item.badge" class="badge -type-{{vm.item.badge.type}}">{{vm.item.badge.count}}</div>
<div ng-show="!vm.item.badge" class="badge -type-alert --error-badge">!</div>
</button>
<ul class="dropdown-menu umb-sub-views-nav-item__anchor_dropdown" ng-if="vm.item.anchors && vm.item.anchors.length > 1">

View File

@@ -34,7 +34,7 @@
navigationService.hideMenu();
},
function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
contentEditingHelper.handleSaveError({
err: err
});

View File

@@ -17,7 +17,7 @@
var hasSomethingToPublish = false;
vm.variants.forEach(variant => {
// if variant is mandatory and not already published:
// if varaint is mandatory and not already published:
if (variant.publish === false && notPublishedMandatoryFilter(variant)) {
return false;
}
@@ -38,8 +38,9 @@
function hasAnyDataFilter(variant) {
if (variant.name == null || variant.name.length === 0) {
return false;
// if we have a name, then we have data.
if (variant.name != null && variant.name.length > 0) {
return true;
}
if(variant.isDirty === true) {
@@ -82,7 +83,7 @@
}
function notPublishedMandatoryFilter(variant) {
return variant.state !== "Published" && isMandatoryFilter(variant);
return variant.state !== "Published" && variant.state !== "PublishedPendingChanges" && variant.isMandatory === true;
}
/**
@@ -120,12 +121,15 @@
// reset to not be published
variant.publish = variant.save = false;
variant.isMandatory = isMandatoryFilter(variant);
// if this is a new node and we have data on this variant.
if (vm.isNew === true && hasAnyDataFilter(variant)) {
variant.save = true;
}
});
vm.availableVariants = vm.variants.filter(publishableVariantFilter);

View File

@@ -21,19 +21,16 @@
on-change="vm.changeSelection(variant)"
server-validation-field="{{variant.htmlId}}">
<span class="umb-variant-selector-entry__title" ng-if="!(variant.segment && variant.language)">
<span ng-bind="variant.displayName"></span>
<strong ng-if="variant.isMandatory" class="umb-control-required">*</strong>
</span>
<span class="umb-variant-selector-entry__title" ng-if="variant.segment && variant.language">
<span ng-bind="variant.segment"></span>
<span class="__secondarytitle"> — {{variant.language.name}}</span>
<strong ng-if="variant.isMandatory" class="umb-control-required">*</strong>
<span class="umb-variant-selector-entry__title">
<span ng-bind="variant.displayName" ng-if="!(variant.segment && variant.language)"></span>
<span ng-bind="variant.segment" ng-if="variant.segment && variant.language"></span>
<span class="__secondarytitle" ng-if="variant.segment && variant.language"> — {{variant.language.name}}</span>
<strong ng-if="variant.isMandatory && variant.state !== 'Published' && variant.state !== 'PublishedPendingChanges'" class="umb-control-required">*</strong>
</span>
<span class="umb-variant-selector-entry__description" ng-if="!publishVariantSelectorForm.publishVariantSelector.$invalid && !(variant.notifications && variant.notifications.length > 0)">
<umb-variant-state variant="variant"></umb-variant-state>
<span ng-if="variant.isMandatory"> - </span>
<span ng-if="variant.isMandatory" ng-class="{'text-error': (variant.publish === false) }"><localize key="languages_mandatoryLanguage"></localize></span>
<span ng-if="variant.isMandatory" ng-class="{'text-error': (variant.state !== 'Published' && variant.state !== 'PublishedPendingChanges' && variant.publish === false) }"><localize key="languages_mandatoryLanguage"></localize></span>
</span>
<span class="umb-variant-selector-entry__description" ng-messages="publishVariantSelectorForm.publishVariantSelector.$error" show-validation-on-submit>
<span class="text-error" ng-message="valServerField">{{publishVariantSelectorForm.publishVariantSelector.errorMsg}}</span>

View File

@@ -21,6 +21,11 @@
$scope.model.title = value;
});
}
if (!vm.labels.includeUnpublished) {
localizationService.localize("content_includeUnpublished").then(function (value) {
vm.labels.includeUnpublished = value;
});
}
if (!vm.labels.includeUnpublished) {
localizationService.localize("content_includeUnpublished").then(value => {
vm.labels.includeUnpublished = value;

View File

@@ -47,7 +47,6 @@
<umb-variant-notification-list notifications="variant.notifications"></umb-variant-notification-list>
</div>
</ng-form>
</div>
<br/>

View File

@@ -181,12 +181,9 @@
</div>
</div>
</ng-form>
</div>
<br />
</div>
</div>
</div>

View File

@@ -47,12 +47,9 @@
<umb-variant-notification-list notifications="variant.notifications"></umb-variant-notification-list>
</div>
</ng-form>
</div>
<br />
</div>
</div>
</div>

View File

@@ -28,10 +28,11 @@ function DataTypeCreateController($scope, $location, navigationService, dataType
var currPath = node.path ? node.path : "-1";
navigationService.syncTree({ tree: "datatypes", path: currPath + "," + folderId, forceReload: true, activate: true });
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderFor });
}, function(err) {
formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderFor, hasErrors: true });
// TODO: Handle errors
});
};

View File

@@ -128,6 +128,7 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic
}, function(err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
//NOTE: in the case of data type values we are setting the orig/new props
// to be the same thing since that only really matters for content/media.
contentEditingHelper.handleSaveError({

View File

@@ -27,13 +27,14 @@ function DictionaryCreateController($scope, $location, dictionaryResource, navig
navigationService.syncTree({ tree: "dictionary", path: currPath + "," + data, forceReload: true, activate: true });
// reset form state
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: this.createDictionaryForm });
// navigate to edit view
var currentSection = appState.getSectionState("currentSection");
$location.path("/" + currentSection + "/dictionary/edit/" + data);
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: this.createDictionaryForm, hasErrors: true });
if (err.data && err.data.message) {
notificationsService.error(err.data.message);
navigationService.hideMenu();

View File

@@ -86,7 +86,7 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes
dictionaryResource.save(vm.content, vm.nameDirty)
.then(function (data) {
formHelper.resetForm({ scope: $scope, notifications: data.notifications });
formHelper.resetForm({ scope: $scope });
bindDictionary(data);
@@ -94,6 +94,8 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes
},
function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
contentEditingHelper.handleSaveError({
err: err
});

View File

@@ -47,12 +47,13 @@ function DocumentTypesCreateController($scope, $location, navigationService, con
activate: true
});
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm });
var section = appState.getSectionState("currentSection");
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true });
$scope.error = err;
});
@@ -83,9 +84,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con
$location.search('create', null);
$location.search('notemplate', null);
formHelper.resetForm({
scope: $scope
});
formHelper.resetForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm });
var section = appState.getSectionState("currentSection");
@@ -94,6 +93,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm, hasErrors: true });
$scope.error = err;
//show any notifications

View File

@@ -161,7 +161,7 @@
}, function (err) {
vm.page.saveButtonState = "error";
formHelper.resetForm({ scope: $scope, hasErrors: true });
formHelper.handleError(err);
});

View File

@@ -19,7 +19,7 @@
<div class="umb-logviewer__main-content">
<div ng-show="!vm.canLoadLogs">
<umb-box>
<umb-box-header title="Unable to view logs"/>
<umb-box-header title="Unable to view logs"></umb-box-header>
<umb-box-content>
<p>Today's log file is too large to be viewed and would cause performance problems.</p>
<p>If you need to view the log files, try opening them manually</p>

View File

@@ -25,7 +25,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ
navigationService.syncTree({ tree: "macros", path: currPath + "," + data, forceReload: true, activate: true });
// reset form state
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: this.createMacroForm });
// navigate to edit view
var currentSection = appState.getSectionState("currentSection");
@@ -33,6 +33,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: this.createMacroForm, hasErrors: true });
if (err.data && err.data.message) {
notificationsService.error(err.data.message);
navigationService.hideMenu();

View File

@@ -33,10 +33,11 @@ function MacrosEditController($scope, $q, $routeParams, macroResource, editorSta
vm.page.saveButtonState = "busy";
macroResource.saveMacro(vm.macro).then(function (data) {
formHelper.resetForm({ scope: $scope, notifications: data.notifications });
formHelper.resetForm({ scope: $scope });
bindMacro(data);
vm.page.saveButtonState = "success";
}, function (error) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
contentEditingHelper.handleSaveError({
err: error
});

View File

@@ -204,6 +204,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
}, function(err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
contentEditingHelper.handleSaveError({
err: err,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data)

View File

@@ -30,11 +30,12 @@ function MediaTypesCreateController($scope, $location, navigationService, mediaT
var currPath = node.path ? node.path : "-1";
navigationService.syncTree({ tree: "mediatypes", path: currPath + "," + folderId, forceReload: true, activate: true });
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm });
var section = appState.getSectionState("currentSection");
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true });
$scope.error = err;
});
};

View File

@@ -228,7 +228,7 @@ function MemberEditController($scope, $routeParams, $location, $http, $q, appSta
}
}, function(err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
contentEditingHelper.handleSaveError({
err: err,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data)

View File

@@ -88,6 +88,7 @@ function MemberGroupsEditController($scope, $routeParams, appState, navigationSe
}, function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
contentEditingHelper.handleSaveError({
err: err
});

View File

@@ -31,10 +31,10 @@ function MemberTypesCreateController($scope, $location, navigationService, membe
var currPath = node.path ? node.path : "-1";
navigationService.syncTree({ tree: "membertypes", path: currPath + "," + folderId, forceReload: true, activate: true });
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm });
}, function(err) {
formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true });
// TODO: Handle errors
});
};

View File

@@ -194,7 +194,7 @@
vm.package = updatedPackage;
vm.buttonState = "success";
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: editPackageForm });
if (create) {
//if we are creating, then redirect to the correct url and reload
@@ -204,6 +204,7 @@
}
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: editPackageForm, hasErrors: true });
formHelper.handleError(err);
vm.buttonState = "error";
});

View File

@@ -46,12 +46,13 @@
activate: true
});
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: form });
var section = appState.getSectionState("currentSection");
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true });
vm.createFolderError = err;
});

View File

@@ -56,12 +56,13 @@
activate: true
});
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: form });
var section = appState.getSectionState("currentSection");
}, function(err) {
formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true });
vm.createFolderError = err;
});
}

View File

@@ -1 +1 @@
<umb-block-list-property-editor model="model"/>
<umb-block-list-property-editor model="model"></umb-block-list-property-editor>

View File

@@ -12,7 +12,7 @@
// boardcast the formSubmitting event to trigger syncronization or none-live property-editors
$scope.$broadcast("formSubmitting", { scope: $scope });
// Some property editors need to performe an action after all property editors have reacted to the formSubmitting.
$scope.$broadcast("postFormSubmitting", { scope: $scope });
$scope.$broadcast("formSubmittingFinalPhase", { scope: $scope });
block.active = false;
} else {

View File

@@ -5,8 +5,8 @@
ng-click="vm.openBlock(block)"
ng-focus="block.focus">
<span class="caret"></span>
<i class="icon {{block.content.icon}}"></i>
<span>{{block.label}}</span>
<i class="icon {{block.content.icon}}" aria-hidden="true"></i>
<span class="name">{{block.label}}</span>
</button>
<div class="blockelement-inlineblock-editor__inner" ng-class="{'--singleGroup':block.content.variants[0].tabs.length === 1}" ng-if="block.active === true">
<umb-element-editor-content model="block.content"></umb-element-editor-content>

View File

@@ -22,6 +22,7 @@
background-color: white;
.caret {
vertical-align: middle;
transform: rotate(-90deg);
transition: transform 80ms ease-out;
}
@@ -32,7 +33,8 @@
vertical-align: middle;
}
span {
span.name {
position: relative;
display: inline-block;
vertical-align: middle;
}
@@ -54,10 +56,55 @@
}
}
&.--error {
border-color: @formErrorBorder !important;
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & {
> button {
color: @formErrorText;
span.caret {
border-top-color: @formErrorText;
}
}
}
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & {
> button {
span.name {
&::after {
content: "!";
text-align: center;
position: absolute;
top: -6px;
right: -15px;
min-width: 10px;
color: @white;
background-color: @ui-active-type;
border: 2px solid @white;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
padding: 2px;
line-height: 10px;
background-color: @formErrorText;
font-weight: 900;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-name: blockelement-inlineblock-editor--badge-bounce;
animation-timing-function: ease;
@keyframes blockelement-inlineblock-editor--badge-bounce {
0% { transform: translateY(0); }
20% { transform: translateY(-4px); }
40% { transform: translateY(0); }
55% { transform: translateY(-2px); }
70% { transform: translateY(0); }
100% { transform: translateY(0); }
}
}
}
}
}
}
.blockelement-inlineblock-editor__inner {
border-top: 1px solid @gray-8;
background-color: @gray-12;

View File

@@ -3,6 +3,6 @@
ng-focus="block.focus"
ng-class="{ '--active': block.active, '--error': parentForm.$invalid && valFormManager.isShowingValidation() }"
val-server-property-class="">
<i class="icon {{block.content.icon}}"></i>
<i class="icon {{block.content.icon}}" aria-hidden="true"></i>
<span>{{block.label}}</span>
</button>

View File

@@ -24,6 +24,7 @@
}
span {
position: relative;
display: inline-block;
vertical-align: middle;
}
@@ -39,7 +40,42 @@
background-color: @ui-active;
}
&.--error {
border-color: @formErrorBorder !important;
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & {
color: @formErrorText;
}
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & {
span {
&::after {
content: "!";
text-align: center;
position: absolute;
top: -6px;
right: -15px;
min-width: 10px;
color: @white;
background-color: @ui-active-type;
border: 2px solid @white;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
padding: 2px;
line-height: 10px;
background-color: @formErrorText;
font-weight: 900;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-name: blockelement-inlineblock-editor--badge-bounce;
animation-timing-function: ease;
@keyframes blockelement-inlineblock-editor--badge-bounce {
0% { transform: translateY(0); }
20% { transform: translateY(-4px); }
40% { transform: translateY(0); }
55% { transform: translateY(-2px); }
70% { transform: translateY(0); }
100% { transform: translateY(0); }
}
}
}
}
}

View File

@@ -1,12 +1,12 @@
<div class="blockelement-unsupportedblock-editor blockelement__draggable-element" ng-focus="block.focus">
<div class="__header">
<i class="icon icon-alert"></i>
<i class="icon icon-alert" aria-hidden="true"></i>
<span>{{block.label}}</span>
</div>
<div class="__body">
This Block is no longer supported in this context.<br/>
You might want to remove this block, or contact your developer to take actions for making this block available again.<br/><br/>
<a href="http://our.umbraco.com" target="_blank">Learn about this circumstance</a>
<a href="http://our.umbraco.com" target="_blank" rel="noreferrer">Learn about this circumstance</a>
<h5>Block data:</h5>
<pre ng-bind="block.data | json : 4"></pre>
</div>

View File

@@ -51,7 +51,7 @@
vm.requestRemoveBlockByIndex = function (index) {
localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) {
var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentTypeKey);
var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey);
overlayService.confirmDelete({
title: data[0],
content: localizationService.tokenReplace(data[1], [contentElementType.name]),
@@ -82,7 +82,7 @@
vm.getAvailableElementTypes = function () {
return vm.elementTypes.filter(function (type) {
return !$scope.model.value.find(function (entry) {
return type.key === entry.contentTypeKey;
return type.key === entry.contentElementTypeKey;
});
});
};
@@ -99,7 +99,7 @@
//we have to add the 'alias' property to the objects, to meet the data requirements of itempicker.
var selectedItems = Utilities.copy($scope.model.value).forEach((obj) => {
obj.alias = vm.getElementTypeByKey(obj.contentTypeKey).alias;
obj.alias = vm.getElementTypeByKey(obj.contentElementTypeKey).alias;
return obj;
});
@@ -158,7 +158,7 @@
vm.addBlockFromElementTypeKey = function(key) {
var entry = {
"contentTypeKey": key,
"contentElementTypeKey": key,
"settingsElementTypeKey": null,
"labelTemplate": "",
"view": null,
@@ -178,7 +178,7 @@
vm.openBlockOverlay = function (block) {
localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [vm.getElementTypeByKey(block.contentTypeKey).name]).then(function (data) {
localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [vm.getElementTypeByKey(block.contentElementTypeKey).name]).then(function (data) {
var clonedBlockData = Utilities.copy(block);
vm.openBlock = block;

View File

@@ -11,7 +11,7 @@
<umb-block-card
block-config-model="block"
element-type-model="vm.getElementTypeByKey(block.contentTypeKey)"
element-type-model="vm.getElementTypeByKey(block.contentElementTypeKey)"
ng-repeat="block in model.value"
ng-class="{'--isOpen':vm.openBlock === block}"
ng-click="vm.openBlockOverlay(block)">

View File

@@ -4,7 +4,7 @@
position: relative;
display: inline-flex;
width: 100%;
height: auto;
height: 100%;
margin-right: 20px;
margin-bottom: 20px;

View File

@@ -23,7 +23,7 @@
return elementTypeResource.getAll().then(function(elementTypes) {
vm.elementTypes = elementTypes;
vm.contentPreview = vm.getElementTypeByKey(vm.block.contentTypeKey);
vm.contentPreview = vm.getElementTypeByKey(vm.block.contentElementTypeKey);
vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey);
});
}
@@ -46,7 +46,7 @@
}
};
editorService.documentTypeEditor(editor);
}
};
vm.createElementTypeAndCallback = function(callback) {
const editor = {
@@ -62,7 +62,7 @@
}
};
editorService.documentTypeEditor(editor);
}
};
vm.addSettingsForBlock = function($event, block) {
@@ -95,11 +95,12 @@
};
overlayService.open(elemTypeSelectorOverlay);
});
};
vm.applySettingsToBlock = function(block, key) {
block.settingsElementTypeKey = key;
vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey);
};
vm.requestRemoveSettingsForBlock = function(block) {
@@ -120,11 +121,11 @@
});
});
};
vm.removeSettingsForBlock = function(block) {
block.settingsElementTypeKey = null;
};
function updateUsedElementTypes(event, args) {
var key = args.documentType.key;
for (var i = 0; i<vm.elementTypes.length; i++) {
@@ -140,13 +141,12 @@
vm.settingsPreview = args.documentType;
$scope.$evalAsync();
}
}
unsubscribe.push(eventsService.on("editors.documentType.saved", updateUsedElementTypes));
vm.addViewForBlock = function(block) {
localizationService.localize("blockEditor_headlineSelectView").then(function(localizedTitle) {
localizationService.localize("blockEditor_headlineAddCustomView").then(function (localizedTitle) {
const filePicker = {
title: localizedTitle,
@@ -159,7 +159,7 @@
},
select: function (node) {
const filepath = decodeURIComponent(node.id.replace(/\+/g, " "));
block.view = filepath;
block.view = "~/" + filepath;
editorService.close();
},
close: function () {
@@ -169,7 +169,8 @@
editorService.treePicker(filePicker);
});
}
};
vm.requestRemoveViewForBlock = function(block) {
localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) {
overlayService.confirmRemove({
@@ -185,12 +186,11 @@
});
});
};
vm.removeViewForBlock = function(block) {
block.view = null;
};
vm.addStylesheetForBlock = function(block) {
localizationService.localize("blockEditor_headlineAddCustomStylesheet").then(function (localizedTitle) {
@@ -205,7 +205,7 @@
},
select: function (node) {
const filepath = decodeURIComponent(node.id.replace(/\+/g, " "));
block.stylesheet = filepath;
block.stylesheet = "~/" + filepath;
editorService.close();
},
close: function () {
@@ -215,7 +215,8 @@
editorService.treePicker(filePicker);
});
}
};
vm.requestRemoveStylesheetForBlock = function(block) {
localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) {
overlayService.confirmRemove({
@@ -231,12 +232,11 @@
});
});
};
vm.removeStylesheetForBlock = function(block) {
block.stylesheet = null;
};
vm.addThumbnailForBlock = function(block) {
localizationService.localize("blockEditor_headlineAddThumbnail").then(function (localizedTitle) {
@@ -250,8 +250,10 @@
filter: function (i) {
return !(i.name.indexOf(".jpg") !== -1 || i.name.indexOf(".jpeg") !== -1 || i.name.indexOf(".png") !== -1 || i.name.indexOf(".svg") !== -1 || i.name.indexOf(".webp") !== -1 || i.name.indexOf(".gif") !== -1);
},
filterCssClass: "not-allowed",
select: function (file) {
block.thumbnail = file.name;
const id = decodeURIComponent(file.id.replace(/\+/g, " "));
block.thumbnail = "~/" + id;
editorService.close();
},
close: function () {
@@ -261,25 +263,23 @@
editorService.treePicker(thumbnailPicker);
});
}
};
vm.removeThumbnailForBlock = function(entry) {
entry.thumbnail = null;
};
vm.submit = function() {
if ($scope.model && $scope.model.submit) {
$scope.model.submit($scope.model);
}
}
};
vm.close = function() {
if ($scope.model && $scope.model.close) {
$scope.model.close($scope.model);
}
}
};
$scope.$on('$destroy', function() {
unsubscribe.forEach(u => { u(); });

View File

@@ -106,10 +106,10 @@
<div class="umb-el-wrap">
<label class="control-label"><localize key="blockEditor_labelContentElementType">Content ElementType</localize></label>
<div class="controls">
<div class="__settings-input --hasValue" ng-if="vm.block.contentTypeKey !== null" >
<div class="__settings-input --hasValue" ng-if="vm.block.contentElementTypeKey !== null" >
<umb-node-preview icon="vm.contentPreview.icon" name="vm.contentPreview.name" alias="vm.contentPreview.alias"></umb-node-preview>
<div class="__control-actions">
<button type="button" class="btn-reset __control-actions-btn --open umb-outline" ng-click="vm.openElementType(vm.block.contentTypeKey)">
<button type="button" class="btn-reset __control-actions-btn --open umb-outline" ng-click="vm.openElementType(vm.block.contentElementTypeKey)">
<i class="icon icon-edit"></i>
</button>
</div>
@@ -227,6 +227,7 @@
<umb-button
action="vm.close()"
shortcut="esc"
button-style="link"
label-key="general_close"
type="button">

View File

@@ -30,7 +30,7 @@
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>
<localize key="grid_addElement">Add content</localize>
</button>
<input type="hidden" name="minCount" ng-model="vm.layout" val-server="minCount" />

View File

@@ -39,6 +39,9 @@
}
}
}
ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-block-list__block--actions {
opacity: 1;
}
.umb-block-list__block--actions {
position: absolute;
z-index:999999999;// We always want to be on top of custom view, but we need to make sure we still are behind relevant Umbraco CMS UI. ToDo: Needs further testing.
@@ -58,15 +61,92 @@
&:hover {
color: @ui-action-discreet-type-hover;
}
> .__error-badge {
position: absolute;
top: -2px;
right: -2px;
min-width: 8px;
color: @white;
background-color: @ui-active-type;
border: 2px solid @white;
border-radius: 50%;
font-size: 8px;
font-weight: bold;
padding: 2px;
line-height: 8px;
background-color: @red;
display: none;
font-weight: 900;
}
&.--error > .__error-badge {
display: block;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-name: umb-block-list__action--badge-bounce;
animation-timing-function: ease;
@keyframes umb-block-list__action--badge-bounce {
0% { transform: translateY(0); }
20% { transform: translateY(-4px); }
40% { transform: translateY(0); }
55% { transform: translateY(-2px); }
70% { transform: translateY(0); }
100% { transform: translateY(0); }
}
}
}
}
.umb-block-list__block--content {
> div {
position: relative;
width: 100%;
min-height: @umb-block-list__item_minimum_height;
background-color: @white;
border-radius: @baseBorderRadius;
box-sizing: border-box;
}
&.--show-validation {
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > & > div {
border: 2px solid @formErrorText;
border-radius: @baseBorderRadius;
&::after {
content: "!";
position: absolute;
top: -12px;
right: -12px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 13px;
text-align: center;
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
border: 2px solid @white;
font-weight: 900;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-name: umb-block-list__block--content--badge-bounce;
animation-timing-function: ease;
@keyframes umb-block-list__block--content--badge-bounce {
0% { transform: translateY(0); }
20% { transform: translateY(-6px); }
40% { transform: translateY(0); }
55% { transform: translateY(-3px); }
70% { transform: translateY(0); }
100% { transform: translateY(0); }
}
}
}
}
}
.blockelement__draggable-element {
@@ -98,20 +178,19 @@
left: 0;
height: 2px;
animation: umb-block-list__block--create-button_before 400ms ease-in-out alternate infinite;
@keyframes umb-block-list__block--create-button_before {
0% { opacity: 1; }
100% { opacity: 0.5; }
}
}
> .__plus {
position: absolute;
pointer-events: none; // lets stop avoiding the mouse values in JS move event.
margin-left: -18px - 10px;
margin-top: -18px;
margin-bottom: -18px;
width: 28px;
height: 25px;
padding-bottom: 3px;
width: 24px;
height: 24px;
padding: 0;
border-radius: 3em;
border: 2px solid @blueMid;
display: flex;
@@ -122,25 +201,29 @@
font-weight: 800;
background-color: rgba(255, 255, 255, .96);
box-shadow: 0 0 0 2px rgba(255, 255, 255, .96);
transform: scale(0);
transform: scale(0) translate(-80%, -50%);
transition: transform 240ms ease-in;
animation: umb-block-list__block--create-button_after 800ms ease-in-out infinite;
@keyframes umb-block-list__block--create-button_after {
0% { color: rgba(@blueMid, 0.8); }
50% { color: rgba(@blueMid, 1); }
100% { color: rgba(@blueMid, 0.8); }
}
}
&:focus {
> .__plus {
border: 2px solid @ui-outline;
}
}
&:hover, &:focus {
opacity: 1;
transition-duration: 120ms;
> .__plus {
transform: scale(1);
transform: scale(1) translate(-80%, -50%);
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
}

View File

@@ -3,6 +3,7 @@
<umb-block-list-block stylesheet="{{::vm.layout.$block.config.stylesheet}}"
class="umb-block-list__block--content"
ng-class="{'blockelement__draggable-element': vm.layout.$block.config.stylesheet, '--show-validation': vm.layout.$block.showValidation === true}"
view="{{vm.layout.$block.view}}"
api="vm.blockEditorApi"
block="vm.layout.$block"
@@ -19,6 +20,7 @@
<span class="sr-only">
<localize key="general_settings">Settings</localize>
</span>
<div class="__error-badge">!</div>
</button>
<button type="button" class="btn-reset umb-outline action --copy" localize="title" title="actions_copy"
ng-click="vm.blockEditorApi.copyBlock(vm.layout.$block);"

View File

@@ -34,8 +34,8 @@
var modelObject;
// Property actions:
var copyAllBlocksAction;
var deleteAllBlocksAction;
var copyAllBlocksAction = null;
var deleteAllBlocksAction = null;
var inlineEditing = false;
var liveEditing = true;
@@ -50,21 +50,21 @@
}
vm.currentBlockInFocus = block;
block.focus = true;
}
};
vm.supportCopy = clipboardService.isSupported();
vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model.
vm.availableBlockTypes = []; // Available block entries of this property editor.
vm.labels = {};
var labels = {};
vm.labels = labels;
localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) {
labels.grid_addElement = data[0];
labels.content_createEmpty = data[1];
vm.labels.grid_addElement = data[0];
vm.labels.content_createEmpty = data[1];
});
vm.$onInit = function() {
if (!vm.umbVariantContent) {
if (vm.umbProperty && !vm.umbVariantContent) {// if we dont have vm.umbProperty, it means we are in the DocumentTypeEditor.
// 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 === "umbVariantContentController");
@@ -101,24 +101,21 @@
scopeOfExistence = vm.umbElementEditorContent.getScope();
}
// Create Model Object, to manage our data for this Block Editor.
modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope);
modelObject.load().then(onLoaded);
copyAllBlocksAction = {
labelKey: "clipboard_labelForCopyAllEntries",
labelTokens: [vm.model.label],
icon: "documents",
method: requestCopyAllBlocks,
isDisabled: true
}
};
deleteAllBlocksAction = {
labelKey: 'clipboard_labelForRemoveAllEntries',
labelTokens: [],
icon: 'trash',
method: requestDeleteAllBlocks,
isDisabled: true
}
};
var propertyActions = [
copyAllBlocksAction,
@@ -128,6 +125,11 @@
if (vm.umbProperty) {
vm.umbProperty.setPropertyActions(propertyActions);
}
// Create Model Object, to manage our data for this Block Editor.
modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope);
modelObject.load().then(onLoaded);
};
// Called when we save the value, the server may return an updated data and our value is re-synced
@@ -193,12 +195,37 @@
function getDefaultViewForBlock(block) {
var defaultViewFolderPath = "views/propertyeditors/blocklist/blocklistentryeditors/";
if (block.config.unsupported === true)
return "views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html";
return defaultViewFolderPath + "unsupportedblock/unsupportedblock.editor.html";
if (inlineEditing === true)
return "views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html";
return "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html";
return defaultViewFolderPath + "inlineblock/inlineblock.editor.html";
return defaultViewFolderPath + "labelblock/labelblock.editor.html";
}
/**
* Ensure that the containing content variant languag and current property culture is transfered along
* to the scaffolded content object representing this block.
* This is required for validation along with ensuring that the umb-property inheritance is constently maintained.
* @param {any} content
*/
function ensureCultureData(content) {
if (!content) return;
if (vm.umbVariantContent.editor.content.language) {
// set the scaffolded content's language to the language of the current editor
content.language = vm.umbVariantContent.editor.content.language;
}
// currently we only ever deal with invariant content for blocks so there's only one
content.variants[0].tabs.forEach(tab => {
tab.properties.forEach(prop => {
// set the scaffolded property to the culture of the containing property
prop.culture = vm.umbProperty.property.culture;
});
});
}
function getBlockObject(entry) {
@@ -206,34 +233,24 @@
if (block === null) return null;
// ensure that the containing content variant language/culture is transfered along
// to the scaffolded content object representing this block. This is required for validation
// along with ensuring that the umb-property inheritance is constently maintained.
if (vm.umbVariantContent.editor.content.language) {
block.content.language = vm.umbVariantContent.editor.content.language;
// currently we only ever deal with invariant content for blocks so there's only one
block.content.variants[0].tabs.forEach(tab => {
tab.properties.forEach(prop => {
prop.culture = vm.umbVariantContent.editor.content.language.culture;
});
});
}
ensureCultureData(block.content);
ensureCultureData(block.settings);
// TODO: Why is there a '/' prefixed? that means this will never work with virtual directories
block.view = (block.config.view ? "/" + block.config.view : getDefaultViewForBlock(block));
block.view = (block.config.view ? block.config.view : getDefaultViewForBlock(block));
block.showValidation = block.config.view ? true : false;
block.hideContentInOverlay = block.config.forceHideContentEditorInOverlay === true || inlineEditing === true;
block.showSettings = block.config.settingsElementTypeKey != null;
block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it doesn't make sense to copy.
block.showCopy = vm.supportCopy && block.config.contentElementTypeKey != null;// if we have content, otherwise it doesn't make sense to copy.
return block;
}
function addNewBlock(index, contentTypeKey) {
function addNewBlock(index, contentElementTypeKey) {
// Create layout entry. (not added to property model jet.)
var layoutEntry = modelObject.create(contentTypeKey);
var layoutEntry = modelObject.create(contentElementTypeKey);
if (layoutEntry === null) {
return false;
}
@@ -271,8 +288,12 @@
var removed = vm.layout.splice(layoutIndex, 1);
removed.forEach(x => {
// remove any server validation errors associated
var guid = udiService.getKey(x.contentUdi);
var guids = [udiService.getKey(x.contentUdi), (x.settingsUdi ? udiService.getKey(x.settingsUdi) : null)];
guids.forEach(guid => {
if (guid) {
serverValidationManager.removePropertyError(guid, vm.umbProperty.property.culture, vm.umbProperty.property.segment, "", { matchType: "contains" });
}
})
});
modelObject.removeDataAndDestroyModel(block);
@@ -280,9 +301,9 @@
}
function deleteAllBlocks() {
vm.layout.forEach(entry => {
deleteBlock(entry.$block);
});
while(vm.layout.length) {
deleteBlock(vm.layout[0].$block);
};
}
function activateBlock(blockObject) {
@@ -325,7 +346,6 @@
openSettings: openSettings === true,
liveEditing: liveEditing,
title: blockObject.label,
index: blockIndex,
view: "views/common/infiniteeditors/blockeditor/blockeditor.html",
size: blockObject.config.editorSize || "medium",
submit: function(blockEditorModel) {
@@ -403,7 +423,7 @@
submit: function(blockPickerModel, mouseEvent) {
var added = false;
if (blockPickerModel && blockPickerModel.selectedItem) {
added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentTypeKey);
added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentElementTypeKey);
}
if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) {
@@ -439,6 +459,7 @@
blockPickerModel.clipboardItems.push(
{
type: "elementType",
date: entry.date,
pasteData: entry.data,
blockConfigModel: modelObject.getScaffoldFromAlias(entry.alias),
elementTypeModel: {
@@ -454,6 +475,7 @@
blockPickerModel.clipboardItems.push(
{
type: "elementTypeArray",
date: entry.date,
pasteData: entry.data,
blockConfigModel: {}, // no block configuration for paste items of elementTypeArray.
elementTypeModel: {
@@ -464,6 +486,10 @@
);
});
blockPickerModel.clipboardItems.sort( (a, b) => {
return b.date - a.date
});
// open block picker overlay
editorService.open(blockPickerModel);
@@ -570,6 +596,7 @@
vm.sortableOptions = {
axis: "y",
containment: "parent",
cursor: "grabbing",
handle: ".blockelement__draggable-element",
cancel: "input,textarea,select,option",
@@ -586,30 +613,31 @@
function onAmountOfBlocksChanged() {
// enable/disable property actions
if (copyAllBlocksAction) {
copyAllBlocksAction.isDisabled = vm.layout.length === 0;
}
if (deleteAllBlocksAction) {
deleteAllBlocksAction.isDisabled = vm.layout.length === 0;
}
// validate limits:
if (vm.propertyForm) {
if (vm.propertyForm && vm.validationLimit) {
var isMinRequirementGood = vm.validationLimit.min === null || vm.layout.length >= vm.validationLimit.min;
vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood);
var isMaxRequirementGood = vm.validationLimit.max === null || vm.layout.length <= vm.validationLimit.max;
vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood);
}
}
}
}
unsubscribe.push($scope.$watch(() => vm.layout.length, onAmountOfBlocksChanged));
$scope.$on("$destroy", function () {
for (const subscription of unsubscribe) {
subscription();
}
});
}
})();

View File

@@ -29,7 +29,7 @@
}
);
function BlockListBlockController($scope, $compile, $element) {
function BlockListBlockController($scope, $compile, $element, umbRequestHelper) {
var model = this;
model.$onInit = function () {
@@ -48,8 +48,6 @@
$scope.valFormManager = model.valFormManager;
if (model.stylesheet) {
// TODO: Not sure why this needs a prefixed /? this means it will never work in a virtual directory
model.stylesheet = "/" + model.stylesheet;
var shadowRoot = $element[0].attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>

View File

@@ -18,7 +18,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo
//ensure when form is saved that we don't store [] or [null] as string values in the database when no items are selected
$scope.$on("formSubmitting", function () {
if ($scope.model.value !== null && ($scope.model.value.length === 0 || $scope.model.value[0] === null)) {
if ($scope.model.value && ($scope.model.value.length === 0 || $scope.model.value[0] === null)) {
$scope.model.value = null;
}
});

View File

@@ -14,7 +14,7 @@
<ng-form name="gridItemConfigEditor" val-form-manager>
<umb-box ng-if="model.config">
<umb-box-header title-key="grid_settings" />
<umb-box-header title-key="grid_settings"></umb-box-header>
<umb-box-content>
<div>
<umb-property property="configValue"
@@ -26,7 +26,7 @@
</umb-box>
<umb-box ng-if="model.styles">
<umb-box-header title-key="grid_styles" />
<umb-box-header title-key="grid_styles"></umb-box-header>
<umb-box-content>
<div>
<umb-property property="styleValue"

View File

@@ -19,10 +19,10 @@ angular.module("umbraco")
$scope.setEmbed = function () {
var original = Utilities.isObject($scope.control.value) ? $scope.control.value : null;
var modify = Utilities.isObject($scope.control.value) ? $scope.control.value : null;
var embed = {
original: original,
modify: modify,
submit: function (model) {
var embed = {

View File

@@ -167,8 +167,7 @@
icon: 'trash',
method: removeAllEntries,
isDisabled: true
}
};
// helper to force the current form into the dirty state
function setDirty() {
if ($scope.$parent.$parent.propertyForm) {
@@ -243,6 +242,7 @@
_.each(singleEntriesForPaste, function (entry) {
dialog.pasteItems.push({
type: "elementType",
date: entry.date,
name: entry.label,
data: entry.data,
icon: entry.icon
@@ -253,12 +253,17 @@
_.each(arrayEntriesForPaste, function (entry) {
dialog.pasteItems.push({
type: "elementTypeArray",
date: entry.date,
name: entry.label,
data: entry.data,
icon: entry.icon
});
});
vm.overlayMenu.pasteItems.sort( (a, b) => {
return b.date - a.date
});
dialog.title = dialog.pasteItems.length > 0 ? labels.grid_addElement : labels.content_createEmpty;
dialog.clickClearPaste = function ($event) {
@@ -294,7 +299,7 @@
return (vm.nodes.length > vm.minItems)
? true
: model.config.contentTypes.length > 1;
}
};
function deleteNode(idx) {
var removed = vm.nodes.splice(idx, 1);
@@ -309,6 +314,7 @@
updateModel();
validate();
};
vm.requestDeleteNode = function (idx) {
if (!vm.canDeleteNode(idx)) {
return;
@@ -396,10 +402,11 @@
var scaffold = getScaffold(model.value[idx].ncContentTypeAlias);
return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : "icon-folder";
}
};
vm.sortableOptions = {
axis: "y",
containment: "parent",
cursor: "move",
handle: '.umb-nested-content__header-bar',
distance: 10,

View File

@@ -36,11 +36,12 @@ function RelationTypeCreateController($scope, $location, relationTypeResource, n
var currentPath = node.path ? node.path : "-1";
navigationService.syncTree({ tree: "relationTypes", path: currentPath + "," + data, forceReload: true, activate: true });
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: this.createRelationTypeForm });
var currentSection = appState.getSectionState("currentSection");
$location.path("/" + currentSection + "/relationTypes/edit/" + data);
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: this.createRelationTypeForm, hasErrors: true });
if (err.data && err.data.message) {
notificationsService.error(err.data.message);
navigationService.hideMenu();

View File

@@ -116,12 +116,13 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource,
vm.page.saveButtonState = "busy";
relationTypeResource.save(vm.relationType).then(function (data) {
formHelper.resetForm({ scope: $scope, notifications: data.notifications });
formHelper.resetForm({ scope: $scope });
bindRelationType(data);
vm.page.saveButtonState = "success";
}, function (error) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
contentEditingHelper.handleSaveError({
err: error
});

View File

@@ -40,12 +40,13 @@
activate: true
});
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: form });
var section = appState.getSectionState("currentSection");
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true });
vm.createFolderError = err;
});

View File

@@ -42,10 +42,11 @@
activate: true
});
formHelper.resetForm({ scope: $scope });
formHelper.resetForm({ scope: $scope, formCtrl: form });
}, function (err) {
formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true });
vm.createFolderError = err;
});

View File

@@ -168,7 +168,9 @@
extendedSave(saved).then(function (result) {
//if all is good, then reset the form
formHelper.resetForm({ scope: $scope });
}, Utilities.noop);
}, function () {
formHelper.resetForm({ scope: $scope, hasErrors: true });
});
vm.user = _.omit(saved, "navigation");
//restore
@@ -179,7 +181,7 @@
vm.page.saveButtonState = "success";
}, function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
contentEditingHelper.handleSaveError({
err: err,
showNotifications: true

View File

@@ -41,7 +41,7 @@
}));
var blockConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: null, view: "testview.html" };
var blockConfigurationMock = { contentElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: null, view: "/testview.html" };
var propertyModelMock = {
layout: {
@@ -60,7 +60,7 @@
]
};
var blockWithSettingsConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", view: "testview.html" };
var blockWithSettingsConfigurationMock = { contentElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", view: "/testview.html" };
var propertyModelWithSettingsMock = {
layout: {
"Umbraco.TestBlockEditor": [
@@ -105,7 +105,7 @@
it('getBlockConfiguration provide the requested block configurtion', function () {
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label);
expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentElementTypeKey).label).toBe(blockConfigurationMock.label);
});
it('load provides data for itemPicker', function (done) {
@@ -115,7 +115,7 @@
try {
var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker();
expect(itemPickerOptions.length).toBe(1);
expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey);
expect(itemPickerOptions[0].blockConfigModel.contentElementTypeKey).toBe(blockConfigurationMock.contentElementTypeKey);
done();
} catch (e) {
done.fail(e);
@@ -139,7 +139,7 @@
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[0].contentUdi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].contentUdi);
done();
} catch (e) {

View File

@@ -6,6 +6,13 @@ describe('umbRequestHelper tests', function () {
beforeEach(inject(function ($injector) {
umbRequestHelper = $injector.get('umbRequestHelper');
// set the Umbraco.Sys.ServerVariables.application.applicationPath
if (!Umbraco) Umbraco = {};
if (!Umbraco.Sys) Umbraco.Sys = {};
if (!Umbraco.Sys.ServerVariables) Umbraco.Sys.ServerVariables = {};
if (!Umbraco.Sys.ServerVariables.application) Umbraco.Sys.ServerVariables.application = {};
Umbraco.Sys.ServerVariables.application.applicationPath = "/mysite/";
}));
describe('formatting Urls', function () {
@@ -34,4 +41,25 @@ describe('umbRequestHelper tests', function () {
});
});
describe('Virtual Paths', function () {
it('can convert virtual path to absolute url', function () {
var result = umbRequestHelper.convertVirtualToAbsolutePath("~/App_Plugins/hello/world.css");
expect(result).toBe("/mysite/App_Plugins/hello/world.css");
});
it('can convert absolute path to absolute url', function () {
var result = umbRequestHelper.convertVirtualToAbsolutePath("/App_Plugins/hello/world.css");
expect(result).toBe("/App_Plugins/hello/world.css");
});
it('throws on invalid virtual path', function () {
var relativePath = "App_Plugins/hello/world.css";
expect(function () {
umbRequestHelper.convertVirtualToAbsolutePath(relativePath);
}).toThrow("The path " + relativePath + " is not a virtual path");
});
});
});

View File

@@ -648,6 +648,7 @@
<key alias="design">Design</key>
<key alias="dictionary">Ordbog</key>
<key alias="dimensions">Dimensioner</key>
<key alias="discard">Kassér</key>
<key alias="down">Ned</key>
<key alias="download">Hent</key>
<key alias="edit">Rediger</key>
@@ -1854,6 +1855,7 @@ Mange hilsner fra Umbraco robotten
<key alias="tabBlockSettings">Indstillinger</key>
<key alias="headlineAdvanced">Avanceret</key>
<key alias="forceHideContentEditor">Skjuld indholds editoren</key>
<key alias="blockHasChanges">Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem?</key>
</area>
</language>

View File

@@ -676,6 +676,7 @@
<key alias="design">Design</key>
<key alias="dictionary">Dictionary</key>
<key alias="dimensions">Dimensions</key>
<key alias="discard">Discard</key>
<key alias="down">Down</key>
<key alias="download">Download</key>
<key alias="edit">Edit</key>
@@ -2479,6 +2480,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="tabBlockSettings">Settings</key>
<key alias="headlineAdvanced">Advanced</key>
<key alias="forceHideContentEditor">Force hide content editor</key>
<key alias="blockHasChanges">You have made changes to this content. Are you sure you want to discard them?</key>
</area>
<area alias="contentTemplatesDashboard">
<key alias="whatHeadline">What are Content Templates?</key>

View File

@@ -684,6 +684,7 @@
<key alias="design">Design</key>
<key alias="dictionary">Dictionary</key>
<key alias="dimensions">Dimensions</key>
<key alias="discard">Discard</key>
<key alias="down">Down</key>
<key alias="download">Download</key>
<key alias="edit">Edit</key>
@@ -2500,6 +2501,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="tabBlockSettings">Settings</key>
<key alias="headlineAdvanced">Advanced</key>
<key alias="forceHideContentEditor">Force hide content editor</key>
<key alias="blockHasChanges">You have made changes to this content. Are you sure you want to discard them?</key>
</area>
<area alias="contentTemplatesDashboard">
<key alias="whatHeadline">What are Content Templates?</key>

View File

@@ -1,13 +1,13 @@
@inherits UmbracoViewPage<BlockListModel>
@using Umbraco.Core.Models.Blocks
@{
if (Model?.Layout == null || !Model.Layout.Any()) { return; }
if (!Model.Any()) { return; }
}
<div class="umb-block-list">
@foreach (var layout in Model.Layout)
@foreach (var block in Model)
{
if (layout?.Udi == null) { continue; }
var data = layout.Data;
@Html.Partial("BlockList/Components/" + data.ContentType.Alias, layout)
if (block?.ContentUdi == null) { continue; }
var data = block.Content;
@Html.Partial("BlockList/Components/" + data.ContentType.Alias, block)
}
</div>

View File

@@ -36,7 +36,9 @@
@helper renderRow(dynamic row, bool singleColumn){
<div @RenderElementAttributes(row)>
@Html.If(singleColumn, "<div class='container'>")
@if (singleColumn) {
@:<div class="container">
}
<div class="row clearfix">
@foreach ( var area in row.areas ) {
<div class="col-md-@area.grid column">
@@ -49,7 +51,9 @@
</div>
</div>}
</div>
@Html.If(singleColumn, "</div>")
@if (singleColumn) {
@:</div>
}
</div>
}

View File

@@ -200,7 +200,7 @@
"event": "click"
},
{
"element": "[data-element='editor-data-type-picker'] [data-element='datatypeconfig-Textarea'] > a",
"element": "[data-element='editor-data-type-picker'] [data-element='datatypeconfig-Textarea']",
"title": "Editor settings",
"content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed.",
"event": "click"

View File

@@ -14,7 +14,7 @@ namespace Umbraco.Web
public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate)
{
if (model?.Layout == null || !model.Layout.Any()) return new MvcHtmlString(string.Empty);
if (model?.Count == 0) return new MvcHtmlString(string.Empty);
var view = DefaultFolder + template;
return html.Partial(view, model);

View File

@@ -48,20 +48,11 @@ namespace Umbraco.Web.Models.Mapping
}
public ContentTypeBasic GetContentType(IContentBase source, MapperContext context)
{
// TODO: We can resolve the UmbracoContext from the IValueResolver options!
// OMG
if (HttpContext.Current != null && Composing.Current.UmbracoContext != null && Composing.Current.UmbracoContext.Security.CurrentUser != null
&& Composing.Current.UmbracoContext.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings)))
{
var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source);
var contentTypeBasic = context.Map<IContentTypeComposition, ContentTypeBasic>(contentType);
return contentTypeBasic;
}
//no access
return null;
}
public string GetTreeNodeUrl<TController>(IContentBase source)
where TController : ContentTreeControllerBase

View File

@@ -1,10 +1,8 @@
using Newtonsoft.Json;
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;
@@ -12,7 +10,6 @@ using Umbraco.Core.Models.Blocks;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using static Umbraco.Core.Models.Blocks.BlockEditorData;
using static Umbraco.Core.Models.Blocks.BlockItemData;
namespace Umbraco.Web.PropertyEditors
@@ -49,7 +46,7 @@ namespace Umbraco.Web.PropertyEditors
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 IDataTypeService _dataTypeService;
private readonly ILogger _logger;
private readonly BlockEditorValues _blockEditorValues;
@@ -60,7 +57,7 @@ namespace Umbraco.Web.PropertyEditors
_dataTypeService = dataTypeService;
_logger = logger;
_blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger);
Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService));
Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService, contentTypeService));
Validators.Add(new MinMaxValidator(_blockEditorValues, textService));
}
@@ -241,19 +238,33 @@ namespace Umbraco.Web.PropertyEditors
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
{
var blockConfig = (BlockListConfiguration)dataTypeConfiguration;
if (blockConfig == null) yield break;
var validationLimit = blockConfig.ValidationLimit;
if (validationLimit == null) yield break;
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
if ((blockEditorData == null && blockConfig?.ValidationLimit?.Min > 0)
|| (blockEditorData != null && blockEditorData.Layout.Count() < blockConfig?.ValidationLimit?.Min))
if ((blockEditorData == null && validationLimit.Min.HasValue && validationLimit.Min > 0)
|| (blockEditorData != null && validationLimit.Min.HasValue && blockEditorData.Layout.Count() < validationLimit.Min))
{
yield return new ValidationResult(
_textService.Localize("validation/entriesShort", new[] { blockConfig.ValidationLimit.Min.ToString(), (blockConfig.ValidationLimit.Min - blockEditorData.Layout.Count()).ToString() }),
_textService.Localize("validation/entriesShort", new[]
{
validationLimit.Min.ToString(),
(validationLimit.Min - blockEditorData.Layout.Count()).ToString()
}),
new[] { "minCount" });
}
if (blockEditorData != null && blockEditorData.Layout.Count() > blockConfig?.ValidationLimit?.Max)
if (blockEditorData != null && validationLimit.Max.HasValue && blockEditorData.Layout.Count() > validationLimit.Max)
{
yield return new ValidationResult(
_textService.Localize("validation/entriesExceed", new[] { blockConfig.ValidationLimit.Max.ToString(), (blockEditorData.Layout.Count() - blockConfig.ValidationLimit.Max).ToString() }),
_textService.Localize("validation/entriesExceed", new[]
{
validationLimit.Max.ToString(),
(blockEditorData.Layout.Count() - validationLimit.Max).ToString()
}),
new[] { "maxCount" });
}
}
@@ -262,10 +273,13 @@ namespace Umbraco.Web.PropertyEditors
internal class BlockEditorValidator : ComplexEditorValidator
{
private readonly BlockEditorValues _blockEditorValues;
private readonly IContentTypeService _contentTypeService;
public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) : base(propertyEditors, dataTypeService, textService)
public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService, IContentTypeService contentTypeService)
: base(propertyEditors, dataTypeService, textService)
{
_blockEditorValues = blockEditorValues;
_contentTypeService = contentTypeService;
}
protected override IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value)
@@ -273,8 +287,28 @@ namespace Umbraco.Web.PropertyEditors
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
if (blockEditorData != null)
{
foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
// There is no guarantee that the client will post data for every property defined in the Element Type but we still
// need to validate that data for each property especially for things like 'required' data to work.
// Lookup all element types for all content/settings and then we can populate any empty properties.
var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList();
var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key);
foreach (var row in allElements)
{
if (!allElementTypes.TryGetValue(row.ContentTypeKey, out var elementType))
throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}");
// now ensure missing properties
foreach (var elementTypeProp in elementType.CompositionPropertyTypes)
{
if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias))
{
// set values to null
row.PropertyValues[elementTypeProp.Alias] = new BlockPropertyValue(null, elementTypeProp);
row.RawPropertyValues[elementTypeProp.Alias] = null;
}
}
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key);
foreach (var prop in row.PropertyValues)
{
@@ -369,11 +403,7 @@ namespace Umbraco.Web.PropertyEditors
else
{
// set the value to include the resolved property type
propValues[prop.Key] = new BlockPropertyValue
{
PropertyType = propType,
Value = prop.Value
};
propValues[prop.Key] = new BlockPropertyValue(prop.Value, propType);
}
}

Some files were not shown because too many files have changed in this diff Show More