Merge branch 'v8/dev' into v8/contrib
# Conflicts: # src/Umbraco.Web.UI.Client/package.json # src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js # src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js # src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js # src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.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/dictionary/edit.html # src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
This commit is contained in:
@@ -36,6 +36,11 @@ namespace Umbraco.Core
|
||||
/// </summary>
|
||||
public static class Aliases
|
||||
{
|
||||
/// <summary>
|
||||
/// Block List.
|
||||
/// </summary>
|
||||
public const string BlockList = "Umbraco.BlockList";
|
||||
|
||||
/// <summary>
|
||||
/// CheckBox List.
|
||||
/// </summary>
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace Umbraco.Core
|
||||
{ Unknown, UdiType.Unknown },
|
||||
|
||||
{ AnyGuid, UdiType.GuidUdi },
|
||||
{ Element, UdiType.GuidUdi },
|
||||
{ Document, UdiType.GuidUdi },
|
||||
{ DocumentBlueprint, UdiType.GuidUdi },
|
||||
{ Media, UdiType.GuidUdi },
|
||||
@@ -64,6 +65,8 @@ namespace Umbraco.Core
|
||||
|
||||
public const string AnyGuid = "any-guid"; // that one is for tests
|
||||
|
||||
public const string Element = "element";
|
||||
|
||||
public const string Document = "document";
|
||||
|
||||
public const string DocumentBlueprint = "document-blueprint";
|
||||
|
||||
@@ -191,9 +191,9 @@ namespace Umbraco.Core.Migrations.Upgrade
|
||||
To<MissingContentVersionsIndexes>("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
|
||||
To<AddMainDomLock>("{2AB29964-02A1-474D-BD6B-72148D2A53A2}");
|
||||
|
||||
|
||||
// to 8.7.0...
|
||||
To<MissingDictionaryIndex>("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}");
|
||||
|
||||
|
||||
//FINAL
|
||||
}
|
||||
}
|
||||
|
||||
45
src/Umbraco.Core/Models/Blocks/BlockEditorData.cs
Normal file
45
src/Umbraco.Core/Models/Blocks/BlockEditorData.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Convertable block data from json
|
||||
/// </summary>
|
||||
public class BlockEditorData
|
||||
{
|
||||
private readonly string _propertyEditorAlias;
|
||||
|
||||
public static BlockEditorData Empty { get; } = new BlockEditorData();
|
||||
|
||||
private BlockEditorData()
|
||||
{
|
||||
BlockValue = new BlockValue();
|
||||
}
|
||||
|
||||
public BlockEditorData(string propertyEditorAlias,
|
||||
IEnumerable<ContentAndSettingsReference> references,
|
||||
BlockValue blockValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propertyEditorAlias))
|
||||
throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias));
|
||||
_propertyEditorAlias = propertyEditorAlias;
|
||||
BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue));
|
||||
References = references != null ? new List<ContentAndSettingsReference>(references) : throw new ArgumentNullException(nameof(references));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the layout for this specific property editor
|
||||
/// </summary>
|
||||
public JToken Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the reference to the original BlockValue
|
||||
/// </summary>
|
||||
public BlockValue BlockValue { get; }
|
||||
|
||||
public List<ContentAndSettingsReference> References { get; } = new List<ContentAndSettingsReference>();
|
||||
}
|
||||
}
|
||||
43
src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs
Normal file
43
src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Converts the block json data into objects
|
||||
/// </summary>
|
||||
public abstract class BlockEditorDataConverter
|
||||
{
|
||||
private readonly string _propertyEditorAlias;
|
||||
|
||||
protected BlockEditorDataConverter(string propertyEditorAlias)
|
||||
{
|
||||
_propertyEditorAlias = propertyEditorAlias;
|
||||
}
|
||||
|
||||
public BlockEditorData Deserialize(string json)
|
||||
{
|
||||
var value = JsonConvert.DeserializeObject<BlockValue>(json);
|
||||
|
||||
if (value.Layout == null)
|
||||
return BlockEditorData.Empty;
|
||||
|
||||
var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout)
|
||||
? GetBlockReferences(layout)
|
||||
: Enumerable.Empty<ContentAndSettingsReference>();
|
||||
|
||||
return new BlockEditorData(_propertyEditorAlias, references, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the collection of <see cref="IBlockReference"/> from the block editor's Layout (which could be an array or an object depending on the editor)
|
||||
/// </summary>
|
||||
/// <param name="jsonLayout"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract IEnumerable<ContentAndSettingsReference> GetBlockReferences(JToken jsonLayout);
|
||||
|
||||
}
|
||||
}
|
||||
36
src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs
Normal file
36
src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
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>();
|
||||
}
|
||||
}
|
||||
56
src/Umbraco.Core/Models/Blocks/BlockItemData.cs
Normal file
56
src/Umbraco.Core/Models/Blocks/BlockItemData.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single block's data in raw form
|
||||
/// </summary>
|
||||
public class BlockItemData
|
||||
{
|
||||
[JsonProperty("contentTypeKey")]
|
||||
public Guid ContentTypeKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// not serialized, manually set and used during internally
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string ContentTypeAlias { get; set; }
|
||||
|
||||
[JsonProperty("udi")]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi Udi { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Guid Key => Udi != null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned");
|
||||
|
||||
/// <summary>
|
||||
/// The remaining properties will be serialized to a dictionary
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
|
||||
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
|
||||
/// NestedContent serializes to string, int, whatever eg
|
||||
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
|
||||
/// </remarks>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object> RawPropertyValues { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to convert the raw property data into data with a property type context
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IDictionary<string, BlockPropertyValue> PropertyValues { get; set; } = new Dictionary<string, BlockPropertyValue>();
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to populate the property value/property type of a block item content property
|
||||
/// </summary>
|
||||
public class BlockPropertyValue
|
||||
{
|
||||
public object Value { get; set; }
|
||||
public PropertyType PropertyType { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Data converter for the block list property editor
|
||||
/// </summary>
|
||||
public class BlockListEditorDataConverter : BlockEditorDataConverter
|
||||
{
|
||||
public BlockListEditorDataConverter() : base(Constants.PropertyEditors.Aliases.BlockList)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<ContentAndSettingsReference> GetBlockReferences(JToken jsonLayout)
|
||||
{
|
||||
var blockListLayout = jsonLayout.ToObject<IEnumerable<BlockListLayoutItem>>();
|
||||
return blockListLayout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs
Normal file
19
src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for deserializing the block list layout
|
||||
/// </summary>
|
||||
public class BlockListLayoutItem
|
||||
{
|
||||
[JsonProperty("contentUdi", Required = Required.Always)]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi ContentUdi { get; set; }
|
||||
|
||||
[JsonProperty("settingsUdi")]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi SettingsUdi { get; set; }
|
||||
}
|
||||
}
|
||||
51
src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs
Normal file
51
src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
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>
|
||||
{
|
||||
public BlockListLayoutReference(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
|
||||
{
|
||||
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
Settings = settings; // can be null
|
||||
SettingsUdi = settingsUdi; // can be null
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Id of the content data item
|
||||
/// </summary>
|
||||
[DataMember(Name = "contentUdi")]
|
||||
public Udi ContentUdi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Id of the settings data item
|
||||
/// </summary>
|
||||
[DataMember(Name = "settingsUdi")]
|
||||
public Udi SettingsUdi { get; }
|
||||
|
||||
/// <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]
|
||||
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]
|
||||
public IPublishedElement Settings { get; }
|
||||
}
|
||||
}
|
||||
33
src/Umbraco.Core/Models/Blocks/BlockListModel.cs
Normal file
33
src/Umbraco.Core/Models/Blocks/BlockListModel.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// The strongly typed model for the Block List editor
|
||||
/// </summary>
|
||||
[DataContract(Name = "blockList", Namespace = "")]
|
||||
public class BlockListModel : BlockEditorModel
|
||||
{
|
||||
public static BlockListModel Empty { get; } = new BlockListModel();
|
||||
|
||||
private BlockListModel()
|
||||
{
|
||||
}
|
||||
|
||||
public BlockListModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData, IEnumerable<BlockListLayoutReference> layout)
|
||||
: base(contentData, settingsData)
|
||||
{
|
||||
Layout = layout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The layout items of the Block List editor
|
||||
/// </summary>
|
||||
[DataMember(Name = "layout")]
|
||||
public IEnumerable<BlockListLayoutReference> Layout { get; } = new List<BlockListLayoutReference>();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
18
src/Umbraco.Core/Models/Blocks/BlockValue.cs
Normal file
18
src/Umbraco.Core/Models/Blocks/BlockValue.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
public class BlockValue
|
||||
{
|
||||
[JsonProperty("layout")]
|
||||
public IDictionary<string, JToken> Layout { get; set; }
|
||||
|
||||
[JsonProperty("contentData")]
|
||||
public List<BlockItemData> ContentData { get; set; } = new List<BlockItemData>();
|
||||
|
||||
[JsonProperty("settingsData")]
|
||||
public List<BlockItemData> SettingsData { get; set; } = new List<BlockItemData>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
public struct ContentAndSettingsReference : IEquatable<ContentAndSettingsReference>
|
||||
{
|
||||
public ContentAndSettingsReference(Udi contentUdi, Udi settingsUdi)
|
||||
{
|
||||
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
|
||||
SettingsUdi = settingsUdi;
|
||||
}
|
||||
|
||||
public Udi ContentUdi { get; }
|
||||
public Udi SettingsUdi { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ContentAndSettingsReference reference && Equals(reference);
|
||||
}
|
||||
|
||||
public bool Equals(ContentAndSettingsReference other)
|
||||
{
|
||||
return EqualityComparer<Udi>.Default.Equals(ContentUdi, other.ContentUdi) &&
|
||||
EqualityComparer<Udi>.Default.Equals(SettingsUdi, other.SettingsUdi);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = 272556606;
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<Udi>.Default.GetHashCode(ContentUdi);
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<Udi>.Default.GetHashCode(SettingsUdi);
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Umbraco.Core/Models/Blocks/IBlockReference.cs
Normal file
26
src/Umbraco.Core/Models/Blocks/IBlockReference.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data item reference for a Block editor implementation
|
||||
/// </summary>
|
||||
/// <typeparam name="TSettings"></typeparam>
|
||||
/// <remarks>
|
||||
/// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
|
||||
/// </remarks>
|
||||
public interface IBlockReference<TSettings> : IBlockReference
|
||||
{
|
||||
TSettings Settings { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data item reference for a Block Editor implementation
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
|
||||
/// </remarks>
|
||||
public interface IBlockReference
|
||||
{
|
||||
Udi ContentUdi { get; }
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ namespace Umbraco.Core.Models
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content is published.
|
||||
/// </summary>
|
||||
/// <remarks>The <see cref="PublishedVersionId"/> property tells you which version of the content is currently published.</remarks>
|
||||
bool Published { get; set; }
|
||||
|
||||
PublishedState PublishedState { get; set; }
|
||||
@@ -32,10 +33,11 @@ namespace Umbraco.Core.Models
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content has been edited.
|
||||
/// </summary>
|
||||
/// <remarks>Will return `true` once unpublished edits have been made after the version with <see cref="PublishedVersionId"/> has been published.</remarks>
|
||||
bool Edited { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the published version identifier.
|
||||
/// Gets the version identifier for the currently published version of the content.
|
||||
/// </summary>
|
||||
int PublishedVersionId { get; set; }
|
||||
|
||||
@@ -129,6 +131,6 @@ namespace Umbraco.Core.Models
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IContent DeepCloneWithResetIdentities();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an <see cref="IPublishedElement"/> type.
|
||||
/// </summary>
|
||||
/// <remarks>Instances implementing the <see cref="IPublishedContentType"/> interface should be
|
||||
/// immutable, ie if the content type changes, then a new instance needs to be created.</remarks>
|
||||
public interface IPublishedContentType2 : IPublishedContentType
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique key for the content type.
|
||||
/// </summary>
|
||||
Guid Key { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an <see cref="IPublishedElement"/> type.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace Umbraco. Core.Models.PublishedContent
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Creates published content types.
|
||||
/// </summary>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Provides the published model creation service.
|
||||
/// </summary>
|
||||
|
||||
@@ -3,6 +3,7 @@ using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Provides strongly typed published content models services.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// </summary>
|
||||
/// <remarks>Instances of the <see cref="PublishedContentType"/> class are immutable, ie
|
||||
/// if the content type changes, then a new class needs to be created.</remarks>
|
||||
public class PublishedContentType : IPublishedContentType
|
||||
public class PublishedContentType : IPublishedContentType2
|
||||
{
|
||||
private readonly IPublishedPropertyType[] _propertyTypes;
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// Initializes a new instance of the <see cref="PublishedContentType"/> class with a content type.
|
||||
/// </summary>
|
||||
public PublishedContentType(IContentTypeComposition contentType, IPublishedContentTypeFactory factory)
|
||||
: this(contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement)
|
||||
: this(contentType.Key, contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement)
|
||||
{
|
||||
var propertyTypes = contentType.CompositionPropertyTypes
|
||||
.Select(x => factory.CreatePropertyType(this, x))
|
||||
@@ -40,8 +40,20 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// <remarks>
|
||||
/// <para>Values are assumed to be consistent and are not checked.</para>
|
||||
/// </remarks>
|
||||
public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes, ContentVariation variations, bool isElement = false)
|
||||
: this(key, id, alias, itemType, compositionAliases, variations, isElement)
|
||||
{
|
||||
var propertyTypesA = propertyTypes.ToArray();
|
||||
foreach (var propertyType in propertyTypesA)
|
||||
propertyType.ContentType = this;
|
||||
_propertyTypes = propertyTypesA;
|
||||
|
||||
InitializeIndexes();
|
||||
}
|
||||
|
||||
[Obsolete("Use the overload specifying a key instead")]
|
||||
public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes, ContentVariation variations, bool isElement = false)
|
||||
: this (id, alias, itemType, compositionAliases, variations, isElement)
|
||||
: this (Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement)
|
||||
{
|
||||
var propertyTypesA = propertyTypes.ToArray();
|
||||
foreach (var propertyType in propertyTypesA)
|
||||
@@ -57,16 +69,26 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// <remarks>
|
||||
/// <para>Values are assumed to be consistent and are not checked.</para>
|
||||
/// </remarks>
|
||||
public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations, bool isElement = false)
|
||||
: this(key, id, alias, itemType, compositionAliases, variations, isElement)
|
||||
{
|
||||
_propertyTypes = propertyTypes(this).ToArray();
|
||||
|
||||
InitializeIndexes();
|
||||
}
|
||||
|
||||
[Obsolete("Use the overload specifying a key instead")]
|
||||
public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations, bool isElement = false)
|
||||
: this(id, alias, itemType, compositionAliases, variations, isElement)
|
||||
: this(Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement)
|
||||
{
|
||||
_propertyTypes = propertyTypes(this).ToArray();
|
||||
|
||||
InitializeIndexes();
|
||||
}
|
||||
|
||||
private PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, ContentVariation variations, bool isElement)
|
||||
private PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, ContentVariation variations, bool isElement)
|
||||
{
|
||||
Key = key;
|
||||
Id = id;
|
||||
Alias = alias;
|
||||
ItemType = itemType;
|
||||
@@ -116,6 +138,9 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
|
||||
#region Content type
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Key { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Id { get; }
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
public static class PublishedContentTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the GUID key from an <see cref="IPublishedContentType"/>
|
||||
/// </summary>
|
||||
/// <param name="publishedContentType"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public static bool TryGetKey(this IPublishedContentType publishedContentType, out Guid key)
|
||||
{
|
||||
if (publishedContentType is IPublishedContentType2 contentTypeWithKey)
|
||||
{
|
||||
key = contentTypeWithKey.Key;
|
||||
return true;
|
||||
}
|
||||
key = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,18 +35,18 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// This method is for tests and is not intended to be used directly from application code.
|
||||
/// </summary>
|
||||
/// <remarks>Values are assumed to be consisted and are not checked.</remarks>
|
||||
internal IPublishedContentType CreateContentType(int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
|
||||
internal IPublishedContentType CreateContentType(Guid key, int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
|
||||
{
|
||||
return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, variations, isElement);
|
||||
return new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, variations, isElement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is for tests and is not intended to be used directly from application code.
|
||||
/// </summary>
|
||||
/// <remarks>Values are assumed to be consisted and are not checked.</remarks>
|
||||
internal IPublishedContentType CreateContentType(int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
|
||||
internal IPublishedContentType CreateContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
|
||||
{
|
||||
return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement);
|
||||
return new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace Umbraco.Core.Models
|
||||
if (contentType == null) throw new ArgumentNullException(nameof(contentType));
|
||||
|
||||
Id = contentType.Id;
|
||||
Key = contentType.Key;
|
||||
Alias = contentType.Alias;
|
||||
Variations = contentType.Variations;
|
||||
Icon = contentType.Icon;
|
||||
@@ -51,6 +52,8 @@ namespace Umbraco.Core.Models
|
||||
|
||||
public int Id { get; }
|
||||
|
||||
public Guid Key { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ITemplate DefaultTemplate { get; }
|
||||
|
||||
@@ -109,13 +112,14 @@ namespace Umbraco.Core.Models
|
||||
string ITreeEntity.Name { get => this.Name; set => throw new NotImplementedException(); }
|
||||
int IEntity.Id { get => this.Id; set => throw new NotImplementedException(); }
|
||||
bool IEntity.HasIdentity => this.Id != default;
|
||||
Guid IEntity.Key { get => this.Key; set => throw new NotImplementedException(); }
|
||||
|
||||
int ITreeEntity.CreatorId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
int ITreeEntity.ParentId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
int ITreeEntity.Level { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
string ITreeEntity.Path { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
int ITreeEntity.SortOrder { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
bool ITreeEntity.Trashed => throw new NotImplementedException();
|
||||
Guid IEntity.Key { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
bool ITreeEntity.Trashed => throw new NotImplementedException();
|
||||
DateTime IEntity.CreateDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
DateTime IEntity.UpdateDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
DateTime? IEntity.DeleteDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
|
||||
@@ -4,6 +4,10 @@ using System.Data.SqlClient;
|
||||
|
||||
namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
{
|
||||
// See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues
|
||||
// Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ?
|
||||
// but i guess that's not netcore so we'll just leave it.
|
||||
|
||||
/// <summary>
|
||||
/// Provides the transient error detection logic for transient faults that are specific to SQL Azure.
|
||||
/// </summary>
|
||||
@@ -71,7 +75,7 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
/// Determines whether the specified exception represents a transient failure that can be compensated by a retry.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception object to be verified.</param>
|
||||
/// <returns>True if the specified exception is considered as transient, otherwise false.</returns>
|
||||
/// <returns>true if the specified exception is considered as transient; otherwise, false.</returns>
|
||||
public bool IsTransient(Exception ex)
|
||||
{
|
||||
if (ex != null)
|
||||
@@ -97,40 +101,50 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
|
||||
return true;
|
||||
|
||||
// SQL Error Code: 40197
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40197:
|
||||
// SQL Error Code: 10928
|
||||
// Resource ID: %d. The %s limit for the database is %d and has been reached.
|
||||
case 10928:
|
||||
// SQL Error Code: 10929
|
||||
// Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d.
|
||||
// However, the server is currently too busy to support requests greater than %d for this database.
|
||||
case 10929:
|
||||
// SQL Error Code: 10053
|
||||
// A transport-level error has occurred when receiving results from the server.
|
||||
// An established connection was aborted by the software in your host machine.
|
||||
case 10053:
|
||||
// SQL Error Code: 10054
|
||||
// A transport-level error has occurred when sending the request to the server.
|
||||
// A transport-level error has occurred when sending the request to the server.
|
||||
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
|
||||
case 10054:
|
||||
// SQL Error Code: 10060
|
||||
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
|
||||
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
|
||||
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
|
||||
// because the connected party did not properly respond after a period of time, or established connection failed
|
||||
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
|
||||
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
|
||||
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
|
||||
// because the connected party did not properly respond after a period of time, or established connection failed
|
||||
// because connected host has failed to respond.)"}
|
||||
case 10060:
|
||||
// SQL Error Code: 40197
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40197:
|
||||
// SQL Error Code: 40540
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40540:
|
||||
// SQL Error Code: 40613
|
||||
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
|
||||
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
|
||||
// support, and provide them the session tracing ID of ZZZZZ.
|
||||
case 40613:
|
||||
// SQL Error Code: 40143
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40143:
|
||||
// SQL Error Code: 233
|
||||
// The client was unable to establish a connection because of an error during connection initialization process before login.
|
||||
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
|
||||
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
|
||||
// The client was unable to establish a connection because of an error during connection initialization process before login.
|
||||
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
|
||||
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
|
||||
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
|
||||
case 233:
|
||||
// SQL Error Code: 64
|
||||
// A connection was successfully established with the server, but then an error occurred during the login process.
|
||||
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
|
||||
// A connection was successfully established with the server, but then an error occurred during the login process.
|
||||
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
|
||||
case 64:
|
||||
// DBNETLIB Error Code: 20
|
||||
// The instance of SQL Server you attempted to connect to does not support encryption.
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
/// </summary>
|
||||
void ClearSchedule(DateTime date);
|
||||
|
||||
void ClearSchedule(DateTime date, ContentScheduleAction action);
|
||||
|
||||
bool HasContentForExpiration(DateTime date);
|
||||
bool HasContentForRelease(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// Gets <see cref="IContent"/> objects having an expiration date before (lower than, or equal to) a specified date.
|
||||
/// </summary>
|
||||
|
||||
@@ -1017,6 +1017,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
Database.Execute(sql);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearSchedule(DateTime date, ContentScheduleAction action)
|
||||
{
|
||||
var a = action.ToString();
|
||||
var sql = Sql().Delete<ContentScheduleDto>().Where<ContentScheduleDto>(x => x.Date <= date && x.Action == a);
|
||||
Database.Execute(sql);
|
||||
}
|
||||
|
||||
private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date)
|
||||
{
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql
|
||||
.SelectCount()
|
||||
.From<ContentScheduleDto>()
|
||||
.Where<ContentScheduleDto>(x => x.Action == SqlTemplate.Arg<string>("action") && x.Date <= SqlTemplate.Arg<DateTime>("date")));
|
||||
|
||||
var sql = template.Sql(action.ToString(), date);
|
||||
return sql;
|
||||
}
|
||||
|
||||
public bool HasContentForExpiration(DateTime date)
|
||||
{
|
||||
var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date);
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
public bool HasContentForRelease(DateTime date)
|
||||
{
|
||||
var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date);
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetContentForRelease(DateTime date)
|
||||
{
|
||||
|
||||
@@ -157,12 +157,11 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
// merge the crop values - the alias + width + height comes from
|
||||
// configuration, but each crop can store its own coordinates
|
||||
|
||||
if (Crops == null) return;
|
||||
|
||||
var configuredCrops = configuration?.Crops;
|
||||
if (configuredCrops == null) return;
|
||||
|
||||
var crops = Crops.ToList();
|
||||
//Use Crops if it's not null, otherwise create a new list
|
||||
var crops = Crops?.ToList() ?? new List<ImageCropperCrop>();
|
||||
|
||||
foreach (var configuredCrop in configuredCrops)
|
||||
{
|
||||
|
||||
@@ -21,12 +21,11 @@ namespace Umbraco.Core.Runtime
|
||||
private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom";
|
||||
private const string UpdatedSuffix = "_updated";
|
||||
private readonly ILogger _logger;
|
||||
private IUmbracoDatabase _db;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider();
|
||||
private bool _mainDomChanging = false;
|
||||
private readonly UmbracoDatabaseFactory _dbFactory;
|
||||
private bool _hasError;
|
||||
private bool _errorDuringAcquiring;
|
||||
private object _locker = new object();
|
||||
|
||||
public SqlMainDomLock(ILogger logger)
|
||||
@@ -56,25 +55,24 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Acquiring lock...");
|
||||
|
||||
var db = GetDatabase();
|
||||
|
||||
var tempId = Guid.NewGuid().ToString();
|
||||
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
// wait to get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch(SqlException ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not acquire MainDom.");
|
||||
_hasError = true;
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -82,15 +80,12 @@ namespace Umbraco.Core.Runtime
|
||||
throw;
|
||||
}
|
||||
|
||||
var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown
|
||||
var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown
|
||||
if (result == RecordPersistenceType.Insert)
|
||||
{
|
||||
// if we've inserted, then there was no MainDom so we can instantly acquire
|
||||
|
||||
// TODO: see the other TODO, could we just delete the row and that would indicate that we
|
||||
// are MainDom? then we don't leave any orphan rows behind.
|
||||
|
||||
InsertLockRecord(_lockId); // so update with our appdomain id
|
||||
InsertLockRecord(_lockId, db); // so update with our appdomain id
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
@@ -100,23 +95,23 @@ namespace Umbraco.Core.Runtime
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, cannot acquire MainDom");
|
||||
_hasError = true;
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
transaction.Complete();
|
||||
}
|
||||
|
||||
|
||||
return await WaitForExistingAsync(tempId, millisecondsTimeout);
|
||||
}
|
||||
|
||||
public Task ListenAsync()
|
||||
{
|
||||
if (_hasError)
|
||||
if (_errorDuringAcquiring)
|
||||
{
|
||||
_logger.Warn<SqlMainDomLock>("Could not acquire MainDom, listening is canceled.");
|
||||
return Task.CompletedTask;
|
||||
@@ -142,8 +137,15 @@ namespace Umbraco.Core.Runtime
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// poll every 1 second
|
||||
Thread.Sleep(1000);
|
||||
// poll every couple of seconds
|
||||
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
|
||||
Thread.Sleep(2000);
|
||||
|
||||
if (!_dbFactory.Configured)
|
||||
{
|
||||
// if we aren't configured, we just keep looping since we can't query the db
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_dbFactory.Configured)
|
||||
{
|
||||
@@ -160,20 +162,14 @@ namespace Umbraco.Core.Runtime
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var db = GetDatabase();
|
||||
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that
|
||||
// we are still the maindom. An empty value might be better because then we won't have any orphan rows
|
||||
// if the app is terminated. Could that work?
|
||||
|
||||
if (!IsMainDomValue(_lockId))
|
||||
if (!IsMainDomValue(_lockId, db))
|
||||
{
|
||||
// we are no longer main dom, another one has come online, exit
|
||||
_mainDomChanging = true;
|
||||
@@ -183,38 +179,23 @@ namespace Umbraco.Core.Runtime
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, listening is canceled.");
|
||||
_hasError = true;
|
||||
return;
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during listening.");
|
||||
|
||||
// We need to keep on listening unless we've been notified by our own AppDomain to shutdown since
|
||||
// we don't want to shutdown resources controlled by MainDom inadvertently. We'll just keep listening otherwise.
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetDatabase()
|
||||
{
|
||||
if (_db.InTransaction)
|
||||
_db.AbortTransaction();
|
||||
_db.Dispose();
|
||||
_db = null;
|
||||
}
|
||||
|
||||
private IUmbracoDatabase GetDatabase()
|
||||
{
|
||||
if (_db != null)
|
||||
return _db;
|
||||
|
||||
_db = _dbFactory.CreateDatabase();
|
||||
return _db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for any existing MainDom to release so we can continue booting
|
||||
/// </summary>
|
||||
@@ -227,121 +208,131 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var db = GetDatabase();
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
while(true)
|
||||
while (true)
|
||||
{
|
||||
// poll very often, we need to take over as fast as we can
|
||||
Thread.Sleep(100);
|
||||
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
|
||||
Thread.Sleep(1000);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// the row
|
||||
var mainDomRows = db.Fetch<KeyValueDto>("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
|
||||
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
|
||||
{
|
||||
// the other main dom has updated our record
|
||||
// Or the other maindom shutdown super fast and just deleted the record
|
||||
// which indicates that we
|
||||
// can acquire it and it has shutdown.
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId))
|
||||
{
|
||||
// in this case, the prefixed ID is different which means
|
||||
// another new AppDomain has come online and is wanting to take over. In that case, we will not
|
||||
// acquire.
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
var acquired = TryAcquire(db, tempId, updatedTempId);
|
||||
if (acquired.HasValue)
|
||||
return acquired.Value;
|
||||
|
||||
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
|
||||
{
|
||||
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
|
||||
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
|
||||
// and it's just been left as an orphan row.
|
||||
// There's really know way of knowing unless we are constantly updating the row for the current maindom
|
||||
// which isn't ideal.
|
||||
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
// something is wrong, we cannot acquire, not much we can do
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
return AcquireWhenMaxWaitTimeElapsed(db);
|
||||
}
|
||||
}
|
||||
}, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId)
|
||||
{
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// the row
|
||||
var mainDomRows = db.Fetch<KeyValueDto>("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
|
||||
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
|
||||
{
|
||||
// the other main dom has updated our record
|
||||
// Or the other maindom shutdown super fast and just deleted the record
|
||||
// which indicates that we
|
||||
// can acquire it and it has shutdown.
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId, db);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId))
|
||||
{
|
||||
// in this case, the prefixed ID is different which means
|
||||
// another new AppDomain has come online and is wanting to take over. In that case, we will not
|
||||
// acquire.
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex as SqlException))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
transaction.Complete();
|
||||
}
|
||||
|
||||
return null; // continue
|
||||
}
|
||||
|
||||
private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db)
|
||||
{
|
||||
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
|
||||
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
|
||||
// and it's just been left as an orphan row.
|
||||
// There's really know way of knowing unless we are constantly updating the row for the current maindom
|
||||
// which isn't ideal.
|
||||
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
|
||||
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId, db);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex as SqlException))
|
||||
{
|
||||
// something is wrong, we cannot acquire, not much we can do
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts or updates the key/value row
|
||||
/// </summary>
|
||||
private RecordPersistenceType InsertLockRecord(string id)
|
||||
private RecordPersistenceType InsertLockRecord(string id, IUmbracoDatabase db)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
return db.InsertOrUpdate(new KeyValueDto
|
||||
{
|
||||
Key = MainDomKey,
|
||||
@@ -354,9 +345,8 @@ namespace Umbraco.Core.Runtime
|
||||
/// Checks if the DB row value is equals the value
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool IsMainDomValue(string val)
|
||||
private bool IsMainDomValue(string val, IUmbracoDatabase db)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
return db.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val",
|
||||
new { key = MainDomKey, val = val }) == 1;
|
||||
}
|
||||
@@ -366,7 +356,7 @@ namespace Umbraco.Core.Runtime
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222;
|
||||
private bool IsLockTimeoutException(SqlException sqlException) => sqlException?.Number == 1222;
|
||||
|
||||
#region IDisposable Support
|
||||
private bool _disposedValue = false; // To detect redundant calls
|
||||
@@ -385,11 +375,11 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
if (_dbFactory.Configured)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
@@ -402,24 +392,21 @@ namespace Umbraco.Core.Runtime
|
||||
if (_mainDomChanging)
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, updating row, new application is booting.");
|
||||
db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
var count = db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, deleting row, application is shutting down.");
|
||||
db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
var count = db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during dipsose.");
|
||||
_hasError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
ResetDatabase();
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
private IQuery<IContent> _queryNotTrashed;
|
||||
//TODO: The non-lazy object should be injected
|
||||
private readonly Lazy<PropertyValidationService> _propertyValidationService = new Lazy<PropertyValidationService>(() => new PropertyValidationService());
|
||||
|
||||
|
||||
|
||||
#region Constructors
|
||||
|
||||
@@ -879,7 +879,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types.");
|
||||
}
|
||||
|
||||
if(content.Name != null && content.Name.Length > 255)
|
||||
if (content.Name != null && content.Name.Length > 255)
|
||||
{
|
||||
throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
|
||||
}
|
||||
@@ -1247,7 +1247,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
if (culturesUnpublishing != null)
|
||||
{
|
||||
// This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
|
||||
|
||||
|
||||
var langs = string.Join(", ", allLangs
|
||||
.Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
|
||||
.Select(x => x.CultureName));
|
||||
@@ -1256,7 +1256,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
if (publishResult == null)
|
||||
throw new PanicException("publishResult == null - should not happen");
|
||||
|
||||
switch(publishResult.Result)
|
||||
switch (publishResult.Result)
|
||||
{
|
||||
case PublishResultType.FailedPublishMandatoryCultureMissing:
|
||||
//occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
|
||||
@@ -1270,7 +1270,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)");
|
||||
return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Audit(AuditType.Unpublish, userId, content.Id);
|
||||
@@ -1290,7 +1290,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
changeType = TreeChangeTypes.RefreshBranch; // whole branch
|
||||
else if (isNew == false && previouslyPublished)
|
||||
changeType = TreeChangeTypes.RefreshNode; // single node
|
||||
|
||||
|
||||
|
||||
// invalidate the node/branch
|
||||
if (!branchOne) // for branches, handled by SaveAndPublishBranch
|
||||
@@ -1364,24 +1364,90 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PublishResult> PerformScheduledPublish(DateTime date)
|
||||
=> PerformScheduledPublishInternal(date).ToList();
|
||||
|
||||
// beware! this method yields results, so the returned IEnumerable *must* be
|
||||
// enumerated for anything to happen - dangerous, so private + exposed via
|
||||
// the public method above, which forces ToList().
|
||||
private IEnumerable<PublishResult> PerformScheduledPublishInternal(DateTime date)
|
||||
{
|
||||
var allLangs = new Lazy<List<ILanguage>>(() => _languageRepository.GetMany().ToList());
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
var results = new List<PublishResult>();
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
|
||||
PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void PerformScheduledPublishingExpiration(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> allLangs)
|
||||
{
|
||||
using var scope = ScopeProvider.CreateScope();
|
||||
|
||||
// do a fast read without any locks since this executes often to see if we even need to proceed
|
||||
if (_documentRepository.HasContentForExpiration(date))
|
||||
{
|
||||
// now take a write lock since we'll be updating
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
var allLangs = _languageRepository.GetMany().ToList();
|
||||
foreach (var d in _documentRepository.GetContentForExpiration(date))
|
||||
{
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date)
|
||||
.Select(x => x.Culture)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
continue; //shouldn't happen but no point in processing this document if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
{
|
||||
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var c in pendingCultures)
|
||||
{
|
||||
//Clear this schedule for this culture
|
||||
d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date);
|
||||
//set the culture to be published
|
||||
d.UnpublishCulture(c);
|
||||
}
|
||||
|
||||
var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
results.Add(result);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Expire, date);
|
||||
var result = Unpublish(d, userId: d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
results.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
_documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private void PerformScheduledPublishingRelease(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> allLangs)
|
||||
{
|
||||
using var scope = ScopeProvider.CreateScope();
|
||||
|
||||
// do a fast read without any locks since this executes often to see if we even need to proceed
|
||||
if (_documentRepository.HasContentForRelease(date))
|
||||
{
|
||||
// now take a write lock since we'll be updating
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
foreach (var d in _documentRepository.GetContentForRelease(date))
|
||||
{
|
||||
PublishResult result;
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
@@ -1391,11 +1457,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
.ToList();
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
break; //shouldn't happen but no point in continuing if there's nothing there
|
||||
continue; //shouldn't happen but no point in processing this document if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
|
||||
{
|
||||
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
|
||||
continue; // this document is canceled move next
|
||||
}
|
||||
|
||||
var publishing = true;
|
||||
foreach (var culture in pendingCultures)
|
||||
@@ -1407,94 +1476,51 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
//publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
|
||||
Property[] invalidProperties = null;
|
||||
var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture));
|
||||
var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture));
|
||||
var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
|
||||
if (invalidProperties != null && invalidProperties.Length > 0)
|
||||
Logger.Warn<ContentService>("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
|
||||
d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias)));
|
||||
|
||||
publishing &= tryPublish; //set the culture to be published
|
||||
if (!publishing) break; // no point continuing
|
||||
if (!publishing) continue; // move to next document
|
||||
}
|
||||
|
||||
PublishResult result;
|
||||
|
||||
if (d.Trashed)
|
||||
result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
|
||||
else if (!publishing)
|
||||
result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
|
||||
else
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId);
|
||||
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
|
||||
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
|
||||
yield return result;
|
||||
results.Add(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Release, date);
|
||||
|
||||
result = d.Trashed
|
||||
var result = d.Trashed
|
||||
? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d)
|
||||
: SaveAndPublish(d, userId: d.WriterId);
|
||||
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
|
||||
yield return result;
|
||||
results.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var d in _documentRepository.GetContentForExpiration(date))
|
||||
{
|
||||
PublishResult result;
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date)
|
||||
.Select(x => x.Culture)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
_documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
break; //shouldn't happen but no point in continuing if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
|
||||
|
||||
foreach (var c in pendingCultures)
|
||||
{
|
||||
//Clear this schedule for this culture
|
||||
d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date);
|
||||
//set the culture to be published
|
||||
d.UnpublishCulture(c);
|
||||
}
|
||||
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
yield return result;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Expire, date);
|
||||
result = Unpublish(d, userId: d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
yield return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
_documentRepository.ClearSchedule(date);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
// utility 'PublishCultures' func used by SaveAndPublishBranch
|
||||
@@ -2650,7 +2676,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// there will be nothing to publish/unpublish.
|
||||
return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// missing mandatory culture = cannot be published
|
||||
var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode);
|
||||
@@ -3165,6 +3191,6 @@ namespace Umbraco.Core.Services.Implement
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,6 +511,16 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
// delete content
|
||||
DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id));
|
||||
|
||||
// Next find all other document types that have a reference to this content type
|
||||
var referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes.Any(p=>p.Id.Value==item.Id));
|
||||
foreach (var reference in referenceToAllowedContentTypes)
|
||||
{
|
||||
reference.AllowedContentTypes = reference.AllowedContentTypes.Where(p => p.Id.Value != item.Id);
|
||||
var changedRef = new List<ContentTypeChange<TItem>>() { new ContentTypeChange<TItem>(reference, ContentTypeChangeTypes.RefreshMain) };
|
||||
// Fire change event
|
||||
OnChanged(scope, changedRef.ToEventArgs());
|
||||
}
|
||||
|
||||
// finally delete the content type
|
||||
// - recursively deletes all descendants
|
||||
@@ -518,7 +528,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// (contents of any descendant type have been deleted but
|
||||
// contents of any composed (impacted) type remain but
|
||||
// need to have their property data cleared)
|
||||
Repository.Delete(item);
|
||||
Repository.Delete(item);
|
||||
|
||||
//...
|
||||
var changes = descendantsAndSelf.Select(x => new ContentTypeChange<TItem>(x, ContentTypeChangeTypes.Remove))
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Collections;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
@@ -15,19 +13,73 @@ namespace Umbraco.Core.Services
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILocalizedTextService _textService;
|
||||
|
||||
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService)
|
||||
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_textService = textService;
|
||||
}
|
||||
|
||||
//TODO: Remove this method in favor of the overload specifying all dependencies
|
||||
public PropertyValidationService()
|
||||
: this(Current.PropertyEditors, Current.Services.DataTypeService)
|
||||
: this(Current.PropertyEditors, Current.Services.DataTypeService, Current.Services.TextService)
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> ValidatePropertyValue(
|
||||
PropertyType propertyType,
|
||||
object postedValue)
|
||||
{
|
||||
if (propertyType is null) throw new ArgumentNullException(nameof(propertyType));
|
||||
var dataType = _dataTypeService.GetDataType(propertyType.DataTypeId);
|
||||
if (dataType == null) throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId);
|
||||
|
||||
var editor = _propertyEditors[propertyType.PropertyEditorAlias];
|
||||
if (editor == null) throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias);
|
||||
|
||||
return ValidatePropertyValue(_textService, editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage);
|
||||
}
|
||||
|
||||
internal static IEnumerable<ValidationResult> ValidatePropertyValue(
|
||||
ILocalizedTextService textService,
|
||||
IDataEditor editor,
|
||||
IDataType dataType,
|
||||
object postedValue,
|
||||
bool isRequired,
|
||||
string validationRegExp,
|
||||
string isRequiredMessage,
|
||||
string validationRegExpMessage)
|
||||
{
|
||||
// Retrieve default messages used for required and regex validatation. We'll replace these
|
||||
// if set with custom ones if they've been provided for a given property.
|
||||
var requiredDefaultMessages = new[]
|
||||
{
|
||||
textService.Localize("validation", "invalidNull"),
|
||||
textService.Localize("validation", "invalidEmpty")
|
||||
};
|
||||
var formatDefaultMessages = new[]
|
||||
{
|
||||
textService.Localize("validation", "invalidPattern"),
|
||||
};
|
||||
|
||||
var valueEditor = editor.GetValueEditor(dataType.Configuration);
|
||||
foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp))
|
||||
{
|
||||
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
|
||||
if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
validationResult.ErrorMessage = isRequiredMessage;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
validationResult.ErrorMessage = validationRegExpMessage;
|
||||
}
|
||||
yield return validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the content item's properties pass validation rules
|
||||
/// </summary>
|
||||
|
||||
@@ -132,15 +132,27 @@
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\ContentTypeDto80.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\PropertyDataDto80.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\PropertyTypeDto80.cs" />
|
||||
<Compile Include="Models\Blocks\BlockEditorDataConverter.cs" />
|
||||
<Compile Include="Models\Blocks\BlockEditorData.cs" />
|
||||
<Compile Include="Models\Blocks\BlockItemData.cs" />
|
||||
<Compile Include="Models\Blocks\BlockListLayoutItem.cs" />
|
||||
<Compile Include="Models\Blocks\BlockListEditorDataConverter.cs" />
|
||||
<Compile Include="Models\Blocks\BlockValue.cs" />
|
||||
<Compile Include="Models\Blocks\ContentAndSettingsReference.cs" />
|
||||
<Compile Include="Models\Blocks\IBlockReference.cs" />
|
||||
<Compile Include="Models\ContentDataIntegrityReport.cs" />
|
||||
<Compile Include="Models\ContentDataIntegrityReportEntry.cs" />
|
||||
<Compile Include="Models\ContentDataIntegrityReportOptions.cs" />
|
||||
<Compile Include="Models\InstallLog.cs" />
|
||||
<Compile Include="Models\PublishedContent\PublishedContentTypeExtensions.cs" />
|
||||
<Compile Include="Models\RelationTypeExtensions.cs" />
|
||||
<Compile Include="Persistence\Repositories\IInstallationRepository.cs" />
|
||||
<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\BlockListModel.cs" />
|
||||
<Compile Include="Models\UpgradeResult.cs" />
|
||||
<Compile Include="Persistence\Repositories\Implement\UpgradeCheckRepository.cs" />
|
||||
<Compile Include="Persistence\Repositories\IUpgradeCheckRepository.cs" />
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Examine;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
|
||||
namespace Umbraco.Examine
|
||||
{
|
||||
@@ -12,12 +14,20 @@ namespace Umbraco.Examine
|
||||
/// </summary>
|
||||
public class IndexRebuilder
|
||||
{
|
||||
private readonly IProfilingLogger _logger;
|
||||
private readonly IEnumerable<IIndexPopulator> _populators;
|
||||
public IExamineManager ExamineManager { get; }
|
||||
|
||||
[Obsolete("Use constructor with all dependencies")]
|
||||
public IndexRebuilder(IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
|
||||
: this(Current.ProfilingLogger, examineManager, populators)
|
||||
{
|
||||
}
|
||||
|
||||
public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
|
||||
{
|
||||
_populators = populators;
|
||||
_logger = logger;
|
||||
ExamineManager = examineManager;
|
||||
}
|
||||
|
||||
@@ -50,8 +60,18 @@ namespace Umbraco.Examine
|
||||
index.CreateIndex(); // clear the index
|
||||
}
|
||||
|
||||
//run the populators in parallel against all indexes
|
||||
Parallel.ForEach(_populators, populator => populator.Populate(indexes));
|
||||
// run each populator over the indexes
|
||||
foreach(var populator in _populators)
|
||||
{
|
||||
try
|
||||
{
|
||||
populator.Populate(indexes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error<IndexRebuilder>(e, "Index populating failed for populator {Populator}", populator.GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -542,8 +542,10 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
if (modelInfos.TryGetValue(typeName, out var modelInfo))
|
||||
throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\".");
|
||||
|
||||
// fixme use Core's ReflectionUtilities.EmitCtor !!
|
||||
// TODO: use Core's ReflectionUtilities.EmitCtor !!
|
||||
// Yes .. DynamicMethod is uber slow
|
||||
// TODO: But perhaps https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 is better still?
|
||||
// See CtorInvokeBenchmarks
|
||||
var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true);
|
||||
var gen = meth.GetILGenerator();
|
||||
gen.Emit(OpCodes.Ldarg_0);
|
||||
|
||||
371
src/Umbraco.TestData/LoadTestController.cs
Normal file
371
src/Umbraco.TestData/LoadTestController.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Models;
|
||||
using System.Web;
|
||||
using System.Web.Hosting;
|
||||
using System.Web.Routing;
|
||||
using System.Diagnostics;
|
||||
using Umbraco.Core.Composing;
|
||||
using System.Configuration;
|
||||
|
||||
// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting
|
||||
|
||||
namespace Umbraco.TestData
|
||||
{
|
||||
public class LoadTestController : Controller
|
||||
{
|
||||
public LoadTestController(ServiceContext serviceContext)
|
||||
{
|
||||
_serviceContext = serviceContext;
|
||||
}
|
||||
|
||||
private static readonly Random _random = new Random();
|
||||
private static readonly object _locko = new object();
|
||||
|
||||
private static volatile int _containerId = -1;
|
||||
|
||||
private const string _containerAlias = "LoadTestContainer";
|
||||
private const string _contentAlias = "LoadTestContent";
|
||||
private const int _textboxDefinitionId = -88;
|
||||
private const int _maxCreate = 1000;
|
||||
|
||||
private static readonly string HeadHtml = @"<html>
|
||||
<head>
|
||||
<title>LoadTest</title>
|
||||
<style>
|
||||
body { font-family: arial; }
|
||||
a,a:visited { color: blue; }
|
||||
h1 { margin: 0; padding: 0; font-size: 120%; font-weight: bold; }
|
||||
h1 a { text-decoration: none; }
|
||||
div.block { margin: 20px 0; }
|
||||
ul { margin:0; }
|
||||
div.ver { font-size: 80%; }
|
||||
div.head { padding:0 0 10px 0; margin: 0 0 20px 0; border-bottom: 1px solid #cccccc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class=""head"">
|
||||
<h1><a href=""/LoadTest"">LoadTest</a></h1>
|
||||
<div class=""ver"">" + System.Configuration.ConfigurationManager.AppSettings["umbracoConfigurationStatus"] + @"</div>
|
||||
</div>
|
||||
";
|
||||
|
||||
private const string FootHtml = @"</body>
|
||||
</html>";
|
||||
|
||||
private static readonly string _containerTemplateText = @"
|
||||
@inherits Umbraco.Web.Mvc.UmbracoViewPage
|
||||
@{
|
||||
Layout = null;
|
||||
var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault();
|
||||
var contents = container.Children().ToArray();
|
||||
var groups = contents.GroupBy(x => x.Value<string>(""origin""));
|
||||
var id = contents.Length > 0 ? contents[0].Id : -1;
|
||||
var wurl = Request.QueryString[""u""] == ""1"";
|
||||
var missing = contents.Length > 0 && contents[contents.Length - 1].Id - contents[0].Id >= contents.Length;
|
||||
}
|
||||
" + HeadHtml + @"
|
||||
<div class=""block"">
|
||||
<span @Html.Raw(missing ? ""style=\""color:red;\"""" : """")>@contents.Length items</span>
|
||||
<ul>
|
||||
@foreach (var group in groups)
|
||||
{
|
||||
<li>@group.Key: @group.Count()</li>
|
||||
}
|
||||
</ul></div>
|
||||
<div class=""block"">
|
||||
@foreach (var content in contents)
|
||||
{
|
||||
while (content.Id > id)
|
||||
{
|
||||
<div style=""color:red;"">@id :: MISSING</div>
|
||||
id++;
|
||||
}
|
||||
if (wurl)
|
||||
{
|
||||
<div>@content.Id :: @content.Name :: @content.Url</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>@content.Id :: @content.Name</div>
|
||||
} id++;
|
||||
}
|
||||
</div>
|
||||
" + FootHtml;
|
||||
private readonly ServiceContext _serviceContext;
|
||||
|
||||
private ActionResult ContentHtml(string s)
|
||||
{
|
||||
return Content(HeadHtml + s + FootHtml);
|
||||
}
|
||||
|
||||
public ActionResult Index()
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
var html = @"Welcome. You can:
|
||||
<ul>
|
||||
<li><a href=""/LoadTestContainer"">List existing contents</a> (u:url)</li>
|
||||
<li><a href=""/LoadTest/Create?o=browser"">Create a content</a> (o:origin, r:restart, n:number)</li>
|
||||
<li><a href=""/LoadTest/Clear"">Clear all contents</a></li>
|
||||
<li><a href=""/LoadTest/Domains"">List the current domains in w3wp.exe</a></li>
|
||||
<li><a href=""/LoadTest/Restart"">Restart the current AppDomain</a></li>
|
||||
<li><a href=""/LoadTest/Recycle"">Recycle the AppPool</a></li>
|
||||
<li><a href=""/LoadTest/Die"">Cause w3wp.exe to die</a></li>
|
||||
</ul>
|
||||
";
|
||||
|
||||
return ContentHtml(html);
|
||||
}
|
||||
|
||||
private ActionResult EnsureInitialize()
|
||||
{
|
||||
if (_containerId > 0) return null;
|
||||
|
||||
lock (_locko)
|
||||
{
|
||||
if (_containerId > 0) return null;
|
||||
|
||||
var contentTypeService = _serviceContext.ContentTypeService;
|
||||
var contentType = contentTypeService.Get(_contentAlias);
|
||||
if (contentType == null)
|
||||
return ContentHtml("Not installed, first you must <a href=\"/LoadTest/Install\">install</a>.");
|
||||
|
||||
var containerType = contentTypeService.Get(_containerAlias);
|
||||
if (containerType == null)
|
||||
return ContentHtml("Panic! Container type is missing.");
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
var container = contentService.GetPagedOfType(containerType.Id, 0, 100, out _, null).FirstOrDefault();
|
||||
if (container == null)
|
||||
return ContentHtml("Panic! Container is missing.");
|
||||
|
||||
_containerId = container.Id;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult Install()
|
||||
{
|
||||
var dataTypeService = _serviceContext.DataTypeService;
|
||||
|
||||
//var dataType = dataTypeService.GetAll(Constants.DataTypes.DefaultContentListView);
|
||||
|
||||
|
||||
//if (!dict.ContainsKey("pageSize")) dict["pageSize"] = new PreValue("10");
|
||||
//dict["pageSize"].Value = "200";
|
||||
//dataTypeService.SavePreValues(dataType, dict);
|
||||
|
||||
var contentTypeService = _serviceContext.ContentTypeService;
|
||||
|
||||
var contentType = new ContentType(-1)
|
||||
{
|
||||
Alias = _contentAlias,
|
||||
Name = "LoadTest Content",
|
||||
Description = "Content for LoadTest",
|
||||
Icon = "icon-document"
|
||||
};
|
||||
var def = _serviceContext.DataTypeService.GetDataType(_textboxDefinitionId);
|
||||
contentType.AddPropertyType(new PropertyType(def)
|
||||
{
|
||||
Name = "Origin",
|
||||
Alias = "origin",
|
||||
Description = "The origin of the content.",
|
||||
});
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var containerTemplate = ImportTemplate(_serviceContext,
|
||||
"LoadTestContainer", "LoadTestContainer", _containerTemplateText);
|
||||
|
||||
var containerType = new ContentType(-1)
|
||||
{
|
||||
Alias = _containerAlias,
|
||||
Name = "LoadTest Container",
|
||||
Description = "Container for LoadTest content",
|
||||
Icon = "icon-document",
|
||||
AllowedAsRoot = true,
|
||||
IsContainer = true
|
||||
};
|
||||
containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[]
|
||||
{
|
||||
new ContentTypeSort(new Lazy<int>(() => contentType.Id), 0, contentType.Alias),
|
||||
});
|
||||
containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate });
|
||||
containerType.SetDefaultTemplate(containerTemplate);
|
||||
contentTypeService.Save(containerType);
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
var content = contentService.Create("LoadTestContainer", -1, _containerAlias);
|
||||
contentService.SaveAndPublish(content);
|
||||
|
||||
return ContentHtml("Installed.");
|
||||
}
|
||||
|
||||
public ActionResult Create(int n = 1, int r = 0, string o = null)
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
if (r < 0) r = 0;
|
||||
if (r > 100) r = 100;
|
||||
var restart = GetRandom(0, 100) > (100 - r);
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
|
||||
if (n < 1) n = 1;
|
||||
if (n > _maxCreate) n = _maxCreate;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o;
|
||||
var content = contentService.Create(name, _containerId, _contentAlias);
|
||||
content.SetValue("origin", o);
|
||||
contentService.SaveAndPublish(content);
|
||||
}
|
||||
|
||||
if (restart)
|
||||
DoRestart();
|
||||
|
||||
return ContentHtml("Created " + n + " content"
|
||||
+ (restart ? ", and restarted" : "")
|
||||
+ ".");
|
||||
}
|
||||
|
||||
private int GetRandom(int minValue, int maxValue)
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
return _random.Next(minValue, maxValue);
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult Clear()
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
var contentType = _serviceContext.ContentTypeService.Get(_contentAlias);
|
||||
_serviceContext.ContentService.DeleteOfType(contentType.Id);
|
||||
|
||||
return ContentHtml("Cleared.");
|
||||
}
|
||||
|
||||
private void DoRestart()
|
||||
{
|
||||
HttpContext.User = null;
|
||||
System.Web.HttpContext.Current.User = null;
|
||||
Thread.CurrentPrincipal = null;
|
||||
HttpRuntime.UnloadAppDomain();
|
||||
}
|
||||
|
||||
public ActionResult Restart()
|
||||
{
|
||||
DoRestart();
|
||||
|
||||
return ContentHtml("Restarted.");
|
||||
}
|
||||
|
||||
public ActionResult Die()
|
||||
{
|
||||
var timer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
throw new Exception("die!");
|
||||
});
|
||||
timer.Change(100, 0);
|
||||
|
||||
return ContentHtml("Dying.");
|
||||
}
|
||||
|
||||
public ActionResult Domains()
|
||||
{
|
||||
var currentDomain = AppDomain.CurrentDomain;
|
||||
var currentName = currentDomain.FriendlyName;
|
||||
var pos = currentName.IndexOf('-');
|
||||
if (pos > 0) currentName = currentName.Substring(0, pos);
|
||||
|
||||
var text = new System.Text.StringBuilder();
|
||||
text.Append("<div class=\"block\">Process ID: " + Process.GetCurrentProcess().Id + "</div>");
|
||||
text.Append("<div class=\"block\">");
|
||||
text.Append("<div>IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "</div>");
|
||||
text.Append("<div>App ID: " + currentName + "</div>");
|
||||
//text.Append("<div>AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "</div>");
|
||||
text.Append("</div>");
|
||||
|
||||
text.Append("<div class=\"block\">Domains:<ul>");
|
||||
text.Append("<li>Not implemented.</li>");
|
||||
/*
|
||||
foreach (var domain in Zbu.WebManagement.AppDomainHelper.GetAppDomains().OrderBy(x => x.Id))
|
||||
{
|
||||
var name = domain.FriendlyName;
|
||||
pos = name.IndexOf('-');
|
||||
if (pos > 0) name = name.Substring(0, pos);
|
||||
text.Append("<li style=\""
|
||||
+ (name != currentName ? "color: #cccccc;" : "")
|
||||
//+ (domain.Id == currentDomain.Id ? "" : "")
|
||||
+ "\">"
|
||||
+"[" + domain.Id + "] " + name
|
||||
+ (domain.IsDefaultAppDomain() ? " (default)" : "")
|
||||
+ (domain.Id == currentDomain.Id ? " (current)" : "")
|
||||
+ "</li>");
|
||||
}
|
||||
*/
|
||||
text.Append("</ul></div>");
|
||||
|
||||
return ContentHtml(text.ToString());
|
||||
}
|
||||
|
||||
public ActionResult Recycle()
|
||||
{
|
||||
return ContentHtml("Not implemented—please use IIS console.");
|
||||
}
|
||||
|
||||
private static Template ImportTemplate(ServiceContext svces, string name, string alias, string text, ITemplate master = null)
|
||||
{
|
||||
var t = new Template(name, alias) { Content = text };
|
||||
if (master != null)
|
||||
t.SetMasterTemplate(master);
|
||||
svces.FileService.SaveTemplate(t);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestComponent : IComponent
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
|
||||
return;
|
||||
|
||||
RouteTable.Routes.MapRoute(
|
||||
name: "LoadTest",
|
||||
url: "LoadTest/{action}",
|
||||
defaults: new
|
||||
{
|
||||
controller = "LoadTest",
|
||||
action = "Index"
|
||||
},
|
||||
namespaces: new[] { "Umbraco.TestData" }
|
||||
);
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class TestComposer : ComponentComposer<TestComponent>, IUserComposer
|
||||
{
|
||||
public override void Compose(Composition composition)
|
||||
{
|
||||
base.Compose(composition);
|
||||
|
||||
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
|
||||
return;
|
||||
|
||||
composition.Register(typeof(LoadTestController), Lifetime.Request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="LoadTestController.cs" />
|
||||
<Compile Include="SegmentTestController.cs" />
|
||||
<Compile Include="UmbracoTestDataController.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
context('User Groups', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create member group', () => {
|
||||
const name = "Test Group";
|
||||
|
||||
cy.umbracoEnsureMemberGroupNameNotExists(name);
|
||||
|
||||
cy.umbracoSection('member');
|
||||
cy.get('li .umb-tree-root:contains("Members")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("member", ["Member Groups"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
// Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureMemberGroupNameNotExists(name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Members', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
|
||||
});
|
||||
|
||||
it('Create member', () => {
|
||||
const name = "Alice Bobson";
|
||||
const email = "alice-bobson@acceptancetest.umbraco";
|
||||
const password = "$AUlkoF*St0kgPiyyVEk5iU5JWdN*F7&@OSl5Y4pOofnidfifkBj5Ns2ONv%FzsTl36V1E924Gw97zcuSeT7UwK&qb5l&O9h!d!w";
|
||||
|
||||
cy.umbracoEnsureMemberEmailNotExists(email);
|
||||
cy.umbracoSection('member');
|
||||
cy.get('li .umb-tree-root:contains("Members")').should("be.visible");
|
||||
|
||||
cy.umbracoTreeItem("member", ["Members"]).rightclick();
|
||||
|
||||
cy.umbracoContextMenuAction("action-create").click();
|
||||
cy.get('.menu-label').first().click();
|
||||
|
||||
//Type name
|
||||
cy.umbracoEditorHeaderName(name);
|
||||
|
||||
cy.get('input#_umb_login').clear().type(email);
|
||||
cy.get('input#_umb_email').clear().type(email);
|
||||
cy.get('input#password').clear().type(password);
|
||||
cy.get('input#confirmPassword').clear().type(password);
|
||||
|
||||
// Save
|
||||
cy.get('.btn-success').click();
|
||||
|
||||
//Assert
|
||||
cy.umbracoSuccessNotification().should('be.visible');
|
||||
|
||||
//Clean up
|
||||
cy.umbracoEnsureMemberEmailNotExists(email);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -34,7 +34,7 @@ context('Document Types', () => {
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
@@ -34,7 +34,7 @@ context('Media Types', () => {
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
@@ -32,7 +32,7 @@ context('Member Types', () => {
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
context('User Groups', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -6,8 +6,8 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.2",
|
||||
"ncp": "^2.0.0",
|
||||
"cypress": "^4.6.0",
|
||||
"umbraco-cypress-testhelpers": "1.0.0-beta-39"
|
||||
"cypress": "^4.9.0",
|
||||
"umbraco-cypress-testhelpers": "1.0.0-beta-44"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "^3.9.2"
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace Umbraco.Tests.Benchmarks
|
||||
// - it's faster to get+invoke the ctor
|
||||
// - emitting the ctor is unless if invoked only 1
|
||||
|
||||
// TODO: Check out https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 ?
|
||||
|
||||
//[Config(typeof(Config))]
|
||||
[MemoryDiagnoser]
|
||||
public class CtorInvokeBenchmarks
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace Umbraco.Tests.Cache.PublishedCache
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var type = new AutoPublishedContentType(22, "myType", new PublishedPropertyType[] { });
|
||||
var image = new AutoPublishedContentType(23, "Image", new PublishedPropertyType[] { });
|
||||
var testMediaType = new AutoPublishedContentType(24, "TestMediaType", new PublishedPropertyType[] { });
|
||||
var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "myType", new PublishedPropertyType[] { });
|
||||
var image = new AutoPublishedContentType(Guid.NewGuid(), 23, "Image", new PublishedPropertyType[] { });
|
||||
var testMediaType = new AutoPublishedContentType(Guid.NewGuid(), 24, "TestMediaType", new PublishedPropertyType[] { });
|
||||
_mediaTypes = new Dictionary<string, PublishedContentType>
|
||||
{
|
||||
{ type.Alias, type },
|
||||
|
||||
@@ -268,7 +268,7 @@ AnotherContentFinder
|
||||
public void GetDataEditors()
|
||||
{
|
||||
var types = _typeLoader.GetDataEditors();
|
||||
Assert.AreEqual(38, types.Count());
|
||||
Assert.AreEqual(39, types.Count());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,7 +14,7 @@ using Umbraco.Web.Routing;
|
||||
|
||||
namespace Umbraco.Tests.LegacyXmlPublishedCache
|
||||
{
|
||||
internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache
|
||||
internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache2
|
||||
{
|
||||
private readonly IAppCache _appCache;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
@@ -532,15 +532,11 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
|
||||
|
||||
#region Content types
|
||||
|
||||
public override IPublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Content, id);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Content, id);
|
||||
|
||||
public override IPublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Content, alias);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Content, alias);
|
||||
|
||||
public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Content, key);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
|
||||
/// <remarks>
|
||||
/// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly.
|
||||
/// </remarks>
|
||||
internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache
|
||||
internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache2
|
||||
{
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IUserService _userService;
|
||||
@@ -612,15 +612,11 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
|
||||
|
||||
#region Content types
|
||||
|
||||
public override IPublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Media, id);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Media, id);
|
||||
|
||||
public override IPublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Media, alias);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Media, alias);
|
||||
|
||||
public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Media, key);
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
|
||||
{
|
||||
|
||||
@@ -442,7 +442,10 @@ namespace Umbraco.Tests.Models
|
||||
[Test]
|
||||
public void ContentPublishValuesWithMixedPropertyTypeVariations()
|
||||
{
|
||||
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(
|
||||
Current.Factory.GetInstance<PropertyEditorCollection>(),
|
||||
Current.Factory.GetInstance<ServiceContext>().DataTypeService,
|
||||
Current.Factory.GetInstance<ServiceContext>().TextService);
|
||||
const string langFr = "fr-FR";
|
||||
|
||||
// content type varies by Culture
|
||||
@@ -574,7 +577,10 @@ namespace Umbraco.Tests.Models
|
||||
prop.SetValue("a");
|
||||
Assert.AreEqual("a", prop.GetValue());
|
||||
Assert.IsNull(prop.GetValue(published: true));
|
||||
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(
|
||||
Current.Factory.GetInstance<PropertyEditorCollection>(),
|
||||
Current.Factory.GetInstance<ServiceContext>().DataTypeService,
|
||||
Current.Factory.GetInstance<ServiceContext>().TextService);
|
||||
|
||||
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));
|
||||
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.Blocks;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using Umbraco.Web.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
|
||||
namespace Umbraco.Tests.PropertyEditors
|
||||
{
|
||||
[TestFixture]
|
||||
public class BlockListPropertyValueConverterTests
|
||||
{
|
||||
private readonly Guid ContentKey1 = Guid.NewGuid();
|
||||
private readonly Guid ContentKey2 = Guid.NewGuid();
|
||||
private const string ContentAlias1 = "Test1";
|
||||
private const string ContentAlias2 = "Test2";
|
||||
private readonly Guid SettingKey1 = Guid.NewGuid();
|
||||
private readonly Guid SettingKey2 = Guid.NewGuid();
|
||||
private const string SettingAlias1 = "Setting1";
|
||||
private const string SettingAlias2 = "Setting2";
|
||||
|
||||
/// <summary>
|
||||
/// Setup mocks for IPublishedSnapshotAccessor
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IPublishedSnapshotAccessor GetPublishedSnapshotAccessor()
|
||||
{
|
||||
var test1ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == ContentKey1
|
||||
&& x.Alias == ContentAlias1);
|
||||
var test2ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == ContentKey2
|
||||
&& x.Alias == ContentAlias2);
|
||||
var test3ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == SettingKey1
|
||||
&& x.Alias == SettingAlias1);
|
||||
var test4ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == SettingKey2
|
||||
&& x.Alias == SettingAlias2);
|
||||
var contentCache = new Mock<IPublishedContentCache2>();
|
||||
contentCache.Setup(x => x.GetContentType(ContentKey1)).Returns(test1ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(ContentKey2)).Returns(test2ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(SettingKey1)).Returns(test3ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(SettingKey2)).Returns(test4ContentType);
|
||||
var publishedSnapshot = Mock.Of<IPublishedSnapshot>(x => x.Content == contentCache.Object);
|
||||
var publishedSnapshotAccessor = Mock.Of<IPublishedSnapshotAccessor>(x => x.PublishedSnapshot == publishedSnapshot);
|
||||
return publishedSnapshotAccessor;
|
||||
}
|
||||
|
||||
private BlockListPropertyValueConverter CreateConverter()
|
||||
{
|
||||
var publishedSnapshotAccessor = GetPublishedSnapshotAccessor();
|
||||
var publishedModelFactory = new NoopPublishedModelFactory();
|
||||
var editor = new BlockListPropertyValueConverter(
|
||||
Mock.Of<IProfilingLogger>(),
|
||||
new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory));
|
||||
return editor;
|
||||
}
|
||||
|
||||
private BlockListConfiguration ConfigForMany() => new BlockListConfiguration
|
||||
{
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey1,
|
||||
SettingsElementTypeKey = SettingKey2
|
||||
},
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey2,
|
||||
SettingsElementTypeKey = SettingKey1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private BlockListConfiguration ConfigForSingle() => new BlockListConfiguration
|
||||
{
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private IPublishedPropertyType GetPropertyType(BlockListConfiguration config)
|
||||
{
|
||||
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
|
||||
var propertyType = Mock.Of<IPublishedPropertyType>(x =>
|
||||
x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList
|
||||
&& x.DataType == dataType);
|
||||
return propertyType;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Is_Converter_For()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
Assert.IsTrue(editor.IsConverter(Mock.Of<IPublishedPropertyType>(x => x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList)));
|
||||
Assert.IsFalse(editor.IsConverter(Mock.Of<IPublishedPropertyType>(x => x.EditorAlias == Constants.PropertyEditors.Aliases.NestedContent)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Value_Type_Multiple()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
|
||||
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
|
||||
var propType = Mock.Of<IPublishedPropertyType>(x => x.DataType == dataType);
|
||||
|
||||
var valueType = editor.GetPropertyValueType(propType);
|
||||
|
||||
// the result is always block list model
|
||||
Assert.AreEqual(typeof(BlockListModel), valueType);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Value_Type_Single()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForSingle();
|
||||
|
||||
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
|
||||
var propType = Mock.Of<IPublishedPropertyType>(x => x.DataType == dataType);
|
||||
|
||||
var valueType = editor.GetPropertyValueType(propType);
|
||||
|
||||
// the result is always block list model
|
||||
Assert.AreEqual(typeof(BlockListModel), valueType);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Convert_Null_Empty()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
string json = null;
|
||||
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());
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Convert_Valid_Empty_Json()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = "{}";
|
||||
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());
|
||||
|
||||
json = @"{
|
||||
layout: {},
|
||||
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());
|
||||
|
||||
// Even though there is a layout, there is no data, so the conversion will result in zero elements in total
|
||||
json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: []
|
||||
}";
|
||||
|
||||
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());
|
||||
|
||||
// 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 = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
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());
|
||||
|
||||
// Everthing is ok except the udi reference in the layout doesn't match the data so it will be empty
|
||||
json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'key': '1304E1DD-0000-4396-84FE-8A399231CB3D'
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Convert_Valid_Json()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
}";
|
||||
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(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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Data_From_Layout_Item()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settingsUdi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentUdi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
|
||||
'settingsUdi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/E05A034704424AB3A520E048E6197E79'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A'
|
||||
}
|
||||
],
|
||||
settingsData: [
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey1 + @"',
|
||||
'udi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9'
|
||||
}
|
||||
],
|
||||
}";
|
||||
|
||||
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());
|
||||
|
||||
var item0 = converted.Layout.ElementAt(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);
|
||||
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);
|
||||
Assert.AreEqual("Setting1", item1.Settings.ContentType.Alias);
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Data_Item_Removed_If_Removed_From_Config()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
|
||||
// The data below expects that ContentKey1 + ContentKey2 + SettingsKey1 + SettingsKey2 exist but only ContentKey2 exists so
|
||||
// the data should all be filtered.
|
||||
var config = new BlockListConfiguration
|
||||
{
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey2,
|
||||
SettingsElementTypeKey = null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settingsUdi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentUdi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
|
||||
'settingsUdi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/E05A034704424AB3A520E048E6197E79'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A'
|
||||
}
|
||||
],
|
||||
settingsData: [
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey1 + @"',
|
||||
'udi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9'
|
||||
}
|
||||
],
|
||||
}";
|
||||
|
||||
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());
|
||||
|
||||
var item0 = converted.Layout.ElementAt(0);
|
||||
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item0.Content.Key);
|
||||
Assert.AreEqual("Test2", item0.Content.ContentType.Alias);
|
||||
Assert.IsNull(item0.Settings);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Web.Compose;
|
||||
|
||||
namespace Umbraco.Tests.PropertyEditors
|
||||
{
|
||||
[TestFixture]
|
||||
public class NestedContentPropertyComponentTests
|
||||
{
|
||||
[Test]
|
||||
public void Invalid_Json()
|
||||
{
|
||||
var component = new NestedContentPropertyComponent();
|
||||
|
||||
Assert.DoesNotThrow(() => component.CreateNestedContentKeys("this is not json", true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void No_Nesting()
|
||||
{
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid() };
|
||||
var guidCounter = 0;
|
||||
Func<Guid> guidFactory = () => guids[guidCounter++];
|
||||
|
||||
var json = @"[
|
||||
{""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""},
|
||||
{""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""}
|
||||
]";
|
||||
var expected = json
|
||||
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
|
||||
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString());
|
||||
|
||||
var component = new NestedContentPropertyComponent();
|
||||
var result = component.CreateNestedContentKeys(json, false, guidFactory);
|
||||
|
||||
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void One_Level_Nesting_Unescaped()
|
||||
{
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
||||
var guidCounter = 0;
|
||||
Func<Guid> guidFactory = () => guids[guidCounter++];
|
||||
|
||||
var json = @"[{
|
||||
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
|
||||
""name"": ""Item 2"",
|
||||
""ncContentTypeAlias"": ""list"",
|
||||
""text"": ""zoot"",
|
||||
""subItems"": [{
|
||||
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
|
||||
""name"": ""Item 2"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""zoot""
|
||||
}
|
||||
]
|
||||
}
|
||||
]";
|
||||
|
||||
var expected = json
|
||||
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
|
||||
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
|
||||
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
|
||||
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
|
||||
|
||||
var component = new NestedContentPropertyComponent();
|
||||
var result = component.CreateNestedContentKeys(json, false, guidFactory);
|
||||
|
||||
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void One_Level_Nesting_Escaped()
|
||||
{
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
||||
var guidCounter = 0;
|
||||
Func<Guid> guidFactory = () => guids[guidCounter++];
|
||||
|
||||
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
|
||||
// and this is how to do that, the result will also include quotes around it.
|
||||
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
|
||||
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
|
||||
""name"": ""Item 2"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""zoot""
|
||||
}
|
||||
]").ToString());
|
||||
|
||||
var json = @"[{
|
||||
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
|
||||
""name"": ""Item 2"",
|
||||
""ncContentTypeAlias"": ""list"",
|
||||
""text"": ""zoot"",
|
||||
""subItems"":" + subJsonEscaped + @"
|
||||
}
|
||||
]";
|
||||
|
||||
var expected = json
|
||||
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
|
||||
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
|
||||
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
|
||||
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
|
||||
|
||||
var component = new NestedContentPropertyComponent();
|
||||
var result = component.CreateNestedContentKeys(json, false, guidFactory);
|
||||
|
||||
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Nested_In_Complex_Editor_Escaped()
|
||||
{
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
||||
var guidCounter = 0;
|
||||
Func<Guid> guidFactory = () => guids[guidCounter++];
|
||||
|
||||
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
|
||||
// and this is how to do that, the result will also include quotes around it.
|
||||
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
|
||||
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
|
||||
""name"": ""Item 2"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""zoot""
|
||||
}
|
||||
]").ToString());
|
||||
|
||||
// Complex editor such as the grid
|
||||
var complexEditorJsonEscaped = @"{
|
||||
""name"": ""1 column layout"",
|
||||
""sections"": [
|
||||
{
|
||||
""grid"": ""12"",
|
||||
""rows"": [
|
||||
{
|
||||
""name"": ""Article"",
|
||||
""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"",
|
||||
""areas"": [
|
||||
{
|
||||
""grid"": ""4"",
|
||||
""controls"": [
|
||||
{
|
||||
""value"": ""I am quote"",
|
||||
""editor"": {
|
||||
""alias"": ""quote"",
|
||||
""view"": ""textstring""
|
||||
},
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
}],
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
},
|
||||
{
|
||||
""grid"": ""8"",
|
||||
""controls"": [
|
||||
{
|
||||
""value"": ""Header"",
|
||||
""editor"": {
|
||||
""alias"": ""headline"",
|
||||
""view"": ""textstring""
|
||||
},
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
},
|
||||
{
|
||||
""value"": " + subJsonEscaped + @",
|
||||
""editor"": {
|
||||
""alias"": ""madeUpNestedContent"",
|
||||
""view"": ""madeUpNestedContentInGrid""
|
||||
},
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
}],
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
}],
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
}]
|
||||
}]
|
||||
}";
|
||||
|
||||
|
||||
var json = @"[{
|
||||
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
|
||||
""name"": ""Item 2"",
|
||||
""ncContentTypeAlias"": ""list"",
|
||||
""text"": ""zoot"",
|
||||
""subItems"":" + complexEditorJsonEscaped + @"
|
||||
}
|
||||
]";
|
||||
|
||||
var expected = json
|
||||
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
|
||||
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
|
||||
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
|
||||
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
|
||||
|
||||
var component = new NestedContentPropertyComponent();
|
||||
var result = component.CreateNestedContentKeys(json, false, guidFactory);
|
||||
|
||||
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void No_Nesting_Generates_Keys_For_Missing_Items()
|
||||
{
|
||||
var guids = new[] { Guid.NewGuid() };
|
||||
var guidCounter = 0;
|
||||
Func<Guid> guidFactory = () => guids[guidCounter++];
|
||||
|
||||
var json = @"[
|
||||
{""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1 my key wont change"",""ncContentTypeAlias"":""nested"",""text"":""woot""},
|
||||
{""name"":""Item 2 was copied and has no key prop"",""ncContentTypeAlias"":""nested"",""text"":""zoot""}
|
||||
]";
|
||||
|
||||
var component = new NestedContentPropertyComponent();
|
||||
var result = component.CreateNestedContentKeys(json, true, guidFactory);
|
||||
|
||||
// Ensure the new GUID is put in a key into the JSON
|
||||
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
|
||||
|
||||
// Ensure that the original key is NOT changed/modified & still exists
|
||||
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void One_Level_Nesting_Escaped_Generates_Keys_For_Missing_Items()
|
||||
{
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
||||
var guidCounter = 0;
|
||||
Func<Guid> guidFactory = () => guids[guidCounter++];
|
||||
|
||||
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
|
||||
// and this is how to do that, the result will also include quotes around it.
|
||||
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""name"": ""Nested Item 2 was copied and has no key"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""zoot""
|
||||
}
|
||||
]").ToString());
|
||||
|
||||
var json = @"[{
|
||||
""name"": ""Item 1 was copied and has no key"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
|
||||
""name"": ""Item 2"",
|
||||
""ncContentTypeAlias"": ""list"",
|
||||
""text"": ""zoot"",
|
||||
""subItems"":" + subJsonEscaped + @"
|
||||
}
|
||||
]";
|
||||
|
||||
var component = new NestedContentPropertyComponent();
|
||||
var result = component.CreateNestedContentKeys(json, true, guidFactory);
|
||||
|
||||
// Ensure the new GUID is put in a key into the JSON for each item
|
||||
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
|
||||
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString()));
|
||||
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[2].ToString()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Nested_In_Complex_Editor_Escaped_Generates_Keys_For_Missing_Items()
|
||||
{
|
||||
var guids = new[] { Guid.NewGuid(), Guid.NewGuid() };
|
||||
var guidCounter = 0;
|
||||
Func<Guid> guidFactory = () => guids[guidCounter++];
|
||||
|
||||
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
|
||||
// and this is how to do that, the result will also include quotes around it.
|
||||
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
|
||||
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""name"": ""Nested Item 2 was copied and has no key"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""zoot""
|
||||
}
|
||||
]").ToString());
|
||||
|
||||
// Complex editor such as the grid
|
||||
var complexEditorJsonEscaped = @"{
|
||||
""name"": ""1 column layout"",
|
||||
""sections"": [
|
||||
{
|
||||
""grid"": ""12"",
|
||||
""rows"": [
|
||||
{
|
||||
""name"": ""Article"",
|
||||
""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"",
|
||||
""areas"": [
|
||||
{
|
||||
""grid"": ""4"",
|
||||
""controls"": [
|
||||
{
|
||||
""value"": ""I am quote"",
|
||||
""editor"": {
|
||||
""alias"": ""quote"",
|
||||
""view"": ""textstring""
|
||||
},
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
}],
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
},
|
||||
{
|
||||
""grid"": ""8"",
|
||||
""controls"": [
|
||||
{
|
||||
""value"": ""Header"",
|
||||
""editor"": {
|
||||
""alias"": ""headline"",
|
||||
""view"": ""textstring""
|
||||
},
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
},
|
||||
{
|
||||
""value"": " + subJsonEscaped + @",
|
||||
""editor"": {
|
||||
""alias"": ""madeUpNestedContent"",
|
||||
""view"": ""madeUpNestedContentInGrid""
|
||||
},
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
}],
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
}],
|
||||
""styles"": null,
|
||||
""config"": null
|
||||
}]
|
||||
}]
|
||||
}";
|
||||
|
||||
|
||||
var json = @"[{
|
||||
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
|
||||
""name"": ""Item 1"",
|
||||
""ncContentTypeAlias"": ""text"",
|
||||
""text"": ""woot""
|
||||
}, {
|
||||
""name"": ""Item 2 was copied and has no key"",
|
||||
""ncContentTypeAlias"": ""list"",
|
||||
""text"": ""zoot"",
|
||||
""subItems"":" + complexEditorJsonEscaped + @"
|
||||
}
|
||||
]";
|
||||
|
||||
var component = new NestedContentPropertyComponent();
|
||||
var result = component.CreateNestedContentKeys(json, true, guidFactory);
|
||||
|
||||
// Ensure the new GUID is put in a key into the JSON for each item
|
||||
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
|
||||
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
})));
|
||||
|
||||
var publishedPropType = new PublishedPropertyType(
|
||||
new PublishedContentType(1234, "test", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing),
|
||||
new PublishedContentType(Guid.NewGuid(), 1234, "test", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing),
|
||||
new PropertyType("test", ValueStorageType.Nvarchar) { DataTypeId = 123 },
|
||||
new PropertyValueConverterCollection(Enumerable.Empty<IPropertyValueConverter>()),
|
||||
Mock.Of<IPublishedModelFactory>(), mockPublishedContentTypeFactory.Object);
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Umbraco.Tests.Published
|
||||
yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes);
|
||||
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", CreatePropertyTypes);
|
||||
|
||||
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace Umbraco.Tests.Published
|
||||
=> propertyType.EditorAlias.InvariantEquals("Umbraco.Void");
|
||||
|
||||
public Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (int);
|
||||
=> typeof(int);
|
||||
|
||||
public PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
@@ -83,10 +83,10 @@ namespace Umbraco.Tests.Published
|
||||
=> int.TryParse(source as string, out int i) ? i : 0;
|
||||
|
||||
public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> (int) inter;
|
||||
=> (int)inter;
|
||||
|
||||
public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> ((int) inter).ToString();
|
||||
=> ((int)inter).ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -120,11 +120,11 @@ namespace Umbraco.Tests.Published
|
||||
yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes);
|
||||
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", CreatePropertyTypes);
|
||||
|
||||
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
|
||||
|
||||
var cntType1 = contentTypeFactory.CreateContentType(1001, "cnt1", t => Enumerable.Empty<PublishedPropertyType>());
|
||||
var cntType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1001, "cnt1", t => Enumerable.Empty<PublishedPropertyType>());
|
||||
var cnt1 = new SolidPublishedContent(cntType1) { Id = 1234 };
|
||||
cacheContent[cnt1.Id] = cnt1;
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace Umbraco.Tests.Published
|
||||
}
|
||||
|
||||
public bool? IsValue(object value, PropertyValueLevel level)
|
||||
=> value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false);
|
||||
=> value != null && (!(value is string) || string.IsNullOrWhiteSpace((string)value) == false);
|
||||
|
||||
public bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals("Umbraco.Void");
|
||||
@@ -162,10 +162,10 @@ namespace Umbraco.Tests.Published
|
||||
=> int.TryParse(source as string, out int i) ? i : -1;
|
||||
|
||||
public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int) inter);
|
||||
=> _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int)inter);
|
||||
|
||||
public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> ((int) inter).ToString();
|
||||
=> ((int)inter).ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -215,10 +215,10 @@ namespace Umbraco.Tests.Published
|
||||
yield return contentTypeFactory.CreatePropertyType(contentType, "prop" + i, i);
|
||||
}
|
||||
|
||||
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", t => CreatePropertyTypes(t, 1));
|
||||
var elementType2 = contentTypeFactory.CreateContentType(1001, "element2", t => CreatePropertyTypes(t, 2));
|
||||
var contentType1 = contentTypeFactory.CreateContentType(1002, "content1", t => CreatePropertyTypes(t, 1));
|
||||
var contentType2 = contentTypeFactory.CreateContentType(1003, "content2", t => CreatePropertyTypes(t, 2));
|
||||
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", t => CreatePropertyTypes(t, 1));
|
||||
var elementType2 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1001, "element2", t => CreatePropertyTypes(t, 2));
|
||||
var contentType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1002, "content1", t => CreatePropertyTypes(t, 1));
|
||||
var contentType2 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1003, "content2", t => CreatePropertyTypes(t, 2));
|
||||
|
||||
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "val1" } }, false);
|
||||
var element2 = new PublishedElement(elementType2, Guid.NewGuid(), new Dictionary<string, object> { { "prop2", "1003" } }, false);
|
||||
@@ -239,22 +239,22 @@ namespace Umbraco.Tests.Published
|
||||
// can get the actual property Clr type
|
||||
// ie ModelType gets properly mapped by IPublishedContentModelFactory
|
||||
// must test ModelClrType with special equals 'cos they are not ref-equals
|
||||
Assert.IsTrue(ModelType.Equals(typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType));
|
||||
Assert.AreEqual(typeof (IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>), contentType2.GetPropertyType("prop2").ClrType);
|
||||
Assert.IsTrue(ModelType.Equals(typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType));
|
||||
Assert.AreEqual(typeof(IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>), contentType2.GetPropertyType("prop2").ClrType);
|
||||
|
||||
// can create a model for an element
|
||||
var model1 = factory.CreateModel(element1);
|
||||
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestElementModel1>(model1);
|
||||
Assert.AreEqual("val1", ((PublishedSnapshotTestObjects.TestElementModel1) model1).Prop1);
|
||||
Assert.AreEqual("val1", ((PublishedSnapshotTestObjects.TestElementModel1)model1).Prop1);
|
||||
|
||||
// can create a model for a published content
|
||||
var model2 = factory.CreateModel(element2);
|
||||
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestElementModel2>(model2);
|
||||
var mmodel2 = (PublishedSnapshotTestObjects.TestElementModel2) model2;
|
||||
var mmodel2 = (PublishedSnapshotTestObjects.TestElementModel2)model2;
|
||||
|
||||
// and get direct property
|
||||
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestContentModel1[]>(model2.Value("prop2"));
|
||||
Assert.AreEqual(1, ((PublishedSnapshotTestObjects.TestContentModel1[]) model2.Value("prop2")).Length);
|
||||
Assert.AreEqual(1, ((PublishedSnapshotTestObjects.TestContentModel1[])model2.Value("prop2")).Length);
|
||||
|
||||
// and get model property
|
||||
Assert.IsInstanceOf<IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>>(mmodel2.Prop2);
|
||||
@@ -271,7 +271,7 @@ namespace Umbraco.Tests.Published
|
||||
=> propertyType.EditorAlias == "Umbraco.Void";
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (string);
|
||||
=> typeof(string);
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
@@ -290,7 +290,7 @@ namespace Umbraco.Tests.Published
|
||||
=> propertyType.EditorAlias == "Umbraco.Void.2";
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1"));
|
||||
=> typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1"));
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Elements;
|
||||
@@ -303,7 +303,7 @@ namespace Umbraco.Tests.Published
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
return ((int[]) inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1) _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray();
|
||||
return ((int[])inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1)_publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Umbraco.Tests.Published
|
||||
var proflog = new ProfilingLogger(logger, profiler);
|
||||
|
||||
PropertyEditorCollection editors = null;
|
||||
var editor = new NestedContentPropertyEditor(logger, new Lazy<PropertyEditorCollection>(() => editors), Mock.Of<IDataTypeService>(), Mock.Of<IContentTypeService>());
|
||||
var editor = new NestedContentPropertyEditor(logger, new Lazy<PropertyEditorCollection>(() => editors), Mock.Of<IDataTypeService>(), Mock.Of<IContentTypeService>(), Mock.Of<ILocalizedTextService>());
|
||||
editors = new PropertyEditorCollection(new DataEditorCollection(new DataEditor[] { editor }));
|
||||
|
||||
var dataType1 = new DataType(editor)
|
||||
@@ -100,8 +100,8 @@ namespace Umbraco.Tests.Published
|
||||
.Returns((string alias) =>
|
||||
{
|
||||
return alias == "contentN1"
|
||||
? (IList) new List<TestElementModel>()
|
||||
: (IList) new List<IPublishedElement>();
|
||||
? (IList)new List<TestElementModel>()
|
||||
: (IList)new List<IPublishedElement>();
|
||||
});
|
||||
|
||||
var contentCache = new Mock<IPublishedContentCache>();
|
||||
@@ -142,9 +142,9 @@ namespace Umbraco.Tests.Published
|
||||
yield return factory.CreatePropertyType(contentType, "propertyN1", 3);
|
||||
}
|
||||
|
||||
var contentType1 = factory.CreateContentType(1, "content1", CreatePropertyTypes1);
|
||||
var contentType2 = factory.CreateContentType(2, "content2", CreatePropertyTypes2);
|
||||
var contentTypeN1 = factory.CreateContentType(2, "contentN1", CreatePropertyTypesN1, isElement: true);
|
||||
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "content1", CreatePropertyTypes1);
|
||||
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "content2", CreatePropertyTypes2);
|
||||
var contentTypeN1 = factory.CreateContentType(Guid.NewGuid(), 2, "contentN1", CreatePropertyTypesN1, isElement: true);
|
||||
|
||||
// mocked content cache returns content types
|
||||
contentCache
|
||||
@@ -164,7 +164,7 @@ namespace Umbraco.Tests.Published
|
||||
(var contentType1, _) = CreateContentTypes();
|
||||
|
||||
// nested single converter returns the proper value clr type TestModel, and cache level
|
||||
Assert.AreEqual(typeof (TestElementModel), contentType1.GetPropertyType("property1").ClrType);
|
||||
Assert.AreEqual(typeof(TestElementModel), contentType1.GetPropertyType("property1").ClrType);
|
||||
Assert.AreEqual(PropertyCacheLevel.Element, contentType1.GetPropertyType("property1").CacheLevel);
|
||||
|
||||
var key = Guid.NewGuid();
|
||||
@@ -172,7 +172,7 @@ namespace Umbraco.Tests.Published
|
||||
var content = new SolidPublishedContent(contentType1)
|
||||
{
|
||||
Key = key,
|
||||
Properties = new []
|
||||
Properties = new[]
|
||||
{
|
||||
new TestPublishedProperty(contentType1.GetPropertyType("property1"), $@"[
|
||||
{{ ""key"": ""{keyA}"", ""propertyN1"": ""foo"", ""ncContentTypeAlias"": ""contentN1"" }}
|
||||
@@ -183,7 +183,7 @@ namespace Umbraco.Tests.Published
|
||||
|
||||
// nested single converter returns proper TestModel value
|
||||
Assert.IsInstanceOf<TestElementModel>(value);
|
||||
var valueM = (TestElementModel) value;
|
||||
var valueM = (TestElementModel)value;
|
||||
Assert.AreEqual("foo", valueM.PropValue);
|
||||
Assert.AreEqual(keyA, valueM.Key);
|
||||
}
|
||||
@@ -194,7 +194,7 @@ namespace Umbraco.Tests.Published
|
||||
(_, var contentType2) = CreateContentTypes();
|
||||
|
||||
// nested many converter returns the proper value clr type IEnumerable<TestModel>, and cache level
|
||||
Assert.AreEqual(typeof (IEnumerable<TestElementModel>), contentType2.GetPropertyType("property2").ClrType);
|
||||
Assert.AreEqual(typeof(IEnumerable<TestElementModel>), contentType2.GetPropertyType("property2").ClrType);
|
||||
Assert.AreEqual(PropertyCacheLevel.Element, contentType2.GetPropertyType("property2").CacheLevel);
|
||||
|
||||
var key = Guid.NewGuid();
|
||||
@@ -216,7 +216,7 @@ namespace Umbraco.Tests.Published
|
||||
// nested many converter returns proper IEnumerable<TestModel> value
|
||||
Assert.IsInstanceOf<IEnumerable<IPublishedElement>>(value);
|
||||
Assert.IsInstanceOf<IEnumerable<TestElementModel>>(value);
|
||||
var valueM = ((IEnumerable<TestElementModel>) value).ToArray();
|
||||
var valueM = ((IEnumerable<TestElementModel>)value).ToArray();
|
||||
Assert.AreEqual("foo", valueM[0].PropValue);
|
||||
Assert.AreEqual(keyA, valueM[0].Key);
|
||||
Assert.AreEqual("bar", valueM[1].PropValue);
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Umbraco.Tests.Published
|
||||
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
|
||||
|
||||
// PublishedElementPropertyBase.GetCacheLevels:
|
||||
//
|
||||
@@ -122,7 +122,7 @@ namespace Umbraco.Tests.Published
|
||||
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
|
||||
|
||||
var elementsCache = new FastDictionaryAppCache();
|
||||
var snapshotCache = new FastDictionaryAppCache();
|
||||
@@ -199,7 +199,7 @@ namespace Umbraco.Tests.Published
|
||||
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
|
||||
|
||||
Assert.Throws<Exception>(() =>
|
||||
{
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
var doc = GetContent(true, 1);
|
||||
//change a doc type alias
|
||||
var c = (SolidPublishedContent)doc.Children.ElementAt(0);
|
||||
c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
c.ContentType = new PublishedContentType(Guid.NewGuid(), 22, "DontMatch", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
|
||||
var dt = doc.ChildrenAsTable(Current.Services, "Child");
|
||||
|
||||
@@ -129,7 +129,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
|
||||
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
|
||||
var contentTypeAlias = createChildren ? "Parent" : "Child";
|
||||
var contentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var d = new SolidPublishedContent(contentType)
|
||||
{
|
||||
CreateDate = DateTime.Now,
|
||||
|
||||
@@ -73,14 +73,14 @@ namespace Umbraco.Tests.PublishedContent
|
||||
yield return factory.CreatePropertyType(contentType, "noprop", 1, variations: ContentVariation.Culture);
|
||||
}
|
||||
|
||||
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes1);
|
||||
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes1);
|
||||
|
||||
IEnumerable<IPublishedPropertyType> CreatePropertyTypes2(IPublishedContentType contentType)
|
||||
{
|
||||
yield return factory.CreatePropertyType(contentType, "prop3", 1, variations: ContentVariation.Culture);
|
||||
}
|
||||
|
||||
var contentType2 = factory.CreateContentType(2, "contentType2", Enumerable.Empty<string>(), CreatePropertyTypes2);
|
||||
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "contentType2", Enumerable.Empty<string>(), CreatePropertyTypes2);
|
||||
|
||||
var prop1 = new SolidPublishedPropertyWithLanguageVariants
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using Umbraco.Web;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Tests.Testing;
|
||||
using Umbraco.Web.Composing;
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Tests.PublishedContent
|
||||
{
|
||||
@@ -21,9 +22,9 @@ namespace Umbraco.Tests.PublishedContent
|
||||
yield return factory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "ContentType2", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType2Sub = factory.CreateContentType(Guid.NewGuid(), 3, "ContentType2Sub", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
|
||||
var content = new SolidPublishedContent(contentType1)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ using Umbraco.Core.Services;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.Templates;
|
||||
using Umbraco.Web.Models;
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Tests.PublishedContent
|
||||
{
|
||||
@@ -56,7 +57,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "content", 1);
|
||||
}
|
||||
|
||||
var type = new AutoPublishedContentType(0, "anything", CreatePropertyTypes);
|
||||
var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", CreatePropertyTypes);
|
||||
ContentTypesCache.GetPublishedContentTypeByAlias = alias => type;
|
||||
|
||||
var umbracoContext = GetUmbracoContext("/test");
|
||||
|
||||
@@ -83,8 +83,8 @@ namespace Umbraco.Tests.PublishedContent
|
||||
}
|
||||
|
||||
var compositionAliases = new[] { "MyCompositionAlias" };
|
||||
var anythingType = new AutoPublishedContentType(0, "anything", compositionAliases, CreatePropertyTypes);
|
||||
var homeType = new AutoPublishedContentType(0, "home", compositionAliases, CreatePropertyTypes);
|
||||
var anythingType = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", compositionAliases, CreatePropertyTypes);
|
||||
var homeType = new AutoPublishedContentType(Guid.NewGuid(), 0, "home", compositionAliases, CreatePropertyTypes);
|
||||
ContentTypesCache.GetPublishedContentTypeByAlias = alias => alias.InvariantEquals("home") ? homeType : anythingType;
|
||||
}
|
||||
|
||||
@@ -398,8 +398,8 @@ namespace Umbraco.Tests.PublishedContent
|
||||
[Test]
|
||||
public void Children_GroupBy_DocumentTypeAlias()
|
||||
{
|
||||
var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { });
|
||||
var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
|
||||
var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var contentTypes = new Dictionary<string, PublishedContentType>
|
||||
{
|
||||
{ home.Alias, home },
|
||||
@@ -419,8 +419,8 @@ namespace Umbraco.Tests.PublishedContent
|
||||
[Test]
|
||||
public void Children_Where_DocumentTypeAlias()
|
||||
{
|
||||
var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { });
|
||||
var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
|
||||
var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var contentTypes = new Dictionary<string, PublishedContentType>
|
||||
{
|
||||
{ home.Alias, home },
|
||||
@@ -903,7 +903,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
yield return factory.CreatePropertyType(contentType, "detached", 1003);
|
||||
}
|
||||
|
||||
var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes);
|
||||
var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
|
||||
var pt = ct.GetPropertyType("detached");
|
||||
var prop = new PublishedElementPropertyBase(pt, null, false, PropertyCacheLevel.None, 5548);
|
||||
Assert.IsInstanceOf<int>(prop.GetValue());
|
||||
@@ -935,7 +935,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes);
|
||||
var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
|
||||
|
||||
var c = new ImageWithLegendModel(ct, guid, new Dictionary<string, object>
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
pc.Setup(content => content.Path).Returns("-1,1");
|
||||
pc.Setup(content => content.Parent).Returns(() => null);
|
||||
pc.Setup(content => content.Properties).Returns(new Collection<IPublishedProperty>());
|
||||
pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing));
|
||||
pc.Setup(content => content.ContentType).Returns(new PublishedContentType(Guid.NewGuid(), 22, "anything", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing));
|
||||
return pc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
{ }
|
||||
}
|
||||
|
||||
class SolidPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache
|
||||
class SolidPublishedContentCache : PublishedCacheBase, IPublishedContentCache2, IPublishedMediaCache2
|
||||
{
|
||||
private readonly Dictionary<int, IPublishedContent> _content = new Dictionary<int, IPublishedContent>();
|
||||
|
||||
@@ -150,6 +150,11 @@ namespace Umbraco.Tests.PublishedContent
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IPublishedContentType GetContentType(Guid key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
@@ -378,7 +383,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
#endregion
|
||||
}
|
||||
|
||||
class PublishedContentStrong1 : PublishedContentModel
|
||||
internal class PublishedContentStrong1 : PublishedContentModel
|
||||
{
|
||||
public PublishedContentStrong1(IPublishedContent content)
|
||||
: base(content)
|
||||
@@ -387,7 +392,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
public int StrongValue => this.Value<int>("strongValue");
|
||||
}
|
||||
|
||||
class PublishedContentStrong1Sub : PublishedContentStrong1
|
||||
internal class PublishedContentStrong1Sub : PublishedContentStrong1
|
||||
{
|
||||
public PublishedContentStrong1Sub(IPublishedContent content)
|
||||
: base(content)
|
||||
@@ -396,7 +401,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
public int AnotherValue => this.Value<int>("anotherValue");
|
||||
}
|
||||
|
||||
class PublishedContentStrong2 : PublishedContentModel
|
||||
internal class PublishedContentStrong2 : PublishedContentModel
|
||||
{
|
||||
public PublishedContentStrong2(IPublishedContent content)
|
||||
: base(content)
|
||||
@@ -405,7 +410,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
public int StrongValue => this.Value<int>("strongValue");
|
||||
}
|
||||
|
||||
class AutoPublishedContentType : PublishedContentType
|
||||
internal class AutoPublishedContentType : PublishedContentType
|
||||
{
|
||||
private static readonly IPublishedPropertyType Default;
|
||||
|
||||
@@ -418,20 +423,20 @@ namespace Umbraco.Tests.PublishedContent
|
||||
Default = factory.CreatePropertyType("*", 666);
|
||||
}
|
||||
|
||||
public AutoPublishedContentType(int id, string alias, IEnumerable<PublishedPropertyType> propertyTypes)
|
||||
: base(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
|
||||
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<PublishedPropertyType> propertyTypes)
|
||||
: base(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
|
||||
{ }
|
||||
|
||||
public AutoPublishedContentType(int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
|
||||
: base(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
|
||||
public AutoPublishedContentType(Guid key, int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
|
||||
: base(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
|
||||
{ }
|
||||
|
||||
public AutoPublishedContentType(int id, string alias, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes)
|
||||
: base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
|
||||
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes)
|
||||
: base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
|
||||
{ }
|
||||
|
||||
public AutoPublishedContentType(int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
|
||||
: base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
|
||||
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
|
||||
: base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
|
||||
{ }
|
||||
|
||||
public override IPublishedPropertyType GetPropertyType(string alias)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
@@ -27,7 +28,7 @@ namespace Umbraco.Tests.Routing
|
||||
Mock.Of<IPublishedModelFactory>(),
|
||||
Mock.Of<IPublishedContentTypeFactory>()),
|
||||
};
|
||||
_publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
|
||||
_publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
|
||||
}
|
||||
|
||||
protected override PublishedContentType GetPublishedContentTypeByAlias(string alias)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
@@ -25,7 +26,7 @@ namespace Umbraco.Tests.Routing
|
||||
Mock.Of<IPublishedModelFactory>(),
|
||||
Mock.Of<IPublishedContentTypeFactory>()),
|
||||
};
|
||||
_publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
|
||||
_publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
|
||||
}
|
||||
|
||||
protected override PublishedContentType GetPublishedContentTypeByAlias(string alias)
|
||||
|
||||
@@ -139,7 +139,7 @@ namespace Umbraco.Tests.Routing
|
||||
property.SetSourceValue("en", enMediaUrl, true);
|
||||
property.SetSourceValue("da", daMediaUrl);
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), new [] { umbracoFilePropertyType }, ContentVariation.Culture);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), new [] { umbracoFilePropertyType }, ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}};
|
||||
|
||||
var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Auto, "da");
|
||||
@@ -150,7 +150,7 @@ namespace Umbraco.Tests.Routing
|
||||
{
|
||||
var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing);
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(),
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(),
|
||||
new[] {umbracoFilePropertyType}, ContentVariation.Nothing);
|
||||
|
||||
return new SolidPublishedContent(contentType)
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Umbraco.Tests.Routing
|
||||
frequest.TemplateModel = template;
|
||||
|
||||
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
|
||||
var type = new AutoPublishedContentType(22, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "CustomDocument", new PublishedPropertyType[] { });
|
||||
ContentTypesCache.GetPublishedContentTypeByAlias = alias => type;
|
||||
|
||||
var handler = new RenderRouteHandler(umbracoContext, new TestControllerFactory(umbracoContextAccessor, Mock.Of<ILogger>(), context =>
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace Umbraco.Tests.Routing
|
||||
var umbracoSettings = Current.Configs.Settings();
|
||||
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
|
||||
|
||||
var publishedContentCache = new Mock<IPublishedContentCache>();
|
||||
@@ -203,7 +203,7 @@ namespace Umbraco.Tests.Routing
|
||||
|
||||
var umbracoSettings = Current.Configs.Settings();
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
|
||||
|
||||
var publishedContentCache = new Mock<IPublishedContentCache>();
|
||||
@@ -257,7 +257,7 @@ namespace Umbraco.Tests.Routing
|
||||
|
||||
var umbracoSettings = Current.Configs.Settings();
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
|
||||
|
||||
var publishedContentCache = new Mock<IPublishedContentCache>();
|
||||
|
||||
@@ -1197,7 +1197,7 @@ namespace Umbraco.Tests.Services
|
||||
Assert.IsFalse(content.HasIdentity);
|
||||
|
||||
// content cannot publish values because they are invalid
|
||||
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService, ServiceContext.TextService);
|
||||
var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties, CultureImpact.Invariant);
|
||||
Assert.IsFalse(isValid);
|
||||
Assert.IsNotEmpty(invalidProperties);
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
var propEditors = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor }));
|
||||
|
||||
validationService = new PropertyValidationService(propEditors, dataTypeService.Object);
|
||||
validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of<ILocalizedTextService>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Umbraco.Tests.Templates
|
||||
{
|
||||
//setup a mock url provider which we'll use for testing
|
||||
|
||||
var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var media = new Mock<IPublishedContent>();
|
||||
media.Setup(x => x.ContentType).Returns(mediaType);
|
||||
var mediaUrlProvider = new Mock<IMediaUrlProvider>();
|
||||
|
||||
@@ -54,12 +54,12 @@ namespace Umbraco.Tests.Templates
|
||||
contentUrlProvider
|
||||
.Setup(x => x.GetUrl(It.IsAny<UmbracoContext>(), It.IsAny<IPublishedContent>(), It.IsAny<UrlMode>(), It.IsAny<string>(), It.IsAny<Uri>()))
|
||||
.Returns(UrlInfo.Url("/my-test-url"));
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var publishedContent = new Mock<IPublishedContent>();
|
||||
publishedContent.Setup(x => x.Id).Returns(1234);
|
||||
publishedContent.Setup(x => x.ContentType).Returns(contentType);
|
||||
|
||||
var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var media = new Mock<IPublishedContent>();
|
||||
media.Setup(x => x.ContentType).Returns(mediaType);
|
||||
var mediaUrlProvider = new Mock<IMediaUrlProvider>();
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace Umbraco.Tests.TestHelpers
|
||||
new DataType(new VoidEditor(Mock.Of<ILogger>())) { Id = 1 });
|
||||
|
||||
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
|
||||
var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] { });
|
||||
var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", new PublishedPropertyType[] { });
|
||||
ContentTypesCache.GetPublishedContentTypeByAlias = alias => GetPublishedContentTypeByAlias(alias) ?? type;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@ namespace Umbraco.Tests.TestHelpers.Entities
|
||||
};
|
||||
|
||||
var contentCollection = new PropertyTypeCollection(true);
|
||||
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 });
|
||||
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -87 });
|
||||
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox });
|
||||
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.RichtextEditor });
|
||||
|
||||
var metaCollection = new PropertyTypeCollection(true);
|
||||
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 });
|
||||
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -89 });
|
||||
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox });
|
||||
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.Textarea });
|
||||
|
||||
contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 });
|
||||
contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Meta", SortOrder = 2 });
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace Umbraco.Tests.Testing.TestingTests
|
||||
|
||||
var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, Enumerable.Empty<IMediaUrlProvider>(), umbracoContext.VariationContextAccessor);
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var publishedContent = Mock.Of<IPublishedContent>();
|
||||
Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType);
|
||||
|
||||
|
||||
@@ -147,7 +147,9 @@
|
||||
<Compile Include="Persistence\Mappers\MapperTestBase.cs" />
|
||||
<Compile Include="Persistence\Repositories\DocumentRepositoryTest.cs" />
|
||||
<Compile Include="Persistence\Repositories\EntityRepositoryTest.cs" />
|
||||
<Compile Include="PropertyEditors\BlockListPropertyValueConverterTests.cs" />
|
||||
<Compile Include="PropertyEditors\DataValueReferenceFactoryCollectionTests.cs" />
|
||||
<Compile Include="PropertyEditors\NestedContentPropertyComponentTests.cs" />
|
||||
<Compile Include="PublishedContent\NuCacheChildrenTests.cs" />
|
||||
<Compile Include="PublishedContent\PublishedContentLanguageVariantTests.cs" />
|
||||
<Compile Include="PublishedContent\PublishedContentSnapshotTestBase.cs" />
|
||||
@@ -265,7 +267,7 @@
|
||||
<Compile Include="Web\HttpCookieExtensionsTests.cs" />
|
||||
<Compile Include="Templates\HtmlImageSourceParserTests.cs" />
|
||||
<Compile Include="Web\Mvc\HtmlStringUtilitiesTests.cs" />
|
||||
<Compile Include="Web\ModelStateExtensionsTests.cs" />
|
||||
<Compile Include="Web\Validation\ModelStateExtensionsTests.cs" />
|
||||
<Compile Include="Web\Mvc\RenderIndexActionSelectorAttributeTests.cs" />
|
||||
<Compile Include="Persistence\NPocoTests\PetaPocoCachesTest.cs" />
|
||||
<Compile Include="Persistence\Repositories\AuditRepositoryTest.cs" />
|
||||
@@ -512,6 +514,7 @@
|
||||
<Compile Include="CoreThings\VersionExtensionTests.cs" />
|
||||
<Compile Include="Web\TemplateUtilitiesTests.cs" />
|
||||
<Compile Include="Web\UrlHelperExtensionTests.cs" />
|
||||
<Compile Include="Web\Validation\ContentModelValidatorTests.cs" />
|
||||
<Compile Include="Web\WebExtensionMethodTests.cs" />
|
||||
<Compile Include="CoreThings\XmlExtensionsTests.cs" />
|
||||
<Compile Include="Misc\XmlHelperTests.cs" />
|
||||
|
||||
369
src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs
Normal file
369
src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs
Normal file
@@ -0,0 +1,369 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.Editors.Filters;
|
||||
using Umbraco.Tests.TestHelpers.Entities;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
using Umbraco.Web.Editors.Binders;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Tests.Testing;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Composing;
|
||||
using System.Web.Http.ModelBinding;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Web.PropertyEditors.Validation;
|
||||
|
||||
namespace Umbraco.Tests.Web.Validation
|
||||
{
|
||||
[UmbracoTest(Mapper = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)]
|
||||
[TestFixture]
|
||||
public class ContentModelValidatorTests : UmbracoTestBase
|
||||
{
|
||||
private const int ComplexDataTypeId = 9999;
|
||||
private const string ContentTypeAlias = "textPage";
|
||||
private ContentType _contentType;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
base.SetUp();
|
||||
|
||||
_contentType = MockedContentTypes.CreateTextPageContentType(ContentTypeAlias);
|
||||
// add complex editor
|
||||
_contentType.AddPropertyType(
|
||||
new PropertyType("complexTest", ValueStorageType.Ntext) { Alias = "complex", Name = "Complex", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = ComplexDataTypeId },
|
||||
"Content");
|
||||
|
||||
// make them all validate with a regex rule that will not pass
|
||||
foreach (var prop in _contentType.PropertyTypes)
|
||||
{
|
||||
prop.ValidationRegExp = "^donotmatch$";
|
||||
prop.ValidationRegExpMessage = "Does not match!";
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Compose()
|
||||
{
|
||||
base.Compose();
|
||||
|
||||
var complexEditorConfig = new NestedContentConfiguration
|
||||
{
|
||||
ContentTypes = new[]
|
||||
{
|
||||
new NestedContentConfiguration.ContentType { Alias = "feature" }
|
||||
}
|
||||
};
|
||||
var dataTypeService = new Mock<IDataTypeService>();
|
||||
dataTypeService.Setup(x => x.GetDataType(It.IsAny<int>()))
|
||||
.Returns((int id) => id == ComplexDataTypeId
|
||||
? Mock.Of<IDataType>(x => x.Configuration == complexEditorConfig)
|
||||
: Mock.Of<IDataType>());
|
||||
|
||||
var contentTypeService = new Mock<IContentTypeService>();
|
||||
contentTypeService.Setup(x => x.GetAll(It.IsAny<int[]>()))
|
||||
.Returns(() => new List<IContentType>
|
||||
{
|
||||
_contentType
|
||||
});
|
||||
|
||||
var textService = new Mock<ILocalizedTextService>();
|
||||
textService.Setup(x => x.Localize("validation/invalidPattern", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns(() => "invalidPattern");
|
||||
textService.Setup(x => x.Localize("validation/invalidNull", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns("invalidNull");
|
||||
textService.Setup(x => x.Localize("validation/invalidEmpty", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns("invalidEmpty");
|
||||
|
||||
Composition.RegisterUnique(x => Mock.Of<IDataTypeService>(x => x.GetDataType(It.IsAny<int>()) == Mock.Of<IDataType>()));
|
||||
Composition.RegisterUnique(x => dataTypeService.Object);
|
||||
Composition.RegisterUnique(x => contentTypeService.Object);
|
||||
Composition.RegisterUnique(x => textService.Object);
|
||||
|
||||
Composition.WithCollectionBuilder<DataEditorCollectionBuilder>()
|
||||
.Add<TestEditor>()
|
||||
.Add<ComplexTestEditor>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Serializer()
|
||||
{
|
||||
var nestedLevel2 = new ComplexEditorValidationResult();
|
||||
var id1 = Guid.NewGuid();
|
||||
var addressInfoElementTypeResult = new ComplexEditorElementTypeValidationResult("addressInfo", id1);
|
||||
var cityPropertyTypeResult = new ComplexEditorPropertyTypeValidationResult("city");
|
||||
cityPropertyTypeResult.AddValidationResult(new ValidationResult("City is invalid"));
|
||||
cityPropertyTypeResult.AddValidationResult(new ValidationResult("City cannot be empty"));
|
||||
cityPropertyTypeResult.AddValidationResult(new ValidationResult("City is not in Australia", new[] { "country" }));
|
||||
cityPropertyTypeResult.AddValidationResult(new ValidationResult("Not a capital city", new[] { "capital" }));
|
||||
addressInfoElementTypeResult.ValidationResults.Add(cityPropertyTypeResult);
|
||||
nestedLevel2.ValidationResults.Add(addressInfoElementTypeResult);
|
||||
|
||||
var nestedLevel1 = new ComplexEditorValidationResult();
|
||||
var id2 = Guid.NewGuid();
|
||||
var addressBookElementTypeResult = new ComplexEditorElementTypeValidationResult("addressBook", id2);
|
||||
var addressesPropertyTypeResult = new ComplexEditorPropertyTypeValidationResult("addresses");
|
||||
addressesPropertyTypeResult.AddValidationResult(new ValidationResult("Must have at least 3 addresses", new[] { "counter" }));
|
||||
addressesPropertyTypeResult.AddValidationResult(nestedLevel2); // This is a nested result within the level 1
|
||||
addressBookElementTypeResult.ValidationResults.Add(addressesPropertyTypeResult);
|
||||
var bookNamePropertyTypeResult = new ComplexEditorPropertyTypeValidationResult("bookName");
|
||||
bookNamePropertyTypeResult.AddValidationResult(new ValidationResult("Invalid address book name", new[] { "book" }));
|
||||
addressBookElementTypeResult.ValidationResults.Add(bookNamePropertyTypeResult);
|
||||
nestedLevel1.ValidationResults.Add(addressBookElementTypeResult);
|
||||
|
||||
var id3 = Guid.NewGuid();
|
||||
var addressBookElementTypeResult2 = new ComplexEditorElementTypeValidationResult("addressBook", id3);
|
||||
var addressesPropertyTypeResult2 = new ComplexEditorPropertyTypeValidationResult("addresses");
|
||||
addressesPropertyTypeResult2.AddValidationResult(new ValidationResult("Must have at least 2 addresses", new[] { "counter" }));
|
||||
addressBookElementTypeResult2.ValidationResults.Add(addressesPropertyTypeResult);
|
||||
var bookNamePropertyTypeResult2 = new ComplexEditorPropertyTypeValidationResult("bookName");
|
||||
bookNamePropertyTypeResult2.AddValidationResult(new ValidationResult("Name is too long"));
|
||||
addressBookElementTypeResult2.ValidationResults.Add(bookNamePropertyTypeResult2);
|
||||
nestedLevel1.ValidationResults.Add(addressBookElementTypeResult2);
|
||||
|
||||
// books is the outer most validation result and doesn't have it's own direct ValidationResult errors
|
||||
var outerError = new ComplexEditorValidationResult();
|
||||
var id4 = Guid.NewGuid();
|
||||
var addressBookCollectionElementTypeResult = new ComplexEditorElementTypeValidationResult("addressBookCollection", id4);
|
||||
var booksPropertyTypeResult= new ComplexEditorPropertyTypeValidationResult("books");
|
||||
booksPropertyTypeResult.AddValidationResult(nestedLevel1); // books is the outer most validation result
|
||||
addressBookCollectionElementTypeResult.ValidationResults.Add(booksPropertyTypeResult);
|
||||
outerError.ValidationResults.Add(addressBookCollectionElementTypeResult);
|
||||
|
||||
var serialized = JsonConvert.SerializeObject(outerError, Formatting.Indented, new ValidationResultConverter());
|
||||
Console.WriteLine(serialized);
|
||||
|
||||
var jsonError = JsonConvert.DeserializeObject<JArray>(serialized);
|
||||
|
||||
Assert.IsNotNull(jsonError.SelectToken("$[0]"));
|
||||
Assert.AreEqual(id4.ToString(), jsonError.SelectToken("$[0].$id").Value<string>());
|
||||
Assert.AreEqual("addressBookCollection", jsonError.SelectToken("$[0].$elementTypeAlias").Value<string>());
|
||||
Assert.AreEqual(string.Empty, jsonError.SelectToken("$[0].ModelState['_Properties.books.invariant.null'][0]").Value<string>());
|
||||
|
||||
var error0 = jsonError.SelectToken("$[0].books") as JArray;
|
||||
Assert.IsNotNull(error0);
|
||||
Assert.AreEqual(id2.ToString(), error0.SelectToken("$[0].$id").Value<string>());
|
||||
Assert.AreEqual("addressBook", error0.SelectToken("$[0].$elementTypeAlias").Value<string>());
|
||||
Assert.IsNotNull(error0.SelectToken("$[0].ModelState"));
|
||||
Assert.AreEqual(string.Empty, error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null'][0]").Value<string>());
|
||||
var error1 = error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null.counter']") as JArray;
|
||||
Assert.IsNotNull(error1);
|
||||
Assert.AreEqual(1, error1.Count);
|
||||
var error2 = error0.SelectToken("$[0].ModelState['_Properties.bookName.invariant.null.book']") as JArray;
|
||||
Assert.IsNotNull(error2);
|
||||
Assert.AreEqual(1, error2.Count);
|
||||
|
||||
Assert.AreEqual(id3.ToString(), error0.SelectToken("$[1].$id").Value<string>());
|
||||
Assert.AreEqual("addressBook", error0.SelectToken("$[1].$elementTypeAlias").Value<string>());
|
||||
Assert.IsNotNull(error0.SelectToken("$[1].ModelState"));
|
||||
Assert.AreEqual(string.Empty, error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null'][0]").Value<string>());
|
||||
var error6 = error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null.counter']") as JArray;
|
||||
Assert.IsNotNull(error6);
|
||||
Assert.AreEqual(1, error6.Count);
|
||||
var error7 = error0.SelectToken("$[1].ModelState['_Properties.bookName.invariant.null']") as JArray;
|
||||
Assert.IsNotNull(error7);
|
||||
Assert.AreEqual(1, error7.Count);
|
||||
|
||||
Assert.IsNotNull(error0.SelectToken("$[0].addresses"));
|
||||
Assert.AreEqual(id1.ToString(), error0.SelectToken("$[0].addresses[0].$id").Value<string>());
|
||||
Assert.AreEqual("addressInfo", error0.SelectToken("$[0].addresses[0].$elementTypeAlias").Value<string>());
|
||||
Assert.IsNotNull(error0.SelectToken("$[0].addresses[0].ModelState"));
|
||||
var error3 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.country']") as JArray;
|
||||
Assert.IsNotNull(error3);
|
||||
Assert.AreEqual(1, error3.Count);
|
||||
var error4 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.capital']") as JArray;
|
||||
Assert.IsNotNull(error4);
|
||||
Assert.AreEqual(1, error4.Count);
|
||||
var error5 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null']") as JArray;
|
||||
Assert.IsNotNull(error5);
|
||||
Assert.AreEqual(2, error5.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validating_ContentItemSave()
|
||||
{
|
||||
var validator = new ContentSaveModelValidator(
|
||||
Factory.GetInstance<ILogger>(),
|
||||
Mock.Of<IUmbracoContextAccessor>(),
|
||||
Factory.GetInstance<ILocalizedTextService>());
|
||||
|
||||
var content = MockedContent.CreateTextpageContent(_contentType, "test", -1);
|
||||
|
||||
var id1 = new Guid("c8df5136-d606-41f0-9134-dea6ae0c2fd9");
|
||||
var id2 = new Guid("f916104a-4082-48b2-a515-5c4bf2230f38");
|
||||
var id3 = new Guid("77E15DE9-1C79-47B2-BC60-4913BC4D4C6A");
|
||||
|
||||
// TODO: Ok now test with a 4th level complex nested editor
|
||||
|
||||
var complexValue = @"[{
|
||||
""key"": """ + id1.ToString() + @""",
|
||||
""name"": ""Hello world"",
|
||||
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
|
||||
""title"": ""Hello world"",
|
||||
""bodyText"": ""The world is round""
|
||||
}, {
|
||||
""key"": """ + id2.ToString() + @""",
|
||||
""name"": ""Super nested"",
|
||||
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
|
||||
""title"": ""Hi there!"",
|
||||
""bodyText"": ""Well hello there"",
|
||||
""complex"" : [{
|
||||
""key"": """ + id3.ToString() + @""",
|
||||
""name"": ""I am a sub nested content"",
|
||||
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
|
||||
""title"": ""Hello up there :)"",
|
||||
""bodyText"": ""Hello way up there on a different level""
|
||||
}]
|
||||
}
|
||||
]";
|
||||
content.SetValue("complex", complexValue);
|
||||
|
||||
// map the persisted properties to a model representing properties to save
|
||||
//var saveProperties = content.Properties.Select(x => Mapper.Map<ContentPropertyBasic>(x)).ToList();
|
||||
var saveProperties = content.Properties.Select(x =>
|
||||
{
|
||||
return new ContentPropertyBasic
|
||||
{
|
||||
Alias = x.Alias,
|
||||
Id = x.Id,
|
||||
Value = x.GetValue()
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
var saveVariants = new List<ContentVariantSave>
|
||||
{
|
||||
new ContentVariantSave
|
||||
{
|
||||
Culture = string.Empty,
|
||||
Segment = string.Empty,
|
||||
Name = content.Name,
|
||||
Save = true,
|
||||
Properties = saveProperties
|
||||
}
|
||||
};
|
||||
|
||||
var save = new ContentItemSave
|
||||
{
|
||||
Id = content.Id,
|
||||
Action = ContentSaveAction.Save,
|
||||
ContentTypeAlias = _contentType.Alias,
|
||||
ParentId = -1,
|
||||
PersistedContent = content,
|
||||
TemplateAlias = null,
|
||||
Variants = saveVariants
|
||||
};
|
||||
|
||||
// This will map the ContentItemSave.Variants.PropertyCollectionDto and then map the values in the saved model
|
||||
// back onto the persisted IContent model.
|
||||
ContentItemBinder.BindModel(save, content);
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
var isValid = validator.ValidatePropertiesData(save, saveVariants[0], saveVariants[0].PropertyCollectionDto, modelState);
|
||||
|
||||
// list results for debugging
|
||||
foreach (var state in modelState)
|
||||
{
|
||||
Console.WriteLine(state.Key);
|
||||
foreach (var error in state.Value.Errors)
|
||||
{
|
||||
Console.WriteLine("\t" + error.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// assert
|
||||
|
||||
Assert.IsFalse(isValid);
|
||||
Assert.AreEqual(11, modelState.Keys.Count);
|
||||
const string complexPropertyKey = "_Properties.complex.invariant.null";
|
||||
Assert.IsTrue(modelState.Keys.Contains(complexPropertyKey));
|
||||
foreach (var state in modelState.Where(x => x.Key != complexPropertyKey))
|
||||
{
|
||||
foreach (var error in state.Value.Errors)
|
||||
{
|
||||
Assert.IsFalse(error.ErrorMessage.DetectIsJson()); // non complex is just an error message
|
||||
}
|
||||
}
|
||||
var complexEditorErrors = modelState.Single(x => x.Key == complexPropertyKey).Value.Errors;
|
||||
Assert.AreEqual(1, complexEditorErrors.Count);
|
||||
var nestedError = complexEditorErrors[0];
|
||||
var jsonError = JsonConvert.DeserializeObject<JArray>(nestedError.ErrorMessage);
|
||||
|
||||
var modelStateKeys = new[] { "_Properties.title.invariant.null.innerFieldId", "_Properties.title.invariant.null.value", "_Properties.bodyText.invariant.null.innerFieldId", "_Properties.bodyText.invariant.null.value" };
|
||||
AssertNestedValidation(jsonError, 0, id1, modelStateKeys);
|
||||
AssertNestedValidation(jsonError, 1, id2, modelStateKeys.Concat(new[] { "_Properties.complex.invariant.null.innerFieldId", "_Properties.complex.invariant.null.value" }).ToArray());
|
||||
var nestedJsonError = jsonError.SelectToken("$[1].complex") as JArray;
|
||||
Assert.IsNotNull(nestedJsonError);
|
||||
AssertNestedValidation(nestedJsonError, 0, id3, modelStateKeys);
|
||||
}
|
||||
|
||||
private void AssertNestedValidation(JArray jsonError, int index, Guid id, string[] modelStateKeys)
|
||||
{
|
||||
Assert.IsNotNull(jsonError.SelectToken("$[" + index + "]"));
|
||||
Assert.AreEqual(id.ToString(), jsonError.SelectToken("$[" + index + "].$id").Value<string>());
|
||||
Assert.AreEqual("textPage", jsonError.SelectToken("$[" + index + "].$elementTypeAlias").Value<string>());
|
||||
Assert.IsNotNull(jsonError.SelectToken("$[" + index + "].ModelState"));
|
||||
foreach (var key in modelStateKeys)
|
||||
{
|
||||
var error = jsonError.SelectToken("$[" + index + "].ModelState['" + key + "']") as JArray;
|
||||
Assert.IsNotNull(error);
|
||||
Assert.AreEqual(1, error.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[HideFromTypeFinder]
|
||||
[DataEditor("complexTest", "test", "test")]
|
||||
public class ComplexTestEditor : NestedContentPropertyEditor
|
||||
{
|
||||
public ComplexTestEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base(logger, propertyEditors, dataTypeService, contentTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IDataValueEditor CreateValueEditor()
|
||||
{
|
||||
var editor = base.CreateValueEditor();
|
||||
editor.Validators.Add(new NeverValidateValidator());
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
|
||||
[HideFromTypeFinder]
|
||||
[DataEditor("test", "test", "test")] // This alias aligns with the prop editor alias for all properties created from MockedContentTypes.CreateTextPageContentType
|
||||
public class TestEditor : DataEditor
|
||||
{
|
||||
public TestEditor(ILogger logger)
|
||||
: base(logger)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IDataValueEditor CreateValueEditor() => new TestValueEditor(Attribute);
|
||||
|
||||
private class TestValueEditor : DataValueEditor
|
||||
{
|
||||
public TestValueEditor(DataEditorAttribute attribute)
|
||||
: base(attribute)
|
||||
{
|
||||
Validators.Add(new NeverValidateValidator());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class NeverValidateValidator : IValueValidator
|
||||
{
|
||||
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
|
||||
{
|
||||
yield return new ValidationResult("WRONG!", new[] { "innerFieldId" });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using NUnit.Framework;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web;
|
||||
|
||||
namespace Umbraco.Tests.Web
|
||||
namespace Umbraco.Tests.Web.Validation
|
||||
{
|
||||
[TestFixture]
|
||||
public class ModelStateExtensionsTests
|
||||
@@ -31,6 +31,6 @@ exports.build = series(parallel(dependencies, js, less, views), testUnit);
|
||||
exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views), watchTask);
|
||||
exports.watch = series(watchTask);
|
||||
//
|
||||
exports.runTests = series(setTestMode, parallel(js, testUnit));
|
||||
exports.runUnit = series(setTestMode, parallel(js, runUnitTestServer), watchTask);
|
||||
exports.runTests = series(setTestMode, series(js, testUnit));
|
||||
exports.runUnit = series(setTestMode, series(js, runUnitTestServer), watchTask);
|
||||
exports.testE2e = series(setTestMode, parallel(testE2e));
|
||||
|
||||
@@ -69,6 +69,18 @@
|
||||
};
|
||||
}
|
||||
|
||||
if (!String.prototype.trimStartSpecial) {
|
||||
/** trimSpecial extension method for string */
|
||||
// Removes all non printable chars from beginning of a string
|
||||
String.prototype.trimStartSpecial = function () {
|
||||
var index = 0;
|
||||
while (this.charCodeAt(index) <= 46) {
|
||||
index++;
|
||||
}
|
||||
return this.substr(index);
|
||||
};
|
||||
}
|
||||
|
||||
if (!String.prototype.startsWith) {
|
||||
/** startsWith extension method for string */
|
||||
String.prototype.startsWith = function (str) {
|
||||
|
||||
16253
src/Umbraco.Web.UI.Client/package-lock.json
generated
Normal file
16253
src/Umbraco.Web.UI.Client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "gulp runTests",
|
||||
"unit": "gulp testUnit",
|
||||
"unit": "gulp runUnit",
|
||||
"e2e": "gulp testE2e",
|
||||
"build": "gulp build",
|
||||
"dev": "gulp dev",
|
||||
@@ -73,6 +73,7 @@
|
||||
"gulp-wrap-js": "0.4.1",
|
||||
"jasmine-core": "3.5.0",
|
||||
"karma": "4.4.1",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-jasmine": "2.0.1",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-phantomjs-launcher": "1.0.4",
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
@scope
|
||||
|
||||
@description
|
||||
<b>Added in Umbraco 7.8</b>. The tour component is a global component and is already added to the umbraco markup.
|
||||
In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco.
|
||||
You can easily add you own tours to the Help-drawer or show and start tours from
|
||||
<b>Added in Umbraco 7.8</b>. The tour component is a global component and is already added to the umbraco markup.
|
||||
In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco.
|
||||
You can easily add you own tours to the Help-drawer or show and start tours from
|
||||
anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install <a href="https://our.umbraco.com/projects/starter-kits/the-starter-kit/">The Starter Kit</a> in Umbraco 7.8
|
||||
|
||||
<h1><b>Extending the help drawer with custom tours</b></h1>
|
||||
The easiest way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
|
||||
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
|
||||
The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
|
||||
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
|
||||
picked up by Umbraco and shown in the Help-drawer.
|
||||
|
||||
<h3><b>The tour object</b></h3>
|
||||
@@ -26,7 +26,7 @@ The tour object consist of two parts - The overall tour configuration and a list
|
||||
"groupOrder": 200 // Control the order of tour groups
|
||||
"allowDisable": // Adds a "Don't" show this tour again"-button to the intro step
|
||||
"culture" : // From v7.11+. Specifies the culture of the tour (eg. en-US), if set the tour will only be shown to users with this culture set on their profile. If omitted or left empty the tour will be visible to all users
|
||||
"requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
|
||||
"requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
|
||||
"steps": [] // tour steps - see next example
|
||||
}
|
||||
</pre>
|
||||
@@ -43,11 +43,12 @@ The tour object consist of two parts - The overall tour configuration and a list
|
||||
"backdropOpacity": 0.4 // the backdrop opacity
|
||||
"view": "" // add a custom view
|
||||
"customProperties" : {} // add any custom properties needed for the custom view
|
||||
"skipStepIfVisible": ".dashboard div [data-element='my-tour-button']" // if we can find this DOM element on the page then we will skip this step
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h1><b>Adding tours to other parts of the Umbraco backoffice</b></h1>
|
||||
It is also possible to add a list of custom tours to other parts of the Umbraco backoffice,
|
||||
It is also possible to add a list of custom tours to other parts of the Umbraco backoffice,
|
||||
as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service.
|
||||
|
||||
<h1><b>Using the tour service</b></h1>
|
||||
@@ -86,7 +87,8 @@ as an example on a Dashboard in a Custom section. You can then use the {@link um
|
||||
"element": "[data-element='my-tour-button']",
|
||||
"title": "Click the button",
|
||||
"content": "Click the button",
|
||||
"event": "click"
|
||||
"event": "click",
|
||||
"skipStepIfVisible": "[data-element='my-other-tour-button']"
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -257,9 +259,26 @@ In the following example you see how to run some custom logic before a step goes
|
||||
|
||||
// make sure we don't go too far
|
||||
if (scope.model.currentStepIndex !== scope.model.steps.length) {
|
||||
|
||||
var upcomingStep = scope.model.steps[scope.model.currentStepIndex];
|
||||
|
||||
// If the currentStep JSON object has 'skipStepIfVisible'
|
||||
// It's a DOM selector - if we find it then we ship over this step
|
||||
if (upcomingStep.skipStepIfVisible) {
|
||||
let tryFindDomEl = document.querySelector(upcomingStep.skipStepIfVisible);
|
||||
if (tryFindDomEl) {
|
||||
// check if element is visible:
|
||||
if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) {
|
||||
// if it was visible then we skip the step.
|
||||
nextStep();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startStep();
|
||||
// tour completed - final step
|
||||
} else {
|
||||
// tour completed - final step
|
||||
scope.loadingStep = true;
|
||||
|
||||
waitForPendingRerequests().then(function () {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
function ContentEditController($rootScope, $scope, $routeParams, $q, $window,
|
||||
appState, contentResource, entityResource, navigationService, notificationsService,
|
||||
serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper,
|
||||
editorState, $http, eventsService, overlayService, $location, localStorageService, treeService) {
|
||||
editorState, $http, eventsService, overlayService, $location, localStorageService, treeService,
|
||||
$exceptionHandler) {
|
||||
|
||||
var evts = [];
|
||||
var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode;
|
||||
@@ -521,6 +522,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleHttpException(err) {
|
||||
if (err && !err.status) {
|
||||
$exceptionHandler(err);
|
||||
}
|
||||
}
|
||||
|
||||
/** Just shows a simple notification that there are client side validation issues to be fixed */
|
||||
function showValidationNotification() {
|
||||
//TODO: We need to make the validation UI much better, there's a lot of inconsistencies in v8 including colors, issues with the property groups and validation errors between variants
|
||||
@@ -561,6 +568,7 @@
|
||||
view: "views/content/overlays/unpublish.html",
|
||||
variants: $scope.content.variants, //set a model property for the dialog
|
||||
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
|
||||
includeUnpublished: false,
|
||||
submitButtonLabelKey: "content_unpublish",
|
||||
submitButtonStyle: "warning",
|
||||
submit: function (model) {
|
||||
@@ -581,6 +589,7 @@
|
||||
overlayService.close();
|
||||
}, function (err) {
|
||||
$scope.page.buttonGroupState = 'error';
|
||||
handleHttpException(err);
|
||||
});
|
||||
|
||||
|
||||
@@ -626,8 +635,7 @@
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = $scope.content.variants;
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
handleHttpException(err);
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
@@ -648,8 +656,9 @@
|
||||
action: "sendToPublish"
|
||||
}).then(function () {
|
||||
$scope.page.buttonGroupState = "success";
|
||||
}, function () {
|
||||
}, function (err) {
|
||||
$scope.page.buttonGroupState = "error";
|
||||
handleHttpException(err);
|
||||
});;
|
||||
}
|
||||
};
|
||||
@@ -685,8 +694,7 @@
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = $scope.content.variants;
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
handleHttpException(err);
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
@@ -709,8 +717,9 @@
|
||||
action: "publish"
|
||||
}).then(function () {
|
||||
$scope.page.buttonGroupState = "success";
|
||||
}, function () {
|
||||
}, function (err) {
|
||||
$scope.page.buttonGroupState = "error";
|
||||
handleHttpException(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -748,8 +757,7 @@
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = $scope.content.variants;
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
handleHttpException(err);
|
||||
});
|
||||
},
|
||||
close: function (oldModel) {
|
||||
@@ -772,8 +780,9 @@
|
||||
action: "save"
|
||||
}).then(function () {
|
||||
$scope.page.saveButtonState = "success";
|
||||
}, function () {
|
||||
}, function (err) {
|
||||
$scope.page.saveButtonState = "error";
|
||||
handleHttpException(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -826,8 +835,7 @@
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = Utilities.copy($scope.content.variants);
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
handleHttpException(err);
|
||||
});
|
||||
|
||||
},
|
||||
@@ -886,8 +894,7 @@
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = $scope.content.variants;
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
handleHttpException(err);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
vm.selectAppAnchor = selectAppAnchor;
|
||||
vm.requestSplitView = requestSplitView;
|
||||
|
||||
vm.getScope = getScope;// used by property editors to get a scope that is the root of split view, content apps etc.
|
||||
|
||||
//Used to track how many content views there are (for split view there will be 2, it could support more in theory)
|
||||
vm.editors = [];
|
||||
|
||||
@@ -244,6 +246,10 @@
|
||||
vm.onSelectAppAnchor({"app": app, "anchor": anchor});
|
||||
}
|
||||
}
|
||||
function getScope() {
|
||||
return $scope;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('umbraco.directives').component('umbVariantContentEditors', umbVariantContentEditors);
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
'use strict';
|
||||
|
||||
function EditorContentHeader(serverValidationManager, localizationService, editorState) {
|
||||
function link(scope) {
|
||||
function link(scope) {
|
||||
|
||||
var unsubscribe = [];
|
||||
|
||||
|
||||
if (!scope.serverValidationNameField) {
|
||||
scope.serverValidationNameField = "Name";
|
||||
}
|
||||
@@ -46,55 +46,55 @@
|
||||
scope.vm.variantsWithError = [];
|
||||
scope.vm.defaultVariant = null;
|
||||
scope.vm.errorsOnOtherVariants = false;// indicating wether to show that other variants, than the current, have errors.
|
||||
|
||||
|
||||
function updateVaraintErrors() {
|
||||
scope.content.variants.forEach( function (variant) {
|
||||
scope.content.variants.forEach(function (variant) {
|
||||
variant.hasError = scope.variantHasError(variant);
|
||||
|
||||
|
||||
});
|
||||
checkErrorsOnOtherVariants();
|
||||
}
|
||||
|
||||
function checkErrorsOnOtherVariants() {
|
||||
var check = false;
|
||||
scope.content.variants.forEach( function (variant) {
|
||||
scope.content.variants.forEach(function (variant) {
|
||||
if (variant.active !== true && variant.hasError) {
|
||||
check = true;
|
||||
}
|
||||
});
|
||||
scope.vm.errorsOnOtherVariants = check;
|
||||
}
|
||||
|
||||
|
||||
function onVariantValidation(valid, errors, allErrors, culture, segment) {
|
||||
|
||||
// only want to react to property errors:
|
||||
if(errors.findIndex(error => {return error.propertyAlias !== null;}) === -1) {
|
||||
if (errors.findIndex(error => { return error.propertyAlias !== null; }) === -1) {
|
||||
// we dont have any errors for properties, meaning we will back out.
|
||||
return;
|
||||
}
|
||||
|
||||
// If error coming back is invariant, we will assign the error to the default variant by picking the defaultVariant language.
|
||||
if(culture === "invariant") {
|
||||
if (culture === "invariant" && scope.vm.defaultVariant) {
|
||||
culture = scope.vm.defaultVariant.language.culture;
|
||||
}
|
||||
|
||||
var index = scope.vm.variantsWithError.findIndex((item) => item.culture === culture && item.segment === segment)
|
||||
if(valid === true) {
|
||||
if (valid === true) {
|
||||
if (index !== -1) {
|
||||
scope.vm.variantsWithError.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
if (index === -1) {
|
||||
scope.vm.variantsWithError.push({"culture": culture, "segment": segment});
|
||||
scope.vm.variantsWithError.push({ "culture": culture, "segment": segment });
|
||||
}
|
||||
}
|
||||
scope.$evalAsync(updateVaraintErrors);
|
||||
}
|
||||
|
||||
|
||||
function onInit() {
|
||||
|
||||
|
||||
// find default + check if we have variants.
|
||||
scope.content.variants.forEach( function (variant) {
|
||||
scope.content.variants.forEach(function (variant) {
|
||||
if (variant.language !== null && variant.language.isDefault) {
|
||||
scope.vm.defaultVariant = variant;
|
||||
}
|
||||
@@ -113,41 +113,41 @@
|
||||
|
||||
scope.vm.variantMenu = [];
|
||||
if (scope.vm.hasCulture) {
|
||||
scope.content.variants.forEach( (v) => {
|
||||
scope.content.variants.forEach((v) => {
|
||||
if (v.language !== null && v.segment === null) {
|
||||
var variantMenuEntry = {
|
||||
key: String.CreateGuid(),
|
||||
open: v.language && v.language.culture === scope.editor.culture,
|
||||
variant: v,
|
||||
subVariants: scope.content.variants.filter( (subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null)
|
||||
subVariants: scope.content.variants.filter((subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null)
|
||||
};
|
||||
scope.vm.variantMenu.push(variantMenuEntry);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
scope.content.variants.forEach( (v) => {
|
||||
scope.content.variants.forEach((v) => {
|
||||
scope.vm.variantMenu.push({
|
||||
key: String.CreateGuid(),
|
||||
variant: v
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
scope.editor.variantApps.forEach( (app) => {
|
||||
|
||||
scope.editor.variantApps.forEach((app) => {
|
||||
if (app.alias === "umbContent") {
|
||||
app.anchors = scope.editor.content.tabs;
|
||||
}
|
||||
});
|
||||
|
||||
scope.content.variants.forEach( function (variant) {
|
||||
|
||||
scope.content.variants.forEach(function (variant) {
|
||||
|
||||
// if we are looking for the variant with default language then we also want to check for invariant variant.
|
||||
if (variant.language && variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) {
|
||||
if (variant.language && scope.vm.defaultVariant && variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) {
|
||||
unsubscribe.push(serverValidationManager.subscribe(null, "invariant", null, onVariantValidation, null));
|
||||
}
|
||||
unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, null, onVariantValidation, variant.segment));
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
scope.goBack = function () {
|
||||
@@ -164,15 +164,15 @@
|
||||
}
|
||||
};
|
||||
|
||||
scope.selectNavigationItem = function(item) {
|
||||
if(scope.onSelectNavigationItem) {
|
||||
scope.onSelectNavigationItem({"item": item});
|
||||
scope.selectNavigationItem = function (item) {
|
||||
if (scope.onSelectNavigationItem) {
|
||||
scope.onSelectNavigationItem({ "item": item });
|
||||
}
|
||||
}
|
||||
|
||||
scope.selectAnchorItem = function(item, anchor) {
|
||||
if(scope.onSelectAnchorItem) {
|
||||
scope.onSelectAnchorItem({"item": item, "anchor": anchor});
|
||||
scope.selectAnchorItem = function (item, anchor) {
|
||||
if (scope.onSelectAnchorItem) {
|
||||
scope.onSelectAnchorItem({ "item": item, "anchor": anchor });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,20 +188,20 @@
|
||||
scope.onOpenInSplitView({ "variant": variant });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check whether a variant has a error, used to display errors in variant switcher.
|
||||
* @param {any} culture
|
||||
*/
|
||||
scope.variantHasError = function(variant) {
|
||||
if(scope.vm.variantsWithError.find((item) => (!variant.language || item.culture === variant.language.culture) && item.segment === variant.segment) !== undefined) {
|
||||
scope.variantHasError = function (variant) {
|
||||
if (scope.vm.variantsWithError.find((item) => (!variant.language || item.culture === variant.language.culture) && item.segment === variant.segment) !== undefined) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onInit();
|
||||
|
||||
|
||||
scope.$on('$destroy', function () {
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
var sectionId = '#leftcolumn';
|
||||
var isLeftColumnAbove = false;
|
||||
scope.editors = [];
|
||||
|
||||
|
||||
function addEditor(editor) {
|
||||
editor.inFront = true;
|
||||
editor.moveRight = true;
|
||||
editor.level = 0;
|
||||
editor.styleIndex = 0;
|
||||
|
||||
|
||||
// push the new editor to the dom
|
||||
scope.editors.push(editor);
|
||||
|
||||
@@ -32,20 +32,20 @@
|
||||
$timeout(() => {
|
||||
editor.moveRight = false;
|
||||
})
|
||||
|
||||
|
||||
editor.animating = true;
|
||||
setTimeout(revealEditorContent.bind(this, editor), 400);
|
||||
|
||||
|
||||
updateEditors();
|
||||
|
||||
}
|
||||
|
||||
|
||||
function removeEditor(editor) {
|
||||
editor.moveRight = true;
|
||||
|
||||
|
||||
editor.animating = true;
|
||||
setTimeout(removeEditorFromDOM.bind(this, editor), 400);
|
||||
|
||||
|
||||
updateEditors(-1);
|
||||
|
||||
if(scope.editors.length === 1){
|
||||
@@ -56,17 +56,17 @@
|
||||
isLeftColumnAbove = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function revealEditorContent(editor) {
|
||||
|
||||
|
||||
editor.animating = false;
|
||||
|
||||
|
||||
scope.$digest();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
function removeEditorFromDOM(editor) {
|
||||
|
||||
|
||||
// push the new editor to the dom
|
||||
var index = scope.editors.indexOf(editor);
|
||||
if (index !== -1) {
|
||||
@@ -74,42 +74,42 @@
|
||||
}
|
||||
|
||||
updateEditors();
|
||||
|
||||
|
||||
scope.$digest();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** update layer positions. With ability to offset positions, needed for when an item is moving out, then we dont want it to influence positions */
|
||||
function updateEditors(offset) {
|
||||
|
||||
|
||||
offset = offset || 0;// fallback value.
|
||||
|
||||
|
||||
var len = scope.editors.length;
|
||||
var calcLen = len + offset;
|
||||
var ceiling = Math.min(calcLen, allowedNumberOfVisibleEditors);
|
||||
var origin = Math.max(calcLen-1, 0)-ceiling;
|
||||
var origin = Math.max(calcLen - 1, 0) - ceiling;
|
||||
var i = 0;
|
||||
while(i<len) {
|
||||
while (i < len) {
|
||||
var iEditor = scope.editors[i];
|
||||
iEditor.styleIndex = Math.min(i+1, allowedNumberOfVisibleEditors);
|
||||
iEditor.level = Math.max(i-origin, -1);
|
||||
iEditor.styleIndex = Math.min(i + 1, allowedNumberOfVisibleEditors);
|
||||
iEditor.level = Math.max(i - origin, -1);
|
||||
iEditor.inFront = iEditor.level >= ceiling;
|
||||
i++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
evts.push(eventsService.on("appState.editors.open", function (name, args) {
|
||||
addEditor(args.editor);
|
||||
}));
|
||||
|
||||
evts.push(eventsService.on("appState.editors.close", function (name, args) {
|
||||
// remove the closed editor
|
||||
if(args && args.editor) {
|
||||
if (args && args.editor) {
|
||||
removeEditor(args.editor);
|
||||
}
|
||||
// close all editors
|
||||
if(args && !args.editor && args.editors.length === 0) {
|
||||
if (args && !args.editor && args.editors.length === 0) {
|
||||
scope.editors = [];
|
||||
}
|
||||
}));
|
||||
@@ -134,6 +134,74 @@
|
||||
|
||||
}
|
||||
|
||||
// This directive allows for us to run a custom $compile for the view within the repeater which allows
|
||||
// us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the
|
||||
// infinite editing. The retain the $scope hiearchy a special $parentScope property is passed in to the model.
|
||||
function EditorRepeaterDirective($http, $templateCache, $compile, angularHelper) {
|
||||
function link(scope, el, attr, ctrl) {
|
||||
|
||||
var editor = scope && scope.$parent ? scope.$parent.model : null;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
var unsubscribe = [];
|
||||
|
||||
//if a custom parent scope is defined then we need to manually compile the view
|
||||
if (editor.$parentScope) {
|
||||
var element = el.find(".scoped-view");
|
||||
$http.get(editor.view, { cache: $templateCache })
|
||||
.then(function (response) {
|
||||
var templateScope = editor.$parentScope.$new();
|
||||
|
||||
unsubscribe.push(function () {
|
||||
templateScope.$destroy();
|
||||
});
|
||||
|
||||
// NOTE: the 'model' name here directly affects the naming convention used in infinite editors, this why you access the model
|
||||
// like $scope.model.If this is changed, everything breaks.This is because we are entirely reliant upon ng-include and inheriting $scopes.
|
||||
// by default without a $parentScope used for infinite editing the 'model' propety will be set because the view creates the scopes in
|
||||
// ng-repeat by ng-repeat="model in editors"
|
||||
templateScope.model = editor;
|
||||
|
||||
element.show();
|
||||
|
||||
// if a parentForm is supplied then we can link them but to do that we need to inject a top level form
|
||||
if (editor.$parentForm) {
|
||||
element.html("<ng-form name='infiniteEditorForm'>" + response.data + "</ng-form>");
|
||||
}
|
||||
|
||||
$compile(element)(templateScope);
|
||||
|
||||
// if a parentForm is supplied then we can link them
|
||||
if (editor.$parentForm) {
|
||||
editor.$parentForm.$addControl(templateScope.infiniteEditorForm);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.$on('$destroy', function () {
|
||||
for (var i = 0; i < unsubscribe.length; i++) {
|
||||
unsubscribe[i]();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
scope: {
|
||||
editors: "="
|
||||
},
|
||||
template: "<div ng-transclude></div>",
|
||||
link: link
|
||||
};
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
angular.module('umbraco.directives').directive('umbEditors', EditorsDirective);
|
||||
angular.module('umbraco.directives').directive('umbEditorRepeater', EditorRepeaterDirective);
|
||||
|
||||
})();
|
||||
|
||||
@@ -2,13 +2,20 @@ angular.module("umbraco.directives").directive('focusWhen', function ($timeout)
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, elm, attrs, ctrl) {
|
||||
|
||||
var delayTimer;
|
||||
|
||||
attrs.$observe("focusWhen", function (newValue) {
|
||||
if (newValue === "true") {
|
||||
$timeout(function () {
|
||||
elm.trigger("focus");
|
||||
});
|
||||
if (newValue === "true" && document.activeelement !== elm[0]) {
|
||||
delayTimer = $timeout(function () {
|
||||
elm[0].focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
$timeout.cancel(delayTimer);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -284,7 +284,7 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
templateScope.model = scope.model;
|
||||
element.html(response.data);
|
||||
element.show();
|
||||
$compile(element.contents())(templateScope);
|
||||
$compile(element)(templateScope);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -473,7 +473,7 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
|
||||
scope.submitForm = function (model) {
|
||||
if (scope.model.submit) {
|
||||
if (formHelper.submitForm({ scope: scope, skipValidation: scope.model.skipFormValidation })) {
|
||||
if (formHelper.submitForm({ scope: scope, skipValidation: scope.model.skipFormValidation, keepServerValidation: true })) {
|
||||
|
||||
if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) {
|
||||
//wrap in a when since we don't know if this is a promise or not
|
||||
|
||||
@@ -3,46 +3,75 @@
|
||||
* @name umbraco.directives.directive:umbProperty
|
||||
* @restrict E
|
||||
**/
|
||||
angular.module("umbraco.directives")
|
||||
.directive('umbProperty', function (userService) {
|
||||
return {
|
||||
scope: {
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module("umbraco.directives")
|
||||
.component('umbProperty', {
|
||||
templateUrl: 'views/components/property/umb-property.html',
|
||||
controller: UmbPropertyController,
|
||||
controllerAs: 'vm',
|
||||
transclude: true,
|
||||
require: {
|
||||
parentUmbProperty: '?^^umbProperty',
|
||||
parentForm: '?^^form'
|
||||
},
|
||||
bindings: {
|
||||
property: "=",
|
||||
elementKey: "@",
|
||||
// optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/)
|
||||
propertyAlias: "@",
|
||||
showInherit: "<",
|
||||
inheritsFrom: "<"
|
||||
},
|
||||
transclude: true,
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'views/components/property/umb-property.html',
|
||||
link: function (scope) {
|
||||
|
||||
scope.controlLabelTitle = null;
|
||||
if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) {
|
||||
userService.getCurrentUser().then(function (u) {
|
||||
if(u.allowedSections.indexOf("settings") !== -1 ? true : false) {
|
||||
scope.controlLabelTitle = scope.property.alias;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
//Define a controller for this directive to expose APIs to other directives
|
||||
controller: function ($scope) {
|
||||
|
||||
var self = this;
|
||||
|
||||
//set the API properties/methods
|
||||
|
||||
self.property = $scope.property;
|
||||
self.setPropertyError = function (errorMsg) {
|
||||
$scope.property.propertyErrorMessage = errorMsg;
|
||||
};
|
||||
|
||||
$scope.propertyActions = [];
|
||||
self.setPropertyActions = function(actions) {
|
||||
$scope.propertyActions = actions;
|
||||
};
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
function UmbPropertyController($scope, userService, serverValidationManager, udiService, angularHelper) {
|
||||
|
||||
const vm = this;
|
||||
|
||||
vm.$onInit = onInit;
|
||||
|
||||
vm.setPropertyError = function (errorMsg) {
|
||||
vm.property.propertyErrorMessage = errorMsg;
|
||||
};
|
||||
});
|
||||
|
||||
vm.propertyActions = [];
|
||||
vm.setPropertyActions = function (actions) {
|
||||
vm.propertyActions = actions;
|
||||
};
|
||||
|
||||
// returns the validation path for the property to be used as the validation key for server side validation logic
|
||||
vm.getValidationPath = function () {
|
||||
|
||||
var parentValidationPath = vm.parentUmbProperty ? vm.parentUmbProperty.getValidationPath() : null;
|
||||
var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias;
|
||||
// the elementKey will be empty when this is not a nested property
|
||||
var valPath = vm.elementKey ? vm.elementKey + "/" + propAlias : propAlias;
|
||||
return serverValidationManager.createPropertyValidationKey(valPath, parentValidationPath);
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
vm.controlLabelTitle = null;
|
||||
if (Umbraco.Sys.ServerVariables.isDebuggingEnabled) {
|
||||
userService.getCurrentUser().then(function (u) {
|
||||
if (u.allowedSections.indexOf("settings") !== -1 ? true : false) {
|
||||
vm.controlLabelTitle = vm.property.alias;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!vm.parentUmbProperty) {
|
||||
// not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope
|
||||
// inheritance is (i.e.infinite editing)
|
||||
var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "UmbPropertyController");
|
||||
vm.parentUmbProperty = found ? found.vm : null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -9,13 +9,14 @@ function treeSearchResults() {
|
||||
return {
|
||||
scope: {
|
||||
results: "=",
|
||||
selectResultCallback: "="
|
||||
selectResultCallback: "=",
|
||||
emptySearchResultPosition: '@'
|
||||
},
|
||||
restrict: "E", // restrict to an element
|
||||
replace: true, // replace the html element with the template
|
||||
templateUrl: 'views/components/tree/umb-tree-search-results.html',
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
|
||||
scope.emptySearchResultPosition = scope.emptySearchResultPosition || "center";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function umbTextarea($document) {
|
||||
|
||||
function autogrow(scope, element, attributes) {
|
||||
if (!element.hasClass("autogrow")) {
|
||||
// no autogrow for you today
|
||||
return;
|
||||
}
|
||||
|
||||
// get possible minimum height style
|
||||
var minHeight = parseInt(window.getComputedStyle(element[0]).getPropertyValue("min-height")) || 0;
|
||||
|
||||
// prevent newlines in textbox
|
||||
element.on("keydown", function (evt) {
|
||||
if (evt.which === 13) {
|
||||
//evt.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
element.on("input", function (evt) {
|
||||
element.css({
|
||||
height: 'auto',
|
||||
minHeight: 0
|
||||
});
|
||||
|
||||
var contentHeight = this.scrollHeight;
|
||||
var borderHeight = 1;
|
||||
var paddingHeight = 4;
|
||||
|
||||
element.css({
|
||||
minHeight: null, // remove property
|
||||
height: contentHeight + borderHeight + paddingHeight + "px" // because we're using border-box
|
||||
});
|
||||
});
|
||||
|
||||
// watch model changes from the outside to adjust height
|
||||
scope.$watch(attributes.ngModel, trigger);
|
||||
|
||||
// set initial size
|
||||
trigger();
|
||||
|
||||
function trigger() {
|
||||
setTimeout(element.triggerHandler.bind(element, "input"), 1);
|
||||
}
|
||||
}
|
||||
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
link: autogrow
|
||||
};
|
||||
|
||||
return directive;
|
||||
}
|
||||
|
||||
angular.module('umbraco.directives').directive('textarea', umbTextarea);
|
||||
|
||||
})();
|
||||
@@ -12,48 +12,88 @@
|
||||
* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will
|
||||
* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly.
|
||||
**/
|
||||
function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService) {
|
||||
function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) {
|
||||
|
||||
var SHOW_VALIDATION_CLASS_NAME = "show-validation";
|
||||
var SAVING_EVENT_NAME = "formSubmitting";
|
||||
var SAVED_EVENT_NAME = "formSubmitted";
|
||||
|
||||
function notify(scope) {
|
||||
scope.$broadcast("valStatusChanged", { form: scope.formCtrl });
|
||||
}
|
||||
|
||||
function ValFormManagerController($scope) {
|
||||
//This exposes an API for direct use with this directive
|
||||
|
||||
// We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and
|
||||
// because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is
|
||||
// an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager
|
||||
// in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing)
|
||||
$scope.valFormManager = this;
|
||||
|
||||
var unsubscribe = [];
|
||||
var self = this;
|
||||
|
||||
//This is basically the same as a directive subscribing to an event but maybe a little
|
||||
// nicer since the other directive can use this directive's API instead of a magical event
|
||||
this.onValidationStatusChanged = function (cb) {
|
||||
unsubscribe.push($scope.$on("valStatusChanged", function (evt, args) {
|
||||
cb.apply(self, [evt, args]);
|
||||
}));
|
||||
};
|
||||
|
||||
this.isShowingValidation = () => $scope.showValidation === true;
|
||||
|
||||
this.notify = function () {
|
||||
notify($scope);
|
||||
}
|
||||
|
||||
this.isValid = function () {
|
||||
return !$scope.formCtrl.$invalid;
|
||||
}
|
||||
|
||||
//Ensure to remove the event handlers when this instance is destroyted
|
||||
$scope.$on('$destroy', function () {
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find's the valFormManager in the scope/DOM hierarchy
|
||||
* @param {any} scope
|
||||
* @param {any} ctrls
|
||||
* @param {any} index
|
||||
*/
|
||||
function getAncestorValFormManager(scope, ctrls, index) {
|
||||
|
||||
// first check the normal directive inheritance which relies on DOM inheritance
|
||||
var found = ctrls[index];
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
||||
// not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope
|
||||
// inheritance is (i.e.infinite editing)
|
||||
var found = angularHelper.traverseScopeChain(scope, s => s && s.valFormManager && s.valFormManager.constructor.name === "ValFormManagerController");
|
||||
return found ? found.valFormManager : null;
|
||||
}
|
||||
|
||||
return {
|
||||
require: ["form", "^^?valFormManager", "^^?valSubView"],
|
||||
restrict: "A",
|
||||
controller: function($scope) {
|
||||
//This exposes an API for direct use with this directive
|
||||
|
||||
var unsubscribe = [];
|
||||
var self = this;
|
||||
|
||||
//This is basically the same as a directive subscribing to an event but maybe a little
|
||||
// nicer since the other directive can use this directive's API instead of a magical event
|
||||
this.onValidationStatusChanged = function (cb) {
|
||||
unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) {
|
||||
cb.apply(self, [evt, args]);
|
||||
}));
|
||||
};
|
||||
|
||||
this.showValidation = $scope.showValidation === true;
|
||||
|
||||
//Ensure to remove the event handlers when this instance is destroyted
|
||||
$scope.$on('$destroy', function () {
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
}
|
||||
});
|
||||
},
|
||||
controller: ValFormManagerController,
|
||||
link: function (scope, element, attr, ctrls) {
|
||||
|
||||
function notifySubView() {
|
||||
if (subView){
|
||||
if (subView) {
|
||||
subView.valStatusChanged({ form: formCtrl, showValidation: scope.showValidation });
|
||||
}
|
||||
}
|
||||
|
||||
var formCtrl = ctrls[0];
|
||||
var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null;
|
||||
var formCtrl = scope.formCtrl = ctrls[0];
|
||||
var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1);
|
||||
var subView = ctrls.length > 1 ? ctrls[2] : null;
|
||||
var labels = {};
|
||||
|
||||
@@ -72,45 +112,22 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
|
||||
});
|
||||
|
||||
//watch the list of validation errors to notify the application of any validation changes
|
||||
scope.$watch(function () {
|
||||
//the validators are in the $error collection: https://docs.angularjs.org/api/ng/type/form.FormController#$error
|
||||
//since each key is the validator name (i.e. 'required') we can't just watch the number of keys, we need to watch
|
||||
//the sum of the items inside of each key
|
||||
scope.$watch(() => angularHelper.countAllFormErrors(formCtrl),
|
||||
function (e) {
|
||||
|
||||
notify(scope);
|
||||
|
||||
notifySubView();
|
||||
|
||||
//find all invalid elements' .control-group's and apply the error class
|
||||
var inError = element.find(".control-group .ng-invalid").closest(".control-group");
|
||||
inError.addClass("error");
|
||||
|
||||
//find all control group's that have no error and ensure the class is removed
|
||||
var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError);
|
||||
noInError.removeClass("error");
|
||||
|
||||
//get the lengths of each array for each key in the $error collection
|
||||
var validatorLengths = _.map(formCtrl.$error, function (val, key) {
|
||||
// if there are child ng-forms, include the $error collections in those as well
|
||||
var innerErrorCount = _.reduce(
|
||||
_.map(val, v =>
|
||||
_.reduce(
|
||||
_.map(v.$error, e => e.length),
|
||||
(m, n) => m + n
|
||||
)
|
||||
),
|
||||
(memo, num) => memo + num
|
||||
);
|
||||
return val.length + innerErrorCount;
|
||||
});
|
||||
//sum up all numbers in the resulting array
|
||||
var sum = _.reduce(validatorLengths, function (memo, num) {
|
||||
return memo + num;
|
||||
}, 0);
|
||||
//this is the value we watch to notify of any validation changes on the form
|
||||
return sum;
|
||||
}, function (e) {
|
||||
scope.$broadcast("valStatusChanged", { form: formCtrl });
|
||||
|
||||
notifySubView();
|
||||
|
||||
//find all invalid elements' .control-group's and apply the error class
|
||||
var inError = element.find(".control-group .ng-invalid").closest(".control-group");
|
||||
inError.addClass("error");
|
||||
|
||||
//find all control group's that have no error and ensure the class is removed
|
||||
var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError);
|
||||
noInError.removeClass("error");
|
||||
|
||||
});
|
||||
|
||||
//This tracks if the user is currently saving a new item, we use this to determine
|
||||
// if we should display the warning dialog that they are leaving the page - if a new item
|
||||
@@ -119,7 +136,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
|
||||
var isSavingNewItem = false;
|
||||
|
||||
//we should show validation if there are any msgs in the server validation collection
|
||||
if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.showValidation)) {
|
||||
if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.isShowingValidation())) {
|
||||
element.addClass(SHOW_VALIDATION_CLASS_NAME);
|
||||
scope.showValidation = true;
|
||||
notifySubView();
|
||||
@@ -128,7 +145,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
|
||||
var unsubscribe = [];
|
||||
|
||||
//listen for the forms saving event
|
||||
unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function(ev, args) {
|
||||
unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) {
|
||||
element.addClass(SHOW_VALIDATION_CLASS_NAME);
|
||||
scope.showValidation = true;
|
||||
notifySubView();
|
||||
@@ -137,7 +154,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
|
||||
}));
|
||||
|
||||
//listen for the forms saved event
|
||||
unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function(ev, args) {
|
||||
unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) {
|
||||
//remove validation class
|
||||
element.removeClass(SHOW_VALIDATION_CLASS_NAME);
|
||||
scope.showValidation = false;
|
||||
@@ -151,7 +168,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
|
||||
|
||||
//This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but
|
||||
// the form has pending changes
|
||||
var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) {
|
||||
var locationEvent = $rootScope.$on('$locationChangeStart', function (event, nextLocation, currentLocation) {
|
||||
|
||||
var infiniteEditors = editorService.getEditors();
|
||||
|
||||
@@ -178,10 +195,10 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
|
||||
"disableEscKey": true,
|
||||
"submitButtonLabel": labels.stayButton,
|
||||
"closeButtonLabel": labels.discardChangesButton,
|
||||
submit: function() {
|
||||
submit: function () {
|
||||
overlayService.close();
|
||||
},
|
||||
close: function() {
|
||||
close: function () {
|
||||
// close all editors
|
||||
editorService.closeAll();
|
||||
// allow redirection
|
||||
@@ -215,13 +232,13 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
|
||||
unsubscribe.push(locationEvent);
|
||||
|
||||
//Ensure to remove the event handler when this instance is destroyted
|
||||
scope.$on('$destroy', function() {
|
||||
scope.$on('$destroy', function () {
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
}
|
||||
});
|
||||
|
||||
$timeout(function(){
|
||||
$timeout(function () {
|
||||
formCtrl.$setPristine();
|
||||
}, 1000);
|
||||
|
||||
|
||||
@@ -8,24 +8,26 @@
|
||||
* We will listen for server side validation changes
|
||||
* and when an error is detected for this property we'll show the error message.
|
||||
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
|
||||
* We don't set the validity of this validator to false when client side validation fails, only when server side
|
||||
* validation fails however we do respond to the client side validation changes to display error and adjust UI state.
|
||||
**/
|
||||
function valPropertyMsg(serverValidationManager, localizationService) {
|
||||
function valPropertyMsg(serverValidationManager, localizationService, angularHelper) {
|
||||
|
||||
return {
|
||||
require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'],
|
||||
require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent', '?^^valPropertyMsg'],
|
||||
replace: true,
|
||||
restrict: "E",
|
||||
template: "<div ng-show=\"errorMsg != ''\" class='alert alert-error property-error' >{{errorMsg}}</div>",
|
||||
scope: {},
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
|
||||
|
||||
var unsubscribe = [];
|
||||
var watcher = null;
|
||||
var hasError = false;
|
||||
var hasError = false; // tracks if there is a child error or an explicit error
|
||||
|
||||
//create properties on our custom scope so we can use it in our template
|
||||
scope.errorMsg = "";
|
||||
|
||||
scope.errorMsg = "";
|
||||
|
||||
//the property form controller api
|
||||
var formCtrl = ctrl[0];
|
||||
//the valFormManager controller api
|
||||
@@ -33,21 +35,19 @@ function valPropertyMsg(serverValidationManager, localizationService) {
|
||||
//the property controller api
|
||||
var umbPropCtrl = ctrl[2];
|
||||
//the variants controller api
|
||||
var umbVariantCtrl = ctrl[3];
|
||||
|
||||
var umbVariantCtrl = ctrl[3];
|
||||
|
||||
var currentProperty = umbPropCtrl.property;
|
||||
scope.currentProperty = currentProperty;
|
||||
|
||||
var currentCulture = currentProperty.culture;
|
||||
var currentSegment = currentProperty.segment;
|
||||
|
||||
var currentSegment = currentProperty.segment;
|
||||
|
||||
// validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type)
|
||||
var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined;
|
||||
|
||||
var labels = {};
|
||||
localizationService.localize("errors_propertyHasErrors").then(function (data) {
|
||||
labels.propertyHasErrors = data;
|
||||
});
|
||||
var showValidation = false;
|
||||
|
||||
if (umbVariantCtrl) {
|
||||
//if we are inside of an umbVariantContent directive
|
||||
@@ -61,17 +61,16 @@ function valPropertyMsg(serverValidationManager, localizationService) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language.
|
||||
currentCulture = currentCulture || "invariant";
|
||||
|
||||
|
||||
// Gets the error message to display
|
||||
function getErrorMsg() {
|
||||
//this can be null if no property was assigned
|
||||
if (scope.currentProperty) {
|
||||
//first try to get the error msg from the server collection
|
||||
var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, "", null);
|
||||
var err = serverValidationManager.getPropertyError(umbPropCtrl.getValidationPath(), null, "", null);
|
||||
//if there's an error message use it
|
||||
if (err && err.errorMsg) {
|
||||
return err.errorMsg;
|
||||
@@ -84,43 +83,108 @@ function valPropertyMsg(serverValidationManager, localizationService) {
|
||||
return labels.propertyHasErrors;
|
||||
}
|
||||
|
||||
// We need to subscribe to any changes to our model (based on user input)
|
||||
// This is required because when we have a server error we actually invalidate
|
||||
// the form which means it cannot be resubmitted.
|
||||
// So once a field is changed that has a server error assigned to it
|
||||
// we need to re-validate it for the server side validator so the user can resubmit
|
||||
// the form. Of course normal client-side validators will continue to execute.
|
||||
// check the current errors in the form (and recursive sub forms), if there is 1 or 2 errors
|
||||
// we can check if those are valPropertyMsg or valServer and can clear our error in those cases.
|
||||
function checkAndClearError() {
|
||||
|
||||
var errCount = angularHelper.countAllFormErrors(formCtrl);
|
||||
|
||||
if (errCount === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (errCount > 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasValServer = Utilities.isArray(formCtrl.$error.valServer);
|
||||
if (errCount === 1 && hasValServer) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var hasOwnErr = hasExplicitError();
|
||||
if ((errCount === 1 && hasOwnErr) || (errCount === 2 && hasOwnErr && hasValServer)) {
|
||||
|
||||
var propertyValidationPath = umbPropCtrl.getValidationPath();
|
||||
// check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves
|
||||
if (isLastServerError(propertyValidationPath)) {
|
||||
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if there is an explicit valPropertyMsg validation error on the form
|
||||
function hasExplicitError() {
|
||||
return Utilities.isArray(formCtrl.$error.valPropertyMsg);
|
||||
}
|
||||
|
||||
// returns true if there is only a single server validation error for this property validation key in it's validation path
|
||||
function isLastServerError(propertyValidationPath) {
|
||||
var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath(
|
||||
propertyValidationPath,
|
||||
currentCulture,
|
||||
currentSegment,
|
||||
{ matchType: "prefix" });
|
||||
if (nestedErrs.length === 0 || (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationPath)) {
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// a custom $validator function called on when each child ngModelController changes a value.
|
||||
function resetServerValidityValidator(fieldController) {
|
||||
const storedFieldController = fieldController; // pin a reference to this
|
||||
return (modelValue, viewValue) => {
|
||||
// if the ngModelController value has changed, then we can check and clear the error
|
||||
if (storedFieldController.$dirty) {
|
||||
if (checkAndClearError()) {
|
||||
resetError();
|
||||
}
|
||||
}
|
||||
return true; // this validator is always 'valid'
|
||||
};
|
||||
}
|
||||
|
||||
// collect all ng-model controllers recursively within the umbProperty form
|
||||
// until it reaches the next nested umbProperty form
|
||||
function collectAllNgModelControllersRecursively(controls, ngModels) {
|
||||
controls.forEach(ctrl => {
|
||||
if (angularHelper.isForm(ctrl)) {
|
||||
// if it's not another umbProperty form then recurse
|
||||
if (ctrl.$name !== formCtrl.$name) {
|
||||
collectAllNgModelControllersRecursively(ctrl.$getControls(), ngModels);
|
||||
}
|
||||
}
|
||||
else if (ctrl.hasOwnProperty('$modelValue') && Utilities.isObject(ctrl.$validators)) {
|
||||
ngModels.push(ctrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// We start the watch when there's server validation errors detected.
|
||||
// We watch on the current form's properties and on first watch or if they are dynamically changed
|
||||
// we find all ngModel controls recursively on this form (but stop recursing before we get to the next)
|
||||
// umbProperty form). Then for each ngModelController we assign a new $validator. This $validator
|
||||
// will execute whenever the value is changed which allows us to check and reset the server validator
|
||||
function startWatch() {
|
||||
//if there's not already a watch
|
||||
if (!watcher) {
|
||||
watcher = scope.$watch("currentProperty.value",
|
||||
function (newValue, oldValue) {
|
||||
if (angular.equals(newValue, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var errCount = 0;
|
||||
|
||||
for (var e in formCtrl.$error) {
|
||||
if (Utilities.isArray(formCtrl.$error[e])) {
|
||||
errCount++;
|
||||
watcher = scope.$watchCollection(
|
||||
() => formCtrl,
|
||||
function (updatedFormController) {
|
||||
var ngModels = [];
|
||||
collectAllNgModelControllersRecursively(updatedFormController.$getControls(), ngModels);
|
||||
ngModels.forEach(x => {
|
||||
if (!x.$validators.serverValidityResetter) {
|
||||
x.$validators.serverValidityResetter = resetServerValidityValidator(x);
|
||||
}
|
||||
}
|
||||
|
||||
//we are explicitly checking for valServer errors here, since we shouldn't auto clear
|
||||
// based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg
|
||||
// is the only one, then we'll clear.
|
||||
|
||||
if (errCount === 0
|
||||
|| (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg))
|
||||
|| (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) {
|
||||
scope.errorMsg = "";
|
||||
formCtrl.$setValidity('valPropertyMsg', true);
|
||||
} else if (showValidation && scope.errorMsg === "") {
|
||||
formCtrl.$setValidity('valPropertyMsg', false);
|
||||
scope.errorMsg = getErrorMsg();
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,22 +196,31 @@ function valPropertyMsg(serverValidationManager, localizationService) {
|
||||
}
|
||||
}
|
||||
|
||||
function resetError() {
|
||||
stopWatch();
|
||||
hasError = false;
|
||||
formCtrl.$setValidity('valPropertyMsg', true);
|
||||
scope.errorMsg = "";
|
||||
|
||||
}
|
||||
|
||||
// This deals with client side validation changes and is executed anytime validators change on the containing
|
||||
// valFormManager. This allows us to know when to display or clear the property error data for non-server side errors.
|
||||
function checkValidationStatus() {
|
||||
if (formCtrl.$invalid) {
|
||||
//first we need to check if the valPropertyMsg validity is invalid
|
||||
if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) {
|
||||
//since we already have an error we'll just return since this means we've already set the
|
||||
// hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe
|
||||
//hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe
|
||||
return;
|
||||
}
|
||||
//if there are any errors in the current property form that are not valPropertyMsg
|
||||
else if (_.without(_.keys(formCtrl.$error), "valPropertyMsg").length > 0) {
|
||||
|
||||
|
||||
// errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared
|
||||
if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) {
|
||||
hasError = false;
|
||||
showValidation = false;
|
||||
scope.errorMsg = "";
|
||||
|
||||
resetError();
|
||||
|
||||
// if there's no value, the controls can be reset, which clears the error state on formCtrl
|
||||
for (let control of formCtrl.$getControls()) {
|
||||
@@ -164,99 +237,107 @@ function valPropertyMsg(serverValidationManager, localizationService) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
hasError = false;
|
||||
scope.errorMsg = "";
|
||||
resetError();
|
||||
}
|
||||
}
|
||||
else {
|
||||
hasError = false;
|
||||
scope.errorMsg = "";
|
||||
resetError();
|
||||
}
|
||||
}
|
||||
|
||||
//if there's any remaining errors in the server validation service then we should show them.
|
||||
var showValidation = serverValidationManager.items.length > 0;
|
||||
if (!showValidation) {
|
||||
//We can either get the form submitted status by the parent directive valFormManager (if we add a property to it)
|
||||
//or we can just check upwards in the DOM for the css class (easier for now).
|
||||
//The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot
|
||||
//reset the status.
|
||||
showValidation = element.closest(".show-validation").length > 0;
|
||||
}
|
||||
function onInit() {
|
||||
|
||||
localizationService.localize("errors_propertyHasErrors").then(function (data) {
|
||||
|
||||
//listen for form validation changes.
|
||||
//The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then
|
||||
// subscribing to this single watch.
|
||||
valFormManager.onValidationStatusChanged(function (evt, args) {
|
||||
checkValidationStatus();
|
||||
});
|
||||
labels.propertyHasErrors = data;
|
||||
|
||||
//listen for the forms saving event
|
||||
unsubscribe.push(scope.$on("formSubmitting", function (ev, args) {
|
||||
showValidation = true;
|
||||
if (hasError && scope.errorMsg === "") {
|
||||
scope.errorMsg = getErrorMsg();
|
||||
startWatch();
|
||||
}
|
||||
else if (!hasError) {
|
||||
scope.errorMsg = "";
|
||||
stopWatch();
|
||||
}
|
||||
}));
|
||||
|
||||
//listen for the forms saved event
|
||||
unsubscribe.push(scope.$on("formSubmitted", function (ev, args) {
|
||||
showValidation = false;
|
||||
scope.errorMsg = "";
|
||||
formCtrl.$setValidity('valPropertyMsg', true);
|
||||
stopWatch();
|
||||
}));
|
||||
|
||||
//listen for server validation changes
|
||||
// NOTE: we pass in "" in order to listen for all validation changes to the content property, not for
|
||||
// validation changes to fields in the property this is because some server side validators may not
|
||||
// return the field name for which the error belongs too, just the property for which it belongs.
|
||||
// It's important to note that we need to subscribe to server validation changes here because we always must
|
||||
// indicate that a content property is invalid at the property level since developers may not actually implement
|
||||
// the correct field validation in their property editors.
|
||||
|
||||
if (scope.currentProperty) { //this can be null if no property was assigned
|
||||
|
||||
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
|
||||
hasError = !isValid;
|
||||
if (hasError) {
|
||||
//set the error message to the server message
|
||||
scope.errorMsg = propertyErrors[0].errorMsg;
|
||||
//flag that the current validator is invalid
|
||||
formCtrl.$setValidity('valPropertyMsg', false);
|
||||
startWatch();
|
||||
//if there's any remaining errors in the server validation service then we should show them.
|
||||
showValidation = serverValidationManager.items.length > 0;
|
||||
if (!showValidation) {
|
||||
//We can either get the form submitted status by the parent directive valFormManager (if we add a property to it)
|
||||
//or we can just check upwards in the DOM for the css class (easier for now).
|
||||
//The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot
|
||||
//reset the status.
|
||||
showValidation = element.closest(".show-validation").length > 0;
|
||||
}
|
||||
else {
|
||||
scope.errorMsg = "";
|
||||
//flag that the current validator is valid
|
||||
formCtrl.$setValidity('valPropertyMsg', true);
|
||||
stopWatch();
|
||||
|
||||
//listen for form validation changes.
|
||||
//The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then
|
||||
// subscribing to this single watch.
|
||||
valFormManager.onValidationStatusChanged(function (evt, args) {
|
||||
checkValidationStatus();
|
||||
});
|
||||
|
||||
//listen for the forms saving event
|
||||
unsubscribe.push(scope.$on("formSubmitting", function (ev, args) {
|
||||
showValidation = true;
|
||||
if (hasError && scope.errorMsg === "") {
|
||||
scope.errorMsg = getErrorMsg();
|
||||
startWatch();
|
||||
}
|
||||
else if (!hasError) {
|
||||
resetError();
|
||||
}
|
||||
}));
|
||||
|
||||
//listen for the forms saved event
|
||||
unsubscribe.push(scope.$on("formSubmitted", function (ev, args) {
|
||||
showValidation = false;
|
||||
resetError();
|
||||
}));
|
||||
|
||||
if (scope.currentProperty) { //this can be null if no property was assigned
|
||||
|
||||
// listen for server validation changes for property validation path prefix.
|
||||
// We pass in "" in order to listen for all validation changes to the content property, not for
|
||||
// validation changes to fields in the property this is because some server side validators may not
|
||||
// return the field name for which the error belongs too, just the property for which it belongs.
|
||||
// It's important to note that we need to subscribe to server validation changes here because we always must
|
||||
// indicate that a content property is invalid at the property level since developers may not actually implement
|
||||
// the correct field validation in their property editors.
|
||||
|
||||
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
|
||||
var hadError = hasError;
|
||||
hasError = !isValid;
|
||||
if (hasError) {
|
||||
//set the error message to the server message
|
||||
scope.errorMsg = propertyErrors.length > 1 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors;
|
||||
//flag that the current validator is invalid
|
||||
formCtrl.$setValidity('valPropertyMsg', false);
|
||||
startWatch();
|
||||
|
||||
|
||||
if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) {
|
||||
var propertyValidationPath = umbPropCtrl.getValidationPath();
|
||||
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment);
|
||||
resetError();
|
||||
}
|
||||
}
|
||||
else {
|
||||
resetError();
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe.push(serverValidationManager.subscribe(
|
||||
umbPropCtrl.getValidationPath(),
|
||||
currentCulture,
|
||||
"",
|
||||
serverValidationManagerCallback,
|
||||
currentSegment,
|
||||
{ matchType: "prefix" } // match property validation path prefix
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe.push(serverValidationManager.subscribe(scope.currentProperty.alias,
|
||||
currentCulture,
|
||||
"",
|
||||
serverValidationManagerCallback,
|
||||
currentSegment
|
||||
)
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
//when the scope is disposed we need to unsubscribe
|
||||
scope.$on('$destroy', function () {
|
||||
stopWatch();
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
}
|
||||
unsubscribe.forEach(u => u());
|
||||
});
|
||||
|
||||
onInit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ function valServer(serverValidationManager) {
|
||||
link: function (scope, element, attr, ctrls) {
|
||||
|
||||
var modelCtrl = ctrls[0];
|
||||
var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null;
|
||||
var umbPropCtrl = ctrls[1];
|
||||
if (!umbPropCtrl) {
|
||||
//we cannot proceed, this validator will be disabled
|
||||
return;
|
||||
}
|
||||
|
||||
// optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages.
|
||||
var umbVariantCtrl = ctrls.length > 2 ? ctrls[2] : null;
|
||||
var umbVariantCtrl = ctrls[2];
|
||||
|
||||
var currentProperty = umbPropCtrl.property;
|
||||
var currentCulture = currentProperty.culture;
|
||||
@@ -55,8 +55,11 @@ function valServer(serverValidationManager) {
|
||||
}
|
||||
}
|
||||
|
||||
//Need to watch the value model for it to change, previously we had subscribed to
|
||||
//modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that
|
||||
// Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works
|
||||
var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : currentProperty.alias;
|
||||
|
||||
// Need to watch the value model for it to change, previously we had subscribed to
|
||||
// modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that
|
||||
// doesn't specifically have a 2 way ng binding. This is required because when we
|
||||
// have a server error we actually invalidate the form which means it cannot be
|
||||
// resubmitted. So once a field is changed that has a server error assigned to it
|
||||
@@ -75,8 +78,10 @@ function valServer(serverValidationManager) {
|
||||
|
||||
if (modelCtrl.$invalid) {
|
||||
modelCtrl.$setValidity('valServer', true);
|
||||
|
||||
//clear the server validation entry
|
||||
serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName, currentSegment);
|
||||
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, fieldName, currentSegment);
|
||||
|
||||
stopWatch();
|
||||
}
|
||||
}, true);
|
||||
@@ -105,7 +110,9 @@ function valServer(serverValidationManager) {
|
||||
stopWatch();
|
||||
}
|
||||
}
|
||||
unsubscribe.push(serverValidationManager.subscribe(currentProperty.alias,
|
||||
|
||||
unsubscribe.push(serverValidationManager.subscribe(
|
||||
propertyValidationPath,
|
||||
currentCulture,
|
||||
fieldName,
|
||||
serverValidationManagerCallback,
|
||||
@@ -114,9 +121,7 @@ function valServer(serverValidationManager) {
|
||||
|
||||
scope.$on('$destroy', function () {
|
||||
stopWatch();
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
}
|
||||
unsubscribe.forEach(u => u());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name umbraco.directives.directive:valServerMatch
|
||||
* @restrict A
|
||||
* @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data
|
||||
* contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against
|
||||
* a property validation key. The attribute value can be in multiple value types:
|
||||
* - STRING = The property validation key to have an exact match on. If matched, then the form will have a valServerMatch validator applied.
|
||||
* - OBJECT = A dictionary where the key is the match type: "contains", "prefix", "suffix" and the value is either:
|
||||
* - ARRAY = A list of property validation keys to match on. If any are matched then the form will have a valServerMatch validator applied.
|
||||
* - OBJECT = A dictionary where the key is the validator error name applied to the form and the value is the STRING of the property validation key to match on
|
||||
**/
|
||||
function valServerMatch(serverValidationManager) {
|
||||
|
||||
return {
|
||||
require: ['form', '^^umbProperty', '?^^umbVariantContent'],
|
||||
restrict: "A",
|
||||
scope: {
|
||||
valServerMatch: "="
|
||||
},
|
||||
link: function (scope, element, attr, ctrls) {
|
||||
|
||||
var formCtrl = ctrls[0];
|
||||
var umbPropCtrl = ctrls[1];
|
||||
if (!umbPropCtrl) {
|
||||
//we cannot proceed, this validator will be disabled
|
||||
return;
|
||||
}
|
||||
|
||||
// optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages.
|
||||
var umbVariantCtrl = ctrls[2];
|
||||
|
||||
var currentProperty = umbPropCtrl.property;
|
||||
var currentCulture = currentProperty.culture;
|
||||
var currentSegment = currentProperty.segment;
|
||||
|
||||
if (umbVariantCtrl) {
|
||||
//if we are inside of an umbVariantContent directive
|
||||
|
||||
var currentVariant = umbVariantCtrl.editor.content;
|
||||
|
||||
// Lets check if we have variants and we are on the default language then ...
|
||||
if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) {
|
||||
//This property is locked cause its a invariant property shown on a non-default language.
|
||||
//Therefor do not validate this field.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language.
|
||||
currentCulture = currentCulture || "invariant";
|
||||
|
||||
var unsubscribe = [];
|
||||
|
||||
function bindCallback(validationKey, matchVal, matchType) {
|
||||
|
||||
if (Utilities.isString(matchVal)) {
|
||||
matchVal = [matchVal]; // normalize to an array since the value can also natively be an array
|
||||
}
|
||||
|
||||
// match for each string in the array
|
||||
matchVal.forEach(m => {
|
||||
unsubscribe.push(serverValidationManager.subscribe(
|
||||
m,
|
||||
currentCulture,
|
||||
"",
|
||||
// the callback
|
||||
function (isValid, propertyErrors, allErrors) {
|
||||
if (!isValid) {
|
||||
formCtrl.$setValidity(validationKey, false);
|
||||
}
|
||||
else {
|
||||
formCtrl.$setValidity(validationKey, true);
|
||||
}
|
||||
},
|
||||
currentSegment,
|
||||
matchType ? { matchType: matchType } : null // specify the match type
|
||||
));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (Utilities.isObject(scope.valServerMatch)) {
|
||||
var allowedKeys = ["contains", "prefix", "suffix"];
|
||||
Object.keys(scope.valServerMatch).forEach(matchType => {
|
||||
if (allowedKeys.indexOf(matchType) === -1) {
|
||||
throw "valServerMatch dictionary keys must be one of " + allowedKeys.join();
|
||||
}
|
||||
|
||||
var matchVal = scope.valServerMatch[matchType];
|
||||
|
||||
if (Utilities.isObject(matchVal)) {
|
||||
|
||||
// as an object, the key will be the validation error instead of the default "valServerMatch"
|
||||
Object.keys(matchVal).forEach(valKey => {
|
||||
|
||||
// matchVal[valKey] can be an ARRAY or a STRING
|
||||
bindCallback(valKey, matchVal[valKey], matchType);
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
||||
// matchVal can be an ARRAY or a STRING
|
||||
bindCallback("valServerMatch", matchVal, matchType);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (Utilities.isString(scope.valServerMatch)) {
|
||||
|
||||
// a STRING match which will be an exact match on the string supplied as the property validation key
|
||||
bindCallback("valServerMatch", scope.valServerMatch, null);
|
||||
}
|
||||
else {
|
||||
throw "valServerMatch value must be a string or a dictionary";
|
||||
}
|
||||
|
||||
scope.$on('$destroy', function () {
|
||||
unsubscribe.forEach(u => u());
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
angular.module('umbraco.directives.validation').directive("valServerMatch", valServerMatch);
|
||||
@@ -1,36 +1,49 @@
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name umbraco.directives.directive:valSubView
|
||||
* @restrict A
|
||||
* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data.
|
||||
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
|
||||
* @ngdoc directive
|
||||
* @name umbraco.directives.directive:valSubView
|
||||
* @restrict A
|
||||
* @description Used to show validation warnings for a editor sub view (used in conjunction with:
|
||||
* umb-editor-sub-view or umb-editor-sub-views) to indicate that the section content has validation errors in its data.
|
||||
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
|
||||
* When applied to
|
||||
**/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Since this is a directive applied as an attribute, the value of that attribtue is the 'model' object property
|
||||
// of the current inherited scope that the hasError/errorClass properties will apply to.
|
||||
// This directive cannot have it's own scope because it's an attribute applied to another scoped directive.
|
||||
// Due to backwards compatibility we can't really change this, ideally this would have it's own scope/properties.
|
||||
|
||||
function valSubViewDirective() {
|
||||
|
||||
function controller($scope, $element) {
|
||||
function controller($scope, $element, $attrs) {
|
||||
|
||||
var model = $scope.model; // this is the default and required for backwards compat
|
||||
if ($attrs && $attrs.valSubView) {
|
||||
// get the property to use
|
||||
model = $scope[$attrs.valSubView];
|
||||
}
|
||||
|
||||
//expose api
|
||||
return {
|
||||
valStatusChanged: function (args) {
|
||||
|
||||
// TODO: Verify this is correct, does $scope.model ever exist?
|
||||
if ($scope.model) {
|
||||
if (model) {
|
||||
if (!args.form.$valid) {
|
||||
var subViewContent = $element.find(".ng-invalid");
|
||||
|
||||
if (subViewContent.length > 0) {
|
||||
$scope.model.hasError = true;
|
||||
$scope.model.errorClass = args.showValidation ? 'show-validation' : null;
|
||||
model.hasError = true;
|
||||
model.errorClass = args.showValidation ? 'show-validation' : null;
|
||||
} else {
|
||||
$scope.model.hasError = false;
|
||||
$scope.model.errorClass = null;
|
||||
model.hasError = false;
|
||||
model.errorClass = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$scope.model.hasError = false;
|
||||
$scope.model.errorClass = null;
|
||||
model.hasError = false;
|
||||
model.errorClass = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,12 +53,18 @@
|
||||
function link(scope, el, attr, ctrl) {
|
||||
|
||||
//if there are no containing form or valFormManager controllers, then we do nothing
|
||||
if (!ctrl || !Utilities.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) {
|
||||
if (!ctrl[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var model = scope.model; // this is the default and required for backwards compat
|
||||
if (attr && attr.valSubView) {
|
||||
// get the property to use
|
||||
model = scope[attr.valSubView];
|
||||
}
|
||||
|
||||
var valFormManager = ctrl[1];
|
||||
scope.model.hasError = false;
|
||||
model.hasError = false;
|
||||
|
||||
//listen for form validation changes
|
||||
valFormManager.onValidationStatusChanged(function (evt, args) {
|
||||
@@ -54,14 +73,14 @@
|
||||
var subViewContent = el.find(".ng-invalid");
|
||||
|
||||
if (subViewContent.length > 0) {
|
||||
scope.model.hasError = true;
|
||||
model.hasError = true;
|
||||
} else {
|
||||
scope.model.hasError = false;
|
||||
model.hasError = false;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
scope.model.hasError = false;
|
||||
model.hasError = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('umbraco.mocks').
|
||||
factory('mocksUtils', ['$cookies', function ($cookies) {
|
||||
factory('mocksUtils', ['$cookies', 'udiService', function ($cookies, udiService) {
|
||||
'use strict';
|
||||
|
||||
//by default we will perform authorization
|
||||
@@ -40,13 +40,17 @@ angular.module('umbraco.mocks').
|
||||
},
|
||||
|
||||
/** Creats a mock content object */
|
||||
getMockContent: function(id) {
|
||||
getMockContent: function (id, key, udi) {
|
||||
key = key || String.CreateGuid();
|
||||
var udi = udi || udiService.build("content", key);
|
||||
var node = {
|
||||
name: "My content with id: " + id,
|
||||
updateDate: new Date().toIsoDateTimeString(),
|
||||
publishDate: new Date().toIsoDateTimeString(),
|
||||
createDate: new Date().toIsoDateTimeString(),
|
||||
id: id,
|
||||
key: key,
|
||||
udi: udi,
|
||||
parentId: 1234,
|
||||
icon: "icon-umb-content",
|
||||
owner: { name: "Administrator", id: 0 },
|
||||
@@ -280,6 +284,182 @@ angular.module('umbraco.mocks').
|
||||
return node;
|
||||
},
|
||||
|
||||
|
||||
/** Creats a mock variant content object */
|
||||
getMockVariantContent: function(id, key, udi) {
|
||||
key = key || String.CreateGuid();
|
||||
var udi = udi || udiService.build("content", key);
|
||||
var node = {
|
||||
name: "My content with id: " + id,
|
||||
updateDate: new Date().toIsoDateTimeString(),
|
||||
publishDate: new Date().toIsoDateTimeString(),
|
||||
createDate: new Date().toIsoDateTimeString(),
|
||||
id: id,
|
||||
key: key,
|
||||
udi: udi,
|
||||
parentId: 1234,
|
||||
icon: "icon-umb-content",
|
||||
owner: { name: "Administrator", id: 0 },
|
||||
updater: { name: "Per Ploug Krogslund", id: 1 },
|
||||
path: "-1,1234,2455",
|
||||
allowedActions: ["U", "H", "A"],
|
||||
contentTypeAlias: "testAlias",
|
||||
contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB",
|
||||
apps: [],
|
||||
variants: [
|
||||
{
|
||||
name: "",
|
||||
language: null,
|
||||
segment: null,
|
||||
state: "NotCreated",
|
||||
updateDate: "0001-01-01 00:00:00",
|
||||
createDate: "0001-01-01 00:00:00",
|
||||
publishDate: null,
|
||||
releaseDate: null,
|
||||
expireDate: null,
|
||||
notifications: [],
|
||||
tabs: [
|
||||
{
|
||||
label: "Content",
|
||||
id: 2,
|
||||
properties: [
|
||||
{ alias: "testproperty", label: "Test property", view: "textbox", value: "asdfghjk" },
|
||||
{ alias: "valTest", label: "Validation test", view: "validationtest", value: "asdfasdf" },
|
||||
{ alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>", config: {} },
|
||||
{ alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
|
||||
{ alias: "media", label: "Media picker", view: "mediapicker", value: "1234,23242,23232,23231", config: {multiPicker: 1} }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Sample Editor",
|
||||
id: 3,
|
||||
properties: [
|
||||
{ alias: "datepicker", label: "Datepicker", view: "datepicker", config: { pickTime: false, format: "yyyy-MM-dd" } },
|
||||
{ alias: "tags", label: "Tags", view: "tags", value: "" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "This",
|
||||
id: 4,
|
||||
properties: [
|
||||
{ alias: "valTest4", label: "Validation test", view: "validationtest", value: "asdfasdf" },
|
||||
{ alias: "bodyText4", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>", config: {} },
|
||||
{ alias: "textarea4", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
|
||||
{ alias: "content4", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Is",
|
||||
id: 5,
|
||||
properties: [
|
||||
{ alias: "valTest5", label: "Validation test", view: "validationtest", value: "asdfasdf" },
|
||||
{ alias: "bodyText5", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>", config: {} },
|
||||
{ alias: "textarea5", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
|
||||
{ alias: "content5", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Overflown",
|
||||
id: 6,
|
||||
properties: [
|
||||
{ alias: "valTest6", label: "Validation test", view: "validationtest", value: "asdfasdf" },
|
||||
{ alias: "bodyText6", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>", config: {} },
|
||||
{ alias: "textarea6", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
|
||||
{ alias: "content6", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Generic Properties",
|
||||
id: 0,
|
||||
properties: [
|
||||
{
|
||||
label: 'Id',
|
||||
value: 1234,
|
||||
view: "readonlyvalue",
|
||||
alias: "_umb_id"
|
||||
},
|
||||
{
|
||||
label: 'Created by',
|
||||
description: 'Original author',
|
||||
value: "Administrator",
|
||||
view: "readonlyvalue",
|
||||
alias: "_umb_createdby"
|
||||
},
|
||||
{
|
||||
label: 'Created',
|
||||
description: 'Date/time this document was created',
|
||||
value: new Date().toIsoDateTimeString(),
|
||||
view: "readonlyvalue",
|
||||
alias: "_umb_createdate"
|
||||
},
|
||||
{
|
||||
label: 'Updated',
|
||||
description: 'Date/time this document was created',
|
||||
value: new Date().toIsoDateTimeString(),
|
||||
view: "readonlyvalue",
|
||||
alias: "_umb_updatedate"
|
||||
},
|
||||
{
|
||||
label: 'Document Type',
|
||||
value: "Home page",
|
||||
view: "readonlyvalue",
|
||||
alias: "_umb_doctype"
|
||||
},
|
||||
{
|
||||
label: 'Publish at',
|
||||
description: 'Date/time to publish this document',
|
||||
value: new Date().toIsoDateTimeString(),
|
||||
view: "datepicker",
|
||||
alias: "_umb_releasedate"
|
||||
},
|
||||
{
|
||||
label: 'Unpublish at',
|
||||
description: 'Date/time to un-publish this document',
|
||||
value: new Date().toIsoDateTimeString(),
|
||||
view: "datepicker",
|
||||
alias: "_umb_expiredate"
|
||||
},
|
||||
{
|
||||
label: 'Template',
|
||||
value: "myTemplate",
|
||||
view: "dropdown",
|
||||
alias: "_umb_template",
|
||||
config: {
|
||||
items: {
|
||||
"" : "-- Choose template --",
|
||||
"myTemplate" : "My Templates",
|
||||
"home" : "Home Page",
|
||||
"news" : "News Page"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Link to document',
|
||||
value: ["/testing" + id, "http://localhost/testing" + id, "http://mydomain.com/testing" + id].join(),
|
||||
view: "urllist",
|
||||
alias: "_umb_urllist"
|
||||
},
|
||||
{
|
||||
alias: "test", label: "Stuff", view: "test", value: "",
|
||||
config: {
|
||||
fields: [
|
||||
{ alias: "embedded", label: "Embbeded", view: "textstring", value: "" },
|
||||
{ alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" },
|
||||
{ alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" },
|
||||
{ alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
getMockEntity : function(id){
|
||||
return {name: "hello", id: id, icon: "icon-file"};
|
||||
},
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
angular.module('umbraco.mocks').
|
||||
factory('variantContentMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) {
|
||||
'use strict';
|
||||
|
||||
function returnEmptyVariantNode(status, data, headers) {
|
||||
|
||||
if (!mocksUtils.checkAuth()) {
|
||||
return [401, null, null];
|
||||
}
|
||||
|
||||
var response = returnVariantNodebyId(200, "", null);
|
||||
var node = response[1];
|
||||
var parentId = mocksUtils.getParameterByName(data, "parentId") || 1234;
|
||||
|
||||
node.name = "";
|
||||
node.id = 0;
|
||||
node.parentId = parentId;
|
||||
|
||||
node.tabs.forEach(function(tab) {
|
||||
tab.properties.forEach(function(property) {
|
||||
property.value = "";
|
||||
});
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function returnVariantNodebyId(status, data, headers) {
|
||||
|
||||
if (!mocksUtils.checkAuth()) {
|
||||
return [401, null, null];
|
||||
}
|
||||
|
||||
var id = mocksUtils.getParameterByName(data, "id") || "1234";
|
||||
id = parseInt(id, 10);
|
||||
|
||||
var node = mocksUtils.getMockVariantContent(id);
|
||||
|
||||
return [200, node, null];
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
register: function () {
|
||||
|
||||
$httpBackend
|
||||
.whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Content/GetById?'))
|
||||
.respond(returnVariantNodebyId);
|
||||
|
||||
$httpBackend
|
||||
.whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Content/GetEmpty'))
|
||||
.respond(returnEmptyVariantNode);
|
||||
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -388,7 +388,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
|
||||
umbRequestHelper.getApiUrl(
|
||||
"contentApiBaseUrl",
|
||||
"GetBlueprintById",
|
||||
[{ id: id }])),
|
||||
{ id: id })),
|
||||
'Failed to retrieve data for content id ' + id)
|
||||
.then(function (result) {
|
||||
return $q.when(umbDataFormatter.formatContentGetData(result));
|
||||
@@ -401,7 +401,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
|
||||
umbRequestHelper.getApiUrl(
|
||||
"contentApiBaseUrl",
|
||||
"GetNotificationOptions",
|
||||
[{ contentId: id }])),
|
||||
{ contentId: id })),
|
||||
'Failed to retrieve data for content id ' + id);
|
||||
},
|
||||
|
||||
@@ -498,12 +498,57 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
|
||||
umbRequestHelper.getApiUrl(
|
||||
"contentApiBaseUrl",
|
||||
"GetEmpty",
|
||||
[{ contentTypeAlias: alias }, { parentId: parentId }])),
|
||||
{ contentTypeAlias: alias, parentId: parentId })),
|
||||
'Failed to retrieve data for empty content item type ' + alias)
|
||||
.then(function (result) {
|
||||
return $q.when(umbDataFormatter.formatContentGetData(result));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name umbraco.resources.contentResource#getScaffoldByKey
|
||||
* @methodOf umbraco.resources.contentResource
|
||||
*
|
||||
* @description
|
||||
* Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias.
|
||||
*
|
||||
* - Parent Id must be provided so umbraco knows where to store the content
|
||||
* - Content Type Id must be provided so umbraco knows which properties to put on the content scaffold
|
||||
*
|
||||
* The scaffold is used to build editors for content that has not yet been populated with data.
|
||||
*
|
||||
* ##usage
|
||||
* <pre>
|
||||
* contentResource.getScaffoldByKey(1234, '...')
|
||||
* .then(function(scaffold) {
|
||||
* var myDoc = scaffold;
|
||||
* myDoc.name = "My new document";
|
||||
*
|
||||
* contentResource.publish(myDoc, true)
|
||||
* .then(function(content){
|
||||
* alert("Retrieved, updated and published again");
|
||||
* });
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @param {Int} parentId id of content item to return
|
||||
* @param {String} contentTypeGuid contenttype guid to base the scaffold on
|
||||
* @returns {Promise} resourcePromise object containing the content scaffold.
|
||||
*
|
||||
*/
|
||||
getScaffoldByKey: function (parentId, contentTypeKey) {
|
||||
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
"contentApiBaseUrl",
|
||||
"GetEmptyByKey",
|
||||
{ contentTypeKey: contentTypeKey, parentId: parentId })),
|
||||
'Failed to retrieve data for empty content item id ' + contentTypeKey)
|
||||
.then(function (result) {
|
||||
return $q.when(umbDataFormatter.formatContentGetData(result));
|
||||
});
|
||||
},
|
||||
|
||||
getBlueprintScaffold: function (parentId, blueprintId) {
|
||||
|
||||
@@ -512,7 +557,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
|
||||
umbRequestHelper.getApiUrl(
|
||||
"contentApiBaseUrl",
|
||||
"GetEmpty",
|
||||
[{ blueprintId: blueprintId }, { parentId: parentId }])),
|
||||
{ blueprintId: blueprintId, parentId: parentId })),
|
||||
'Failed to retrieve blueprint for id ' + blueprintId)
|
||||
.then(function (result) {
|
||||
return $q.when(umbDataFormatter.formatContentGetData(result));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user