diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs
index 1fe1e92d9b..5969e0a788 100644
--- a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs
@@ -113,7 +113,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
if (parts.Length != 2)
{
// invalid filter
- _logger.LogInformation($"The \"{nameof(filters)}\" query option \"{filter}\" is not valid");
+ _logger.LogInformation("An invalid filter option was encountered. Please ensure that supplied filter options are two-part, separated by ':'.");
return null;
}
@@ -127,7 +127,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
break;
default:
// unknown filter
- _logger.LogInformation($"The \"{nameof(filters)}\" query option \"{filter}\" is not supported");
+ _logger.LogInformation("An unsupported filter option was supplied for the query. Please use only valid filter options. See the documentation for details.");
return null;
}
}
@@ -143,7 +143,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
if (parts.Length != 2)
{
// invalid sort
- _logger.LogInformation($"The \"{nameof(sorts)}\" query option \"{sort}\" is not valid");
+ _logger.LogInformation("An invalid sort option was encountered. Please ensure that the supplied sort options are two-part, separated by ':'.");
return null;
}
@@ -164,7 +164,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
break;
default:
// unknown sort
- _logger.LogInformation($"The \"{nameof(sorts)}\" query option \"{sort}\" is not supported");
+ _logger.LogInformation("An unsupported sort option was supplied for the query. Please use only valid sort options. See the documentation for details.");
return null;
}
diff --git a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs
index 722a6efca0..e92790ab4e 100644
--- a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs
+++ b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs
@@ -3,7 +3,7 @@ using System.Collections;
namespace Umbraco.Cms.Core.Composing;
///
-/// Provides a base class for builder collections.
+/// Provides a base class for builder collections.
///
/// The type of the items.
public abstract class BuilderCollectionBase : IBuilderCollection
@@ -11,21 +11,18 @@ public abstract class BuilderCollectionBase : IBuilderCollection
private readonly LazyReadOnlyCollection _items;
///
- /// Initializes a new instance of the with items.
+ /// Initializes a new instance of the with items.
///
/// The items.
- public BuilderCollectionBase(Func> items) => _items = new LazyReadOnlyCollection(items);
+ public BuilderCollectionBase(Func> items)
+ => _items = new LazyReadOnlyCollection(items);
///
public int Count => _items.Count;
- ///
- /// Gets an enumerator.
- ///
+ ///
public IEnumerator GetEnumerator() => _items.GetEnumerator();
- ///
- /// Gets an enumerator.
- ///
+ ///
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
diff --git a/src/Umbraco.Core/Composing/IBuilderCollection.cs b/src/Umbraco.Core/Composing/IBuilderCollection.cs
index 56036997bc..c8920149c5 100644
--- a/src/Umbraco.Core/Composing/IBuilderCollection.cs
+++ b/src/Umbraco.Core/Composing/IBuilderCollection.cs
@@ -1,13 +1,16 @@
namespace Umbraco.Cms.Core.Composing;
///
-/// Represents a builder collection, ie an immutable enumeration of items.
+/// Represents a builder collection, ie an immutable enumeration of items.
///
/// The type of the items.
public interface IBuilderCollection : IEnumerable
{
///
- /// Gets the number of items in the collection.
+ /// Gets the number of items in the collection.
///
+ ///
+ /// The count.
+ ///
int Count { get; }
}
diff --git a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs
index c093962408..3f02be10b1 100644
--- a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs
+++ b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs
@@ -1,49 +1,88 @@
namespace Umbraco.Cms.Core.Models.Editors;
///
-/// Used to track reference to other entities in a property value
+/// Used to track a reference to another entity in a property value.
///
public struct UmbracoEntityReference : IEquatable
{
private static readonly UmbracoEntityReference _empty = new(UnknownTypeUdi.Instance, string.Empty);
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The UDI.
+ /// The relation type alias.
public UmbracoEntityReference(Udi udi, string relationTypeAlias)
{
Udi = udi ?? throw new ArgumentNullException(nameof(udi));
RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias));
}
+ ///
+ /// Initializes a new instance of the struct for a document or media item.
+ ///
+ /// The UDI.
public UmbracoEntityReference(Udi udi)
{
Udi = udi ?? throw new ArgumentNullException(nameof(udi));
switch (udi.EntityType)
{
+ case Constants.UdiEntityType.Document:
+ RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias;
+ break;
case Constants.UdiEntityType.Media:
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias;
break;
default:
- RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias;
+ // No relation type alias convention for this entity type, so leave it empty
+ RelationTypeAlias = string.Empty;
break;
}
}
+ ///
+ /// Gets the UDI.
+ ///
+ ///
+ /// The UDI.
+ ///
public Udi Udi { get; }
- public static UmbracoEntityReference Empty() => _empty;
-
- public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
-
+ ///
+ /// Gets the relation type alias.
+ ///
+ ///
+ /// The relation type alias.
+ ///
public string RelationTypeAlias { get; }
- public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right);
+ ///
+ /// Gets an empty reference.
+ ///
+ ///
+ /// An empty reference.
+ ///
+ public static UmbracoEntityReference Empty() => _empty;
+ ///
+ /// Determines whether the specified reference is empty.
+ ///
+ /// The reference.
+ ///
+ /// true if the specified reference is empty; otherwise, false .
+ ///
+ public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
+
+ ///
public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference);
+ ///
public bool Equals(UmbracoEntityReference other) =>
EqualityComparer.Default.Equals(Udi, other.Udi) &&
RelationTypeAlias == other.RelationTypeAlias;
+ ///
public override int GetHashCode()
{
var hashCode = -487348478;
@@ -52,5 +91,9 @@ public struct UmbracoEntityReference : IEquatable
return hashCode;
}
+ ///
+ public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right);
+
+ ///
public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) => !(left == right);
}
diff --git a/src/Umbraco.Core/Notifications/ContentScaffoldedNotification.cs b/src/Umbraco.Core/Notifications/ContentScaffoldedNotification.cs
new file mode 100644
index 0000000000..47eda5468d
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/ContentScaffoldedNotification.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+///
+/// Notification that is send out when a Content item has been scaffolded from an original item and basic cleaning has been performed
+///
+public sealed class ContentScaffoldedNotification : ScaffoldedNotification
+{
+ public ContentScaffoldedNotification(IContent original, IContent scaffold, int parentId, EventMessages messages)
+ : base(original, scaffold, parentId, messages)
+ {
+ }
+}
diff --git a/src/Umbraco.Core/Notifications/ScaffoldedNotification.cs b/src/Umbraco.Core/Notifications/ScaffoldedNotification.cs
new file mode 100644
index 0000000000..f64bfd3933
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/ScaffoldedNotification.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.Events;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+public abstract class ScaffoldedNotification : CancelableObjectNotification
+ where T : class
+{
+ protected ScaffoldedNotification(T original, T scaffold, int parentId, EventMessages messages)
+ : base(original, messages)
+ {
+ Scaffold = scaffold;
+ ParentId = parentId;
+ }
+
+ public T Original => Target;
+
+ public T Scaffold { get; }
+
+ public int ParentId { get; }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs
index c605d45a9a..86c9c48fc0 100644
--- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs
+++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs
@@ -4,56 +4,109 @@ using Umbraco.Cms.Core.Models.Editors;
namespace Umbraco.Cms.Core.PropertyEditors;
+///
+/// Provides a builder collection for items.
+///
public class DataValueReferenceFactoryCollection : BuilderCollectionBase
{
+ // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented
+ // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The items.
public DataValueReferenceFactoryCollection(Func> items)
: base(items)
{ }
- // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented
- // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection
+ ///
+ /// Gets all unique references from the specified properties.
+ ///
+ /// The properties.
+ /// The property editors.
+ ///
+ /// The unique references from the specified properties.
+ ///
public ISet GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors)
{
var references = new HashSet();
- foreach (IProperty property in properties)
+ // Group by property editor alias to avoid duplicate lookups and optimize value parsing
+ foreach (var propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values))
{
- if (!propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor))
+ if (!propertyEditors.TryGet(propertyValuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
continue;
}
- // TODO: We will need to change this once we support tracking via variants/segments
- // for now, we are tracking values from ALL variants
- foreach (IPropertyValue propertyValue in property.Values)
+ // Use distinct values to avoid duplicate parsing of the same value
+ var values = new HashSet(properties.Count);
+ foreach (IPropertyValue propertyValue in propertyValuesByPropertyEditorAlias.SelectMany(x => x))
{
- object? value = propertyValue.EditedValue;
-
- if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
- {
- references.UnionWith(dataValueReference.GetReferences(value));
- }
-
- // Loop over collection that may be add to existing property editors
- // implementation of GetReferences in IDataValueReference.
- // Allows developers to add support for references by a
- // package /property editor that did not implement IDataValueReference themselves
- foreach (IDataValueReferenceFactory dataValueReferenceFactory in this)
- {
- // Check if this value reference is for this datatype/editor
- // Then call it's GetReferences method - to see if the value stored
- // in the dataeditor/property has references to media/content items
- if (dataValueReferenceFactory.IsForEditor(dataEditor))
- {
- references.UnionWith(dataValueReferenceFactory.GetDataValueReference().GetReferences(value));
- }
- }
+ values.Add(propertyValue.EditedValue);
+ values.Add(propertyValue.PublishedValue);
}
+
+ references.UnionWith(GetReferences(dataEditor, values));
}
return references;
}
+ ///
+ /// Gets the references.
+ ///
+ /// The data editor.
+ /// The values.
+ ///
+ /// The references.
+ ///
+ public IEnumerable GetReferences(IDataEditor dataEditor, params object?[] values)
+ => GetReferences(dataEditor, (IEnumerable)values);
+
+ ///
+ /// Gets the references.
+ ///
+ /// The data editor.
+ /// The values.
+ ///
+ /// The references.
+ ///
+ public ISet GetReferences(IDataEditor dataEditor, IEnumerable values) =>
+ GetReferencesEnumerable(dataEditor, values).ToHashSet();
+ private IEnumerable GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable values)
+ {
+ // TODO: We will need to change this once we support tracking via variants/segments
+ // for now, we are tracking values from ALL variants
+ if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
+ {
+ foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences))
+ {
+ yield return reference;
+ }
+ }
+
+ // Loop over collection that may be add to existing property editors
+ // implementation of GetReferences in IDataValueReference.
+ // Allows developers to add support for references by a
+ // package /property editor that did not implement IDataValueReference themselves
+ foreach (IDataValueReferenceFactory dataValueReferenceFactory in this)
+ {
+ // Check if this value reference is for this datatype/editor
+ // Then call it's GetReferences method - to see if the value stored
+ // in the dataeditor/property has references to media/content items
+ if (dataValueReferenceFactory.IsForEditor(dataEditor))
+ {
+ IDataValueReference factoryDataValueReference = dataValueReferenceFactory.GetDataValueReference();
+ foreach (UmbracoEntityReference reference in values.SelectMany(factoryDataValueReference.GetReferences))
+ {
+ yield return reference;
+ }
+ }
+ }
+ }
+
///
/// Gets all relation type aliases that are automatically tracked.
///
@@ -61,7 +114,10 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase
/// All relation type aliases that are automatically tracked.
///
- public ISet GetAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors)
+ [Obsolete("Use GetAllAutomaticRelationTypesAliases. This will be removed in Umbraco 15.")]
+ public ISet GetAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) =>
+ GetAllAutomaticRelationTypesAliases(propertyEditors);
+ public ISet GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors)
{
// Always add default automatic relation types
var automaticRelationTypeAliases = new HashSet(Constants.Conventions.RelationTypes.AutomaticRelationTypes);
@@ -74,15 +130,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase
- /// Gets the relation type aliases that are automatically tracked for all properties.
- ///
- /// The properties.
- /// The property editors.
- ///
- /// The relation type aliases that are automatically tracked for all properties.
- ///
+ [Obsolete("Use non-obsolete GetAutomaticRelationTypesAliases. This will be removed in Umbraco 15.")]
public ISet GetAutomaticRelationTypesAliases(IPropertyCollection properties, PropertyEditorCollection propertyEditors)
{
// Always add default automatic relation types
@@ -93,14 +141,23 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase GetAutomaticRelationTypesAliases(IDataEditor dataEditor)
+ ///
+ /// Gets the automatic relation types aliases.
+ ///
+ /// The data editor.
+ ///
+ /// The automatic relation types aliases.
+ ///
+ public ISet GetAutomaticRelationTypesAliases(IDataEditor dataEditor) =>
+ GetAutomaticRelationTypesAliasesEnumerable(dataEditor).ToHashSet();
+ private IEnumerable GetAutomaticRelationTypesAliasesEnumerable(IDataEditor dataEditor)
{
if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
{
diff --git a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs
index c20ebf9284..2d4345c053 100644
--- a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs
+++ b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs
@@ -1,6 +1,8 @@
using System.Xml.XPath;
+using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Xml;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PublishedCache;
@@ -9,10 +11,24 @@ public abstract class PublishedCacheBase : IPublishedCache
{
private readonly IVariationContextAccessor? _variationContextAccessor;
- public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) => _variationContextAccessor =
- variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor));
- protected PublishedCacheBase(bool previewDefault) => PreviewDefault = previewDefault;
+ [Obsolete("Use ctor with all parameters. This will be removed in V15")]
+ public PublishedCacheBase(IVariationContextAccessor variationContextAccessor)
+ : this(variationContextAccessor, false)
+ {
+ }
+
+ [Obsolete("Use ctor with all parameters. This will be removed in V15")]
+ protected PublishedCacheBase(bool previewDefault)
+ : this(StaticServiceProvider.Instance.GetRequiredService(), previewDefault)
+ {
+ }
+
+ public PublishedCacheBase(IVariationContextAccessor variationContextAccessor, bool previewDefault)
+ {
+ _variationContextAccessor = variationContextAccessor;
+ PreviewDefault = previewDefault;
+ }
public bool PreviewDefault { get; }
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
index a60cef7e7f..345b80a846 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
@@ -367,10 +367,13 @@ public static partial class UmbracoBuilderExtensions
builder
.AddNotificationHandler()
.AddNotificationHandler()
+ .AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
+ .AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
+ .AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index 3ae4248119..ce30372d35 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -1082,20 +1082,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
protected void PersistRelations(TEntity entity)
{
- // Get all references from our core built in DataEditors/Property Editors
- // Along with seeing if developers want to collect additional references from the DataValueReferenceFactories collection
+ // Get all references and automatic relation type aliases
ISet references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors);
-
- // First delete all auto-relations for this entity
- ISet automaticRelationTypeAliases = _dataValueReferenceFactories.GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors);
- RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
+ ISet automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(PropertyEditors);
if (references.Count == 0)
{
+ // Delete all relations using the automatic relation type aliases
+ RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
+
// No need to add new references/relations
return;
}
+ // Lookup all relation type IDs
+ var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty())
+ .Where(x => automaticRelationTypeAliases.Contains(x.Alias))
+ .ToDictionary(x => x.Alias, x => x.Id);
+
// Lookup node IDs for all GUID based UDIs
IEnumerable keys = references.Select(x => x.Udi).OfType().Select(x => x.Guid);
var keysLookup = Database.FetchByGroups(keys, Constants.Sql.MaxParameterCount, guids =>
@@ -1106,15 +1110,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
.WhereIn(x => x.UniqueId, guids);
}).ToDictionary(x => x.UniqueId, x => x.NodeId);
- // Lookup all relation type IDs
- var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty()).ToDictionary(x => x.Alias, x => x.Id);
-
// Get all valid relations
- var relations = new List(references.Count);
+ var relations = new List<(int ChildId, int RelationTypeId)>(references.Count);
foreach (UmbracoEntityReference reference in references)
{
- if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias))
+ if (string.IsNullOrEmpty(reference.RelationTypeAlias))
{
+ // Reference does not specify a relation type alias, so skip adding a relation
+ Logger.LogDebug("The reference to {Udi} does not specify a relation type alias, so it will not be saved as relation.", reference.Udi);
+ }
+ else if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias))
+ {
// Returning a reference that doesn't use an automatic relation type is an issue that should be fixed in code
Logger.LogError("The reference to {Udi} uses a relation type {RelationTypeAlias} that is not an automatic relation type.", reference.Udi, reference.RelationTypeAlias);
}
@@ -1130,12 +1136,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
else
{
- relations.Add(new ReadOnlyRelation(entity.Id, id, relationTypeId));
+ relations.Add((id, relationTypeId));
}
}
- // Save bulk relations
- RelationRepository.SaveBulk(relations);
+ // Get all existing relations (optimize for adding new and keeping existing relations)
+ var query = Query().Where(x => x.ParentId == entity.Id).WhereIn(x => x.RelationTypeId, relationTypeLookup.Values);
+ var existingRelations = RelationRepository.GetPagedRelationsByQuery(query, 0, int.MaxValue, out _, null)
+ .ToDictionary(x => (x.ChildId, x.RelationTypeId)); // Relations are unique by parent ID, child ID and relation type ID
+
+ // Add relations that don't exist yet
+ var relationsToAdd = relations.Except(existingRelations.Keys).Select(x => new ReadOnlyRelation(entity.Id, x.ChildId, x.RelationTypeId));
+ RelationRepository.SaveBulk(relationsToAdd);
+
+ // Delete relations that don't exist anymore
+ foreach (IRelation relation in existingRelations.Where(x => !relations.Contains(x.Key)).Select(x => x.Value))
+ {
+ RelationRepository.Delete(relation);
+ }
}
///
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
index 0d06582739..e23cfde52b 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
@@ -18,18 +18,28 @@ internal abstract class BlockEditorPropertyValueEditor : BlockV
{
private readonly IJsonSerializer _jsonSerializer;
private BlockEditorValues? _blockEditorValues;
+ private readonly IDataTypeService _dataTypeService;
+ private readonly PropertyEditorCollection _propertyEditors;
+ private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
+ private readonly ILogger _logger;
protected BlockEditorPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
ILocalizedTextService textService,
ILogger> logger,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper)
- : base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
- => _jsonSerializer = jsonSerializer;
+ : base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactories)
+ {
+ _propertyEditors = propertyEditors;
+ _dataValueReferenceFactories = dataValueReferenceFactories;
+ _dataTypeService = dataTypeService;
+ _logger = logger;
+ }
protected BlockEditorValues BlockEditorValues
{
@@ -40,29 +50,57 @@ internal abstract class BlockEditorPropertyValueEditor : BlockV
///
public override IEnumerable GetReferences(object? value)
{
- var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
-
- BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
- if (blockEditorData == null)
+ // Group by property editor alias to avoid duplicate lookups and optimize value parsing
+ foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
- return Enumerable.Empty();
- }
+ if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
+ {
+ continue;
+ }
- return GetBlockValueReferences(blockEditorData.BlockValue);
+ // Use distinct values to avoid duplicate parsing of the same value
+ foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
+ {
+ yield return reference;
+ }
+ }
}
///
public override IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId)
+ {
+ foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value))
+ {
+ if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) ||
+ dataEditor.GetValueEditor() is not IDataValueTags dataValueTags)
+ {
+ continue;
+ }
+
+ object? configuration = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration;
+ foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId))
+ {
+ yield return tag;
+ }
+ }
+ }
+
+ private IEnumerable GetAllPropertyValues(object? value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
- if (blockEditorData == null)
+ if (blockEditorData is null)
{
- return Enumerable.Empty();
+ yield break;
}
- return GetBlockValueTags(blockEditorData.BlockValue, languageId);
+ // Return all property values from the content and settings data
+ IEnumerable data = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData);
+ foreach (BlockItemData.BlockPropertyValue propertyValue in data.SelectMany(x => x.PropertyValues.Select(x => x.Value)))
+ {
+ yield return propertyValue;
+ }
}
// note: there is NO variant support here
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
index c7c971047a..24ebbb9d82 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
@@ -50,6 +50,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
public BlockGridEditorPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
ILocalizedTextService textService,
ILogger logger,
@@ -58,7 +59,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
IIOHelper ioHelper,
IContentTypeService contentTypeService,
IPropertyValidationService propertyValidationService)
- : base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
+ : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
{
BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), contentTypeService, logger);
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService));
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
index d7199404fc..e0272fc34b 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
@@ -57,6 +57,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
DataEditorAttribute attribute,
BlockEditorDataConverter blockEditorDataConverter,
PropertyEditorCollection propertyEditors,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
IContentTypeService contentTypeService,
ILocalizedTextService textService,
@@ -65,7 +66,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IPropertyValidationService propertyValidationService) :
- base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
+ base(attribute, propertyEditors, dataValueReferenceFactories,dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
{
BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, contentTypeService, logger);
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService));
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
index 01a2cba307..994d7fcbfe 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
@@ -16,6 +16,7 @@ internal abstract class BlockValuePropertyValueEditorBase : Dat
private readonly IDataTypeService _dataTypeService;
private readonly PropertyEditorCollection _propertyEditors;
private readonly ILogger _logger;
+ private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactoryCollection;
protected BlockValuePropertyValueEditorBase(
DataEditorAttribute attribute,
@@ -25,12 +26,14 @@ internal abstract class BlockValuePropertyValueEditorBase : Dat
ILogger logger,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
- IIOHelper ioHelper)
+ IIOHelper ioHelper,
+ DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection)
: base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_logger = logger;
+ _dataValueReferenceFactoryCollection = dataValueReferenceFactoryCollection;
}
///
@@ -38,26 +41,31 @@ internal abstract class BlockValuePropertyValueEditorBase : Dat
protected IEnumerable GetBlockValueReferences(TValue blockValue)
{
- var result = new List();
-
- // loop through all content and settings data
- foreach (BlockItemData row in blockValue.ContentData.Concat(blockValue.SettingsData))
+ var result = new HashSet();
+ BlockItemData.BlockPropertyValue[] propertyValues = blockValue.ContentData.Concat(blockValue.SettingsData)
+ .SelectMany(x => x.PropertyValues.Values).ToArray();
+ foreach (IGrouping valuesByPropertyEditorAlias in propertyValues.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
- foreach (KeyValuePair prop in row.PropertyValues)
+ if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
- IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
+ continue;
+ }
- IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
- if (!(valueEditor is IDataValueReference reference))
+ var districtValues = valuesByPropertyEditorAlias.Distinct().ToArray();
+
+ if (dataEditor.GetValueEditor() is IDataValueReference reference)
+ {
+ foreach (UmbracoEntityReference value in districtValues.SelectMany(reference.GetReferences))
{
- continue;
+ result.Add(value);
}
+ }
- var val = prop.Value.Value?.ToString();
+ IEnumerable references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, districtValues);
- IEnumerable refs = reference.GetReferences(val);
-
- result.AddRange(refs);
+ foreach (UmbracoEntityReference value in references)
+ {
+ result.Add(value);
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs
index 2b4ac75042..ce867d29e4 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs
@@ -10,9 +10,16 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
+///
+/// Handles nested Udi keys when
+/// - saving: Empty keys get generated
+/// - copy: keys get replaced by new ones while keeping references intact
+/// - scaffolding: keys get replaced by new ones while keeping references intact
+///
public abstract class ComplexPropertyEditorContentNotificationHandler :
INotificationHandler,
- INotificationHandler
+ INotificationHandler,
+ INotificationHandler
{
protected abstract string EditorAlias { get; }
@@ -31,6 +38,12 @@ public abstract class ComplexPropertyEditorContentNotificationHandler :
}
}
+ public void Handle(ContentScaffoldedNotification notification)
+ {
+ IEnumerable props = notification.Scaffold.GetPropertiesByEditor(EditorAlias);
+ UpdatePropertyValues(props, false);
+ }
+
protected abstract string FormatPropertyValue(string rawJson, bool onlyMissingKeys);
private void UpdatePropertyValues(IEnumerable props, bool onlyMissingKeys)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
index 1105b76ac0..0a3f2454f7 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
@@ -91,9 +91,10 @@ public class NestedContentPropertyEditor : DataEditor
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags
{
private readonly IDataTypeService _dataTypeService;
+ private readonly PropertyEditorCollection _propertyEditors;
+ private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
private readonly ILogger _logger;
private readonly NestedContentValues _nestedContentValues;
- private readonly PropertyEditorCollection _propertyEditors;
public NestedContentPropertyValueEditor(
IDataTypeService dataTypeService,
@@ -102,16 +103,19 @@ public class NestedContentPropertyEditor : DataEditor
IShortStringHelper shortStringHelper,
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
ILogger logger,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IPropertyValidationService propertyValidationService)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
- _propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
+ _propertyEditors = propertyEditors;
+ _dataValueReferenceFactories = dataValueReferenceFactories;
_logger = logger;
_nestedContentValues = new NestedContentValues(contentTypeService);
+
Validators.Add(new NestedContentValidator(propertyValidationService, _nestedContentValues, contentTypeService));
}
@@ -139,66 +143,47 @@ public class NestedContentPropertyEditor : DataEditor
}
}
+ ///
public IEnumerable GetReferences(object? value)
{
- var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
-
- var result = new List();
-
- foreach (NestedContentValues.NestedContentRowValue row in _nestedContentValues.GetPropertyValues(rawJson))
+ // Group by property editor alias to avoid duplicate lookups and optimize value parsing
+ foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
- foreach (KeyValuePair prop in
- row.PropertyValues)
+ if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
- IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
+ continue;
+ }
- IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
- if (!(valueEditor is IDataValueReference reference))
- {
- continue;
- }
-
- var val = prop.Value.Value?.ToString();
-
- IEnumerable refs = reference.GetReferences(val);
-
- result.AddRange(refs);
+ // Use distinct values to avoid duplicate parsing of the same value
+ foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
+ {
+ yield return reference;
}
}
-
- return result;
}
///
public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId)
{
- IReadOnlyList rows =
- _nestedContentValues.GetPropertyValues(value);
-
- var result = new List();
-
- foreach (NestedContentValues.NestedContentRowValue row in rows.ToList())
+ foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value))
{
- foreach (KeyValuePair prop in row.PropertyValues
- .ToList())
+ if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) ||
+ dataEditor.GetValueEditor() is not IDataValueTags dataValueTags)
{
- IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
+ continue;
+ }
- IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
- if (valueEditor is not IDataValueTags tagsProvider)
- {
- continue;
- }
-
- object? configurationObject = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.ConfigurationObject;
-
- result.AddRange(tagsProvider.GetTags(prop.Value.Value, configurationObject, languageId));
+ object? configurationObject = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.ConfigurationObject;
+ foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configurationObject, languageId))
+ {
+ yield return tag;
}
}
-
- return result;
}
+ private IEnumerable GetAllPropertyValues(object? value)
+ => _nestedContentValues.GetPropertyValues(value).SelectMany(x => x.PropertyValues.Values);
+
#region DB to String
public override string ConvertDbToString(IPropertyType propertyType, object? propertyValue)
@@ -424,7 +409,8 @@ public class NestedContentPropertyEditor : DataEditor
// set values to null
row.PropertyValues[elementTypeProp.Alias] = new NestedContentValues.NestedContentPropertyValue
{
- PropertyType = elementTypeProp, Value = null,
+ PropertyType = elementTypeProp,
+ Value = null,
};
row.RawPropertyValues[elementTypeProp.Alias] = null;
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
index d16239ce0e..39722b5314 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
@@ -189,8 +189,9 @@ public class RichTextPropertyEditor : DataEditor
IHtmlSanitizer htmlSanitizer,
IHtmlMacroParameterParser macroParameterParser,
IContentTypeService contentTypeService,
- IPropertyValidationService propertyValidationService)
- : base(attribute, propertyEditors, dataTypeService, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper)
+ IPropertyValidationService propertyValidationService,
+ DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection)
+ : base(attribute, propertyEditors, dataTypeService, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_imageSourceParser = imageSourceParser;
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs
index c7fa4e4592..75851ee856 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs
@@ -4,6 +4,6 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
internal static partial class RichTextParsingRegexes
{
- [GeneratedRegex(".[^\"]*)\"><\\/umb-rte-block(?:-inline)?>")]
+ [GeneratedRegex(".[^\"]*)\"><\\/umb-rte-block(?:-inline)?>")]
public static partial Regex BlockRegex();
}
diff --git a/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs
index 5257b2f141..6c7445d2da 100644
--- a/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs
+++ b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Umbraco.Cms.Core;
@@ -6,6 +7,7 @@ using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Macros;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Templates;
@@ -15,12 +17,23 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
private readonly ILogger _logger;
private readonly IMacroService _macroService;
private readonly ParameterEditorCollection _parameterEditors;
+ private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
+ [Obsolete("Use the non-obsolete overload instead, scheduled for removal in v14")]
public HtmlMacroParameterParser(IMacroService macroService, ILogger logger, ParameterEditorCollection parameterEditors)
+ : this(
+ macroService,
+ logger,
+ parameterEditors,
+ StaticServiceProvider.Instance.GetRequiredService())
+ { }
+
+ public HtmlMacroParameterParser(IMacroService macroService, ILogger logger, ParameterEditorCollection parameterEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories)
{
_macroService = macroService;
_logger = logger;
_parameterEditors = parameterEditors;
+ _dataValueReferenceFactories = dataValueReferenceFactories;
}
///
@@ -41,6 +54,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
(macroAlias, macroAttributes) => foundMacros.Add(new Tuple>(
macroAlias,
new Dictionary(macroAttributes, StringComparer.OrdinalIgnoreCase))));
+
foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros))
{
yield return umbracoEntityReference;
@@ -52,8 +66,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
///
///
///
- public IEnumerable FindUmbracoEntityReferencesFromGridControlMacros(
- IEnumerable macroGridControls)
+ public IEnumerable FindUmbracoEntityReferencesFromGridControlMacros(IEnumerable macroGridControls)
{
var foundMacros = new List>>();
@@ -65,8 +78,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
// Collect any macro parameters that contain the media udi format
if (gridMacro is not null && gridMacro.MacroParameters is not null && gridMacro.MacroParameters.Any())
{
- foundMacros.Add(
- new Tuple>(gridMacro.MacroAlias, gridMacro.MacroParameters));
+ foundMacros.Add(new Tuple>(gridMacro.MacroAlias, gridMacro.MacroParameters));
}
}
@@ -101,14 +113,12 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
continue;
}
- foundMacroUmbracoEntityReferences.Add(
- new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Macro, macroConfig.Key)));
+ foundMacroUmbracoEntityReferences.Add(new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Macro, macroConfig.Key)));
// Only do this if the macros actually have parameters
if (macroConfig.Properties.Keys.Any(f => f != "macroAlias"))
{
- foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacroParameters(
- macro.Item2, macroConfig, _parameterEditors))
+ foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacroParameters(macro.Item2, macroConfig, _parameterEditors))
{
yield return umbracoEntityReference;
}
@@ -130,41 +140,23 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
/// look up the corresponding property editor for a macro parameter
///
///
- private IEnumerable GetUmbracoEntityReferencesFromMacroParameters(
- Dictionary macroParameters, IMacro macroConfig, ParameterEditorCollection parameterEditors)
+ private IEnumerable GetUmbracoEntityReferencesFromMacroParameters(Dictionary macroParameters, IMacro macroConfig, ParameterEditorCollection parameterEditors)
{
- var foundUmbracoEntityReferences = new List();
foreach (IMacroProperty parameter in macroConfig.Properties)
{
if (macroParameters.TryGetValue(parameter.Alias, out var parameterValue))
{
var parameterEditorAlias = parameter.EditorAlias;
-
- // Lookup propertyEditor from the registered ParameterEditors with the implmementation to avoid looking up for each parameter
- IDataEditor? parameterEditor = parameterEditors.FirstOrDefault(f =>
- string.Equals(f.Alias, parameterEditorAlias, StringComparison.OrdinalIgnoreCase));
+ IDataEditor? parameterEditor = parameterEditors.FirstOrDefault(f => string.Equals(f.Alias, parameterEditorAlias, StringComparison.OrdinalIgnoreCase));
if (parameterEditor is not null)
{
- // Get the ParameterValueEditor for this PropertyEditor (where the GetReferences method is implemented) - cast as IDataValueReference to determine if 'it is' implemented for the editor
- if (parameterEditor.GetValueEditor() is IDataValueReference parameterValueEditor)
+ foreach (UmbracoEntityReference entityReference in _dataValueReferenceFactories.GetReferences(parameterEditor, parameterValue))
{
- foreach (UmbracoEntityReference entityReference in parameterValueEditor.GetReferences(
- parameterValue))
- {
- foundUmbracoEntityReferences.Add(entityReference);
- }
- }
- else
- {
- _logger.LogInformation(
- "{0} doesn't have a ValueEditor that implements IDataValueReference",
- parameterEditor.Alias);
+ yield return entityReference;
}
}
}
}
-
- return foundUmbracoEntityReferences;
}
// Poco class to deserialise the Json for a Macro Control
diff --git a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs
index d854de4b95..6897f04d72 100644
--- a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs
+++ b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs
@@ -36,7 +36,7 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab
IDomainCache domainCache,
IOptions globalSettings,
IVariationContextAccessor variationContextAccessor)
- : base(previewDefault)
+ : base(variationContextAccessor, previewDefault)
{
_snapshot = snapshot;
_snapshotCache = snapshotCache;
diff --git a/src/Umbraco.PublishedCache.NuCache/MediaCache.cs b/src/Umbraco.PublishedCache.NuCache/MediaCache.cs
index 014140e884..516d2f555e 100644
--- a/src/Umbraco.PublishedCache.NuCache/MediaCache.cs
+++ b/src/Umbraco.PublishedCache.NuCache/MediaCache.cs
@@ -17,7 +17,7 @@ public class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableDa
#region Constructors
public MediaCache(bool previewDefault, ContentStore.Snapshot snapshot, IVariationContextAccessor variationContextAccessor)
- : base(previewDefault)
+ : base(variationContextAccessor, previewDefault)
{
_snapshot = snapshot;
_variationContextAccessor = variationContextAccessor;
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
index 2d9c4bc09a..d6d024325d 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
@@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Validation;
+using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
@@ -635,17 +636,33 @@ public class ContentController : ContentControllerBase
[OutgoingEditorModelEvent]
public ActionResult GetEmptyBlueprint(int blueprintId, int parentId)
{
- IContent? blueprint = _contentService.GetBlueprintById(blueprintId);
- if (blueprint == null)
+ IContent? scaffold;
+ using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
- return NotFound();
+ IContent? blueprint = _contentService.GetBlueprintById(blueprintId);
+ if (blueprint is null)
+ {
+ return NotFound();
+ }
+ scaffold = (IContent)blueprint.DeepClone();
+
+ scaffold.Id = 0;
+ scaffold.Name = string.Empty;
+ scaffold.ParentId = parentId;
+
+ var scaffoldedNotification = new ContentScaffoldedNotification(blueprint, scaffold, parentId, new EventMessages());
+ if (scope.Notifications.PublishCancelable(scaffoldedNotification))
+ {
+ scope.Complete();
+ return Problem("Scaffolding was cancelled");
+ }
+
+ scope.Complete();
}
- blueprint.Id = 0;
- blueprint.Name = string.Empty;
- blueprint.ParentId = parentId;
- ContentItemDisplay? mapped = _umbracoMapper.Map(blueprint);
+
+ ContentItemDisplay? mapped = _umbracoMapper.Map(scaffold);
if (mapped is not null)
{
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
index 4969a4e5c9..8cbb4adb3e 100644
--- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
@@ -39,9 +40,9 @@ public class BackOfficeSignInManager : UmbracoSignInManager confirmation,
IEventAggregator eventAggregator,
IOptions securitySettings,
- IOptions backOfficeAuthenticationTypeSettings
- )
- : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, securitySettings)
+ IOptions backOfficeAuthenticationTypeSettings,
+ IRequestCache requestCache)
+ : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, securitySettings, requestCache)
{
_userManager = userManager;
_externalLogins = externalLogins;
@@ -50,6 +51,36 @@ public class BackOfficeSignInManager : UmbracoSignInManager claimsFactory,
+ IOptions optionsAccessor,
+ IOptions globalSettings,
+ ILogger> logger,
+ IAuthenticationSchemeProvider schemes,
+ IUserConfirmation confirmation,
+ IEventAggregator eventAggregator,
+ IOptions securitySettings)
+ : this(
+ userManager,
+ contextAccessor,
+ externalLogins,
+ claimsFactory,
+ optionsAccessor,
+ globalSettings,
+ logger,
+ schemes,
+ confirmation,
+ eventAggregator,
+ securitySettings,
+ StaticServiceProvider.Instance.GetRequiredService>(),
+ StaticServiceProvider.Instance.GetRequiredService()
+ )
+ {
+ }
protected override string AuthenticationType => _backOfficeAuthenticationTypeSettings.Value.AuthenticationType;
diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
index 9a8aaa72f4..1139c04270 100644
--- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
+++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
@@ -32,13 +33,41 @@ public class MemberSignInManager : UmbracoSignInManager, IMe
IUserConfirmation confirmation,
IMemberExternalLoginProviders memberExternalLoginProviders,
IEventAggregator eventAggregator,
- IOptions securitySettings)
- : base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, securitySettings)
+ IOptions securitySettings,
+ IRequestCache requestCache)
+ : base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, securitySettings, requestCache)
{
_memberExternalLoginProviders = memberExternalLoginProviders;
_eventAggregator = eventAggregator;
}
+ [Obsolete("Use non-obsolete constructor. This is scheduled for removal in V15.")]
+ public MemberSignInManager(
+ UserManager memberManager,
+ IHttpContextAccessor contextAccessor,
+ IUserClaimsPrincipalFactory claimsFactory,
+ IOptions optionsAccessor,
+ ILogger> logger,
+ IAuthenticationSchemeProvider schemes,
+ IUserConfirmation confirmation,
+ IMemberExternalLoginProviders memberExternalLoginProviders,
+ IEventAggregator eventAggregator,
+ IOptions securitySettings)
+ : this(
+ memberManager,
+ contextAccessor,
+ claimsFactory,
+ optionsAccessor,
+ logger,
+ schemes,
+ confirmation,
+ memberExternalLoginProviders,
+ eventAggregator,
+ securitySettings,
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
[Obsolete("Use non-obsolete constructor. This is scheduled for removal in V14.")]
public MemberSignInManager(
UserManager memberManager,
diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs
index 84cbce6d8d..f52db46241 100644
--- a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs
+++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security;
@@ -19,6 +20,7 @@ namespace Umbraco.Cms.Web.Common.Security;
public abstract class UmbracoSignInManager : SignInManager
where TUser : UmbracoIdentityUser
{
+ private readonly IRequestCache _requestCache;
private SecuritySettings _securitySettings;
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
@@ -44,7 +46,31 @@ public abstract class UmbracoSignInManager : SignInManager
logger,
schemes,
confirmation,
- StaticServiceProvider.Instance.GetRequiredService>())
+ StaticServiceProvider.Instance.GetRequiredService>(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ [Obsolete("Use non-obsolete constructor. This is scheduled for removal in V15.")]
+ public UmbracoSignInManager(
+ UserManager userManager,
+ IHttpContextAccessor contextAccessor,
+ IUserClaimsPrincipalFactory claimsFactory,
+ IOptions optionsAccessor,
+ ILogger> logger,
+ IAuthenticationSchemeProvider schemes,
+ IUserConfirmation confirmation,
+ IOptions securitySettingsOptions)
+ : this(
+ userManager,
+ contextAccessor,
+ claimsFactory,
+ optionsAccessor,
+ logger,
+ schemes,
+ confirmation,
+ securitySettingsOptions,
+ StaticServiceProvider.Instance.GetRequiredService())
{
}
@@ -56,9 +82,11 @@ public abstract class UmbracoSignInManager : SignInManager
ILogger> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation confirmation,
- IOptions securitySettingsOptions)
+ IOptions securitySettingsOptions,
+ IRequestCache requestCache)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
+ _requestCache = requestCache;
_securitySettings = securitySettingsOptions.Value;
}
@@ -370,7 +398,13 @@ public abstract class UmbracoSignInManager : SignInManager
if (_securitySettings.AllowConcurrentLogins is false)
{
- await UserManager.UpdateSecurityStampAsync(user);
+
+ if (_requestCache.Get("SecurityStampUpdated") is null)
+ {
+ await UserManager.UpdateSecurityStampAsync(user);
+ _requestCache.Set("SecurityStampUpdated", true);
+ }
+
}
Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json
index 9916f2b270..c5ba92ffc2 100644
--- a/src/Umbraco.Web.UI.Client/package-lock.json
+++ b/src/Umbraco.Web.UI.Client/package-lock.json
@@ -39,7 +39,7 @@
"ng-file-upload": "12.2.13",
"nouislider": "15.7.1",
"spectrum-colorpicker2": "2.0.10",
- "tinymce": "6.8.1",
+ "tinymce": "6.8.2",
"typeahead.js": "0.11.1",
"underscore": "1.13.6",
"wicg-inert": "3.1.2"
@@ -16606,9 +16606,9 @@
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"node_modules/tinymce": {
- "version": "6.8.1",
- "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.1.tgz",
- "integrity": "sha512-WYPvMXIjBrXM/oBiqGCbT2a8ptiO3TWXm/xxPWDCl8SxRKMW7Rfp0Lk190E9fXmX6uh9lJMRCnmKHzvryz0ftA=="
+ "version": "6.8.2",
+ "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.2.tgz",
+ "integrity": "sha512-Lho79o2Y1Yn+XdlTEkHTEkEmzwYWTXz7IUsvPwxJF3VTtgHUIAAuBab29kik+f2KED3rZvQavr9D7sHVMJ9x4A=="
},
"node_modules/to-absolute-glob": {
"version": "2.0.2",
diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index f23a0ab750..ec8ec22307 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -51,7 +51,7 @@
"ng-file-upload": "12.2.13",
"nouislider": "15.7.1",
"spectrum-colorpicker2": "2.0.10",
- "tinymce": "6.8.1",
+ "tinymce": "6.8.2",
"typeahead.js": "0.11.1",
"underscore": "1.13.6",
"wicg-inert": "3.1.2"
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
index 812fec6e9c..7821c00e69 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
@@ -12,22 +12,18 @@ angular.module("umbraco.directives")
replace: true,
link: function (scope, element, attrs) {
- // TODO: A lot of the code below should be shared between the grid rte and the normal rte
-
scope.isLoading = true;
- var promises = [];
-
//To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias
// because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because
// we have this mini content editor panel that can be launched with MNTP.
scope.textAreaHtmlId = scope.uniqueId + "_" + String.CreateGuid();
- var editorConfig = scope.configuration ? scope.configuration : null;
+ let editorConfig = scope.configuration ? scope.configuration : null;
if (!editorConfig || Utilities.isString(editorConfig)) {
editorConfig = tinyMceService.defaultPrevalues();
//for the grid by default, we don't want to include the macro or the block-picker toolbar
- editorConfig.toolbar = _.without(editorConfig, "umbmacro", "umbblockpicker");
+ editorConfig.toolbar = _.without(editorConfig.toolbar, "umbmacro", "umbblockpicker");
}
//ensure the grid's global config is being passed up to the RTE, these 2 properties need to be in this format
@@ -39,46 +35,50 @@ angular.module("umbraco.directives")
scope.dataTypeKey = scope.datatypeKey; //Yes - this casing is rediculous, but it's because the var starts with `data` so it can't be `data-type-id` :/
//stores a reference to the editor
- var tinyMceEditor = null;
+ let tinyMceEditor = null;
+
+ const assetPromises = [];
//queue file loading
tinyMceAssets.forEach(function (tinyJsAsset) {
- promises.push(assetsService.loadJs(tinyJsAsset, scope));
+ assetPromises.push(assetsService.loadJs(tinyJsAsset, scope));
});
- promises.push(tinyMceService.getTinyMceEditorConfig({
- htmlId: scope.textAreaHtmlId,
- stylesheets: editorConfig.stylesheets,
- toolbar: editorConfig.toolbar,
- mode: editorConfig.mode
- }));
- $q.all(promises).then(function (result) {
+ //wait for assets to load before proceeding
+ $q.all(assetPromises)
+ .then(function () {
+ return tinyMceService.getTinyMceEditorConfig({
+ htmlId: scope.textAreaHtmlId,
+ stylesheets: editorConfig.stylesheets,
+ toolbar: editorConfig.toolbar,
+ mode: editorConfig.mode
+ })
+ })
- var standardConfig = result[promises.length - 1];
+ // Handle additional assets loading depending on the configuration before initializing the editor
+ .then(function (tinyMceConfig) {
+ // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified
+ if (tinyMceConfig.cloudApiKey) {
+ return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`)
+ .then(() => tinyMceConfig);
+ }
+
+ return tinyMceConfig;
+ })
+
+ //wait for config to be ready after assets have loaded
+ .then(function (standardConfig) {
//create a baseline Config to extend upon
- var baseLineConfigObj = {
- maxImageSize: editorConfig.maxImageSize,
- toolbar_sticky: true
+ let baseLineConfigObj = {
+ maxImageSize: editorConfig.maxImageSize
};
- Utilities.extend(baseLineConfigObj, standardConfig);
-
baseLineConfigObj.setup = function (editor) {
//set the reference
tinyMceEditor = editor;
- //initialize the standard editor functionality for Umbraco
- tinyMceService.initializeEditor({
- editor: editor,
- toolbar: editorConfig.toolbar,
- model: scope,
- // Form is found in the scope of the grid controller above us, not in our isolated scope
- // https://github.com/umbraco/Umbraco-CMS/issues/7461
- currentForm: angularHelper.getCurrentForm(scope.$parent)
- });
-
//custom initialization for this editor within the grid
editor.on('init', function (e) {
@@ -96,49 +96,52 @@ angular.module("umbraco.directives")
}, 400);
});
+
+ //initialize the standard editor functionality for Umbraco
+ tinyMceService.initializeEditor({
+ editor: editor,
+ toolbar: editorConfig.toolbar,
+ model: scope,
+ // Form is found in the scope of the grid controller above us, not in our isolated scope
+ // https://github.com/umbraco/Umbraco-CMS/issues/7461
+ currentForm: angularHelper.getCurrentForm(scope.$parent)
+ });
};
- /** Loads in the editor */
- function loadTinyMce() {
-
- //we need to add a timeout here, to force a redraw so TinyMCE can find
- //the elements needed
- $timeout(function () {
- tinymce.init(baseLineConfigObj);
- }, 150, false);
- }
-
- loadTinyMce();
-
- // TODO: This should probably be in place for all RTE, not just for the grid, which means
- // this code can live in tinyMceService.initializeEditor
- var tabShownListener = eventsService.on("app.tabChange", function (e, args) {
-
- var tabId = args.id;
- var myTabId = element.closest(".umb-tab-pane").attr("rel");
-
- if (String(tabId) === myTabId) {
- //the tab has been shown, trigger the mceAutoResize (as it could have timed out before the tab was shown)
- if (tinyMceEditor !== undefined && tinyMceEditor != null) {
- tinyMceEditor.execCommand('mceAutoResize', false, null, null);
- }
- }
-
- });
-
- //when the element is disposed we need to unsubscribe!
- // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
- // element might still be there even after the modal has been hidden.
- scope.$on('$destroy', function () {
- eventsService.unsubscribe(tabShownListener);
- //ensure we unbind this in case the blur doesn't fire above
- if (tinyMceEditor !== undefined && tinyMceEditor != null) {
- tinyMceEditor.destroy()
- }
- });
+ Utilities.extend(baseLineConfigObj, standardConfig);
+ //we need to add a timeout here, to force a redraw so TinyMCE can find
+ //the elements needed
+ $timeout(function () {
+ tinymce.init(baseLineConfigObj);
+ }, 150);
});
+ const tabShownListener = eventsService.on("app.tabChange", function (e, args) {
+
+ const tabId = String(args.id);
+ const myTabId = element.closest(".umb-tab-pane").attr("rel");
+
+ if (tabId === myTabId) {
+ //the tab has been shown, trigger the mceAutoResize (as it could have timed out before the tab was shown)
+ if (tinyMceEditor !== undefined && tinyMceEditor != null) {
+ tinyMceEditor.execCommand('mceAutoResize', false, null, null);
+ }
+ }
+ });
+
+ //when the element is disposed we need to unsubscribe!
+ // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
+ // element might still be there even after the modal has been hidden.
+ scope.$on('$destroy', function () {
+ eventsService.unsubscribe(tabShownListener);
+
+ //ensure we unbind this in case the blur doesn't fire above
+ if (tinyMceEditor) {
+ tinyMceEditor.destroy();
+ tinyMceEditor = null;
+ }
+ });
}
};
});
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
index 8f447e3f23..ebf759c20c 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
@@ -543,10 +543,15 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
* @param {Object} editor the TinyMCE editor instance
*/
createInsertEmbeddedMedia: function (editor, callback) {
- editor.ui.registry.addButton('umbembeddialog', {
+ editor.ui.registry.addToggleButton('umbembeddialog', {
icon: 'embed',
tooltip: 'Embed',
- stateSelector: 'div[data-embed-url]',
+ onSetup: function (api) {
+ const changed = editor.selection.selectorChangedWithUnbind('div[data-embed-url]', (state) =>
+ api.setActive(state)
+ );
+ return () => changed.unbind();
+ },
onAction: function () {
// Get the selected element
@@ -555,6 +560,12 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
var nodeName = selectedElm.nodeName;
var modify = null;
+ // If we have an iframe, we need to get the parent element
+ if (nodeName.toUpperCase() === "IFRAME") {
+ selectedElm = selectedElm.parentElement;
+ nodeName = selectedElm.nodeName;
+ }
+
if (nodeName.toUpperCase() === "DIV" && selectedElm.classList.contains("embeditem")) {
// See if we can go and get the attributes
var embedUrl = editor.dom.getAttrib(selectedElm, "data-embed-url");
@@ -630,10 +641,15 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
* @param {Object} editor the TinyMCE editor instance
*/
createMediaPicker: function (editor, callback) {
- editor.ui.registry.addButton('umbmediapicker', {
+ editor.ui.registry.addToggleButton('umbmediapicker', {
icon: 'image',
tooltip: 'Image Picker',
- stateSelector: 'img[data-udi]',
+ onSetup: function (api) {
+ const changed = editor.selection.selectorChangedWithUnbind('img[data-udi]', (state) =>
+ api.setActive(state)
+ );
+ return () => changed.unbind();
+ },
onAction: function () {
var selectedElm = editor.selection.getNode(),
@@ -781,10 +797,20 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
});
});
- editor.ui.registry.addButton('umbblockpicker', {
+ // Do not add any further controls if the block editor is not available
+ if (!blockEditorApi) {
+ return;
+ }
+
+ editor.ui.registry.addToggleButton('umbblockpicker', {
icon: 'visualblocks',
tooltip: 'Insert Block',
- stateSelector: 'umb-rte-block[data-content-udi], umb-rte-block-inline[data-content-udi]',
+ onSetup: function (api) {
+ const changed = editor.selection.selectorChangedWithUnbind('umb-rte-block[data-content-udi], umb-rte-block-inline[data-content-udi]', (state) =>
+ api.setActive(state)
+ );
+ return () => changed.unbind();
+ },
onAction: function () {
var blockEl = editor.selection.getNode();
@@ -900,9 +926,15 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
}
/** Adds the button instance */
- editor.ui.registry.addButton('umbmacro', {
+ editor.ui.registry.addToggleButton('umbmacro', {
icon: 'preferences',
tooltip: 'Insert macro',
+ onSetup: function (api) {
+ const changed = editor.selection.selectorChangedWithUnbind('div.umb-macro-holder', (state) =>
+ api.setActive(state)
+ );
+ return () => changed.unbind();
+ },
/** The insert macro button click event handler */
onAction: function () {
@@ -1207,32 +1239,47 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
});
}
- editor.ui.registry.addButton('link', {
+ editor.ui.registry.addToggleButton('link', {
icon: 'link',
tooltip: 'Insert/edit link',
shortcut: 'Ctrl+K',
onAction: createLinkList(showDialog),
- stateSelector: 'a[href]'
+ onSetup: function (api) {
+ const changed = editor.selection.selectorChangedWithUnbind('a[href]', (state) =>
+ api.setActive(state)
+ );
+ return () => changed.unbind();
+ }
});
- editor.ui.registry.addButton('unlink', {
+ editor.ui.registry.addToggleButton('unlink', {
icon: 'unlink',
tooltip: 'Remove link',
onAction: () => {
editor.execCommand('unlink');
},
- stateSelector: 'a[href]'
+ onSetup: function (api) {
+ const changed = editor.selection.selectorChangedWithUnbind('a[href]', (state) =>
+ api.setActive(state)
+ );
+ return () => changed.unbind();
+ }
});
editor.addShortcut('Ctrl+K', '', createLinkList(showDialog));
this.showDialog = showDialog;
- editor.ui.registry.addMenuItem('link', {
+ editor.ui.registry.addToggleMenuItem('link', {
icon: 'link',
text: 'Insert link',
shortcut: 'Ctrl+K',
onAction: createLinkList(showDialog),
- stateSelector: 'a[href]',
+ onSetup: function (api) {
+ const changed = editor.selection.selectorChangedWithUnbind('a[href]', (state) =>
+ api.setActive(state)
+ );
+ return () => changed.unbind();
+ },
context: 'insert',
prependToContext: true
});
@@ -1439,6 +1486,10 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
function initBlocks() {
+ if(!args.blockEditorApi) {
+ return;
+ }
+
const blockEls = args.editor.contentDocument.querySelectorAll('umb-rte-block, umb-rte-block-inline');
for (var blockEl of blockEls) {
if(!blockEl._isInitializedUmbBlock) {
@@ -1708,7 +1759,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
});
//Create the insert media plugin
- self.createMediaPicker(args.editor, function (currentTarget, userData, imgDomElement) {
+ self.createMediaPicker(args.editor, function (currentTarget, userData) {
var startNodeId, startNodeIsVirtual;
if (!args.model.config.startNodeId) {
@@ -1732,7 +1783,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
startNodeIsVirtual: startNodeIsVirtual,
dataTypeKey: args.model.dataTypeKey,
submit: function (model) {
- self.insertMediaInEditor(args.editor, model.selection[0], imgDomElement);
+ self.insertMediaInEditor(args.editor, model.selection[0]);
editorService.close();
},
close: function () {
@@ -1743,19 +1794,21 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
});
- //Create the insert block plugin
- self.createBlockPicker(args.editor, args.blockEditorApi, function (currentTarget, userData, imgDomElement) {
- args.blockEditorApi.showCreateDialog(0, false, (newBlock) => {
- // TODO: Handle if its an array:
- if(Utilities.isArray(newBlock)) {
- newBlock.forEach(block => {
- self.insertBlockInEditor(args.editor, block.layout.contentUdi, block.config.displayInline);
- });
- } else {
- self.insertBlockInEditor(args.editor, newBlock.layout.contentUdi, newBlock.config.displayInline);
- }
+ if(args.blockEditorApi) {
+ //Create the insert block plugin
+ self.createBlockPicker(args.editor, args.blockEditorApi, function (currentTarget, userData, imgDomElement) {
+ args.blockEditorApi.showCreateDialog(0, false, (newBlock) => {
+ // TODO: Handle if its an array:
+ if(Utilities.isArray(newBlock)) {
+ newBlock.forEach(block => {
+ self.insertBlockInEditor(args.editor, block.layout.contentUdi, block.config.displayInline);
+ });
+ } else {
+ self.insertBlockInEditor(args.editor, newBlock.layout.contentUdi, newBlock.config.displayInline);
+ }
+ });
});
- });
+ }
//Create the embedded plugin
self.createInsertEmbeddedMedia(args.editor, function (activeElement, modify) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html
index fb6f3b2a4b..2fbd0b2fe3 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html
@@ -4,8 +4,8 @@
configuration="model.config.rte"
value="control.value"
unique-id="control.$uniqueId"
- datatype-key="{{model.dataTypeKey}}"
- ignore-user-start-nodes="{{model.config.ignoreUserStartNodes}}">
+ datatype-key="{{model.dataTypeKey}}"
+ ignore-user-start-nodes="{{model.config.ignoreUserStartNodes}}">
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
index 2bb5b6c3a0..7f06215148 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
@@ -43,6 +43,7 @@
var vm = this;
vm.readonly = false;
+ vm.noBlocksMode = false;
vm.tinyMceEditor = null;
$attrs.$observe('readonly', (value) => {
@@ -102,58 +103,69 @@
var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "umbVariantContentController");
vm.umbVariantContent = found ? found.vm : null;
if (!vm.umbVariantContent) {
- throw "Could not find umbVariantContent in the $scope chain";
+ //Could not find umbVariantContent in the $scope chain, lets go into no blocks mode:
+ vm.noBlocksMode = true;
+ vm.blocksLoading = false;
+ this.updateLoading();
}
}
+ const config = vm.model.config || {};
+
// set the onValueChanged callback, this will tell us if the block list model changed on the server
// once the data is submitted. If so we need to re-initialize
vm.model.onValueChanged = onServerValueChanged;
- liveEditing = vm.model.config.useLiveEditing;
+ liveEditing = config.useLiveEditing;
vm.listWrapperStyles = {};
- if (vm.model.config.maxPropertyWidth) {
- vm.listWrapperStyles['max-width'] = vm.model.config.maxPropertyWidth;
+ if (config.maxPropertyWidth) {
+ vm.listWrapperStyles['max-width'] = config.maxPropertyWidth;
}
- // We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated.
+ // We need to ensure that the property model value is an object, this is needed for modelObject to receive a reference and keep that updated.
ensurePropertyValue(vm.model.value);
- var scopeOfExistence = $scope;
- if (vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) {
- scopeOfExistence = vm.umbVariantContentEditors.getScope();
- } else if(vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) {
- scopeOfExistence = vm.umbElementEditorContent.getScope();
+ const assetPromises = [];
+
+ if(vm.noBlocksMode !== true) {
+
+ var scopeOfExistence = $scope;
+ if (vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) {
+ scopeOfExistence = vm.umbVariantContentEditors.getScope();
+ } else if(vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) {
+ scopeOfExistence = vm.umbElementEditorContent.getScope();
+ }
+
+ /*
+ copyAllBlocksAction = {
+ labelKey: "clipboard_labelForCopyAllEntries",
+ labelTokens: [vm.model.label],
+ icon: "icon-documents",
+ method: requestCopyAllBlocks,
+ isDisabled: true,
+ useLegacyIcon: false
+ };
+
+ deleteAllBlocksAction = {
+ labelKey: "clipboard_labelForRemoveAllEntries",
+ labelTokens: [],
+ icon: "icon-trash",
+ method: requestDeleteAllBlocks,
+ isDisabled: true,
+ useLegacyIcon: false
+ };
+
+ var propertyActions = [copyAllBlocksAction, deleteAllBlocksAction];
+ */
+
+ // Create Model Object, to manage our data for this Block Editor.
+ modelObject = blockEditorService.createModelObject(vm.model.value.blocks, vm.model.editor, config.blocks, scopeOfExistence, $scope);
+ const blockModelObjectLoading = modelObject.load();
+ assetPromises.push(blockModelObjectLoading)
+ blockModelObjectLoading.then(onLoaded);
}
- /*
- copyAllBlocksAction = {
- labelKey: "clipboard_labelForCopyAllEntries",
- labelTokens: [vm.model.label],
- icon: "icon-documents",
- method: requestCopyAllBlocks,
- isDisabled: true,
- useLegacyIcon: false
- };
-
- deleteAllBlocksAction = {
- labelKey: "clipboard_labelForRemoveAllEntries",
- labelTokens: [],
- icon: "icon-trash",
- method: requestDeleteAllBlocks,
- isDisabled: true,
- useLegacyIcon: false
- };
-
- var propertyActions = [copyAllBlocksAction, deleteAllBlocksAction];
- */
-
- // Create Model Object, to manage our data for this Block Editor.
- modelObject = blockEditorService.createModelObject(vm.model.value.blocks, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope);
- const blockModelObjectLoading = modelObject.load()
- blockModelObjectLoading.then(onLoaded);
-
// ******************** //
// RTE PART:
@@ -165,188 +177,175 @@
// we have this mini content editor panel that can be launched with MNTP.
vm.textAreaHtmlId = vm.model.alias + "_" + String.CreateGuid();
- var editorConfig = vm.model.config ? vm.model.config.editor : null;
+ var editorConfig = config.editor ?? null;
if (!editorConfig || Utilities.isString(editorConfig)) {
editorConfig = tinyMceService.defaultPrevalues();
}
+
var width = editorConfig.dimensions ? parseInt(editorConfig.dimensions.width, 10) || null : null;
var height = editorConfig.dimensions ? parseInt(editorConfig.dimensions.height, 10) || null : null;
vm.containerWidth = "auto";
vm.containerHeight = "auto";
- vm.containerOverflow = "inherit";
-
- const assetPromises = [blockModelObjectLoading];
+ vm.containerOverflow = "inherit"
//queue file loading
tinyMceAssets.forEach(function (tinyJsAsset) {
assetPromises.push(assetsService.loadJs(tinyJsAsset, $scope));
});
- const tinyMceConfigDeferred = $q.defer();
-
//wait for assets to load before proceeding
- $q.all(assetPromises).then(function () {
-
- tinyMceService.getTinyMceEditorConfig({
- htmlId: vm.textAreaHtmlId,
- stylesheets: editorConfig.stylesheets,
- toolbar: editorConfig.toolbar,
- mode: editorConfig.mode
+ $q.all(assetPromises)
+ .then(function () {
+ return tinyMceService.getTinyMceEditorConfig({
+ htmlId: vm.textAreaHtmlId,
+ stylesheets: editorConfig.stylesheets,
+ toolbar: editorConfig.toolbar,
+ mode: editorConfig.mode
+ })
})
- .then(function (tinyMceConfig) {
- // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified
- if (tinyMceConfig.cloudApiKey) {
- return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`)
- .then(() => tinyMceConfig);
+
+ // Handle additional assets loading depending on the configuration before initializing the editor
+ .then(function (tinyMceConfig) {
+ // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified
+ if (tinyMceConfig.cloudApiKey) {
+ return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`)
+ .then(() => tinyMceConfig);
+ }
+
+ return tinyMceConfig;
+ })
+
+ //wait for config to be ready after assets have loaded
+ .then(function (standardConfig) {
+
+ if (height !== null) {
+ standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1);
}
- return tinyMceConfig;
- })
- .then(function (tinyMceConfig) {
- tinyMceConfigDeferred.resolve(tinyMceConfig);
- });
- });
+ //create a baseline Config to extend upon
+ let baseLineConfigObj = {
+ maxImageSize: editorConfig.maxImageSize,
+ width: width,
+ height: height
+ };
- //wait for config to be ready after assets have loaded
- tinyMceConfigDeferred.promise.then(function (standardConfig) {
+ baseLineConfigObj.setup = function (editor) {
- if (height !== null) {
- standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1);
- }
+ //set the reference
+ vm.tinyMceEditor = editor;
- //create a baseline Config to extend upon
- var baseLineConfigObj = {
- maxImageSize: editorConfig.maxImageSize,
- width: width,
- height: height
- };
-
- baseLineConfigObj.setup = function (editor) {
-
- //set the reference
- vm.tinyMceEditor = editor;
-
- vm.tinyMceEditor.on('init', function (e) {
- $timeout(function () {
- vm.rteLoading = false;
- vm.updateLoading();
- });
- });
- vm.tinyMceEditor.on("focus", function () {
- $element[0].dispatchEvent(new CustomEvent('umb-rte-focus', {composed: true, bubbles: true}));
- });
- vm.tinyMceEditor.on("blur", function () {
- $element[0].dispatchEvent(new CustomEvent('umb-rte-blur', {composed: true, bubbles: true}));
- });
-
- //initialize the standard editor functionality for Umbraco
- tinyMceService.initializeEditor({
- //scope: $scope,
- editor: editor,
- toolbar: editorConfig.toolbar,
- model: vm.model,
- getValue: function () {
- return vm.model.value.markup;
- },
- setValue: function (newVal) {
- vm.model.value.markup = newVal;
- $scope.$evalAsync();
- },
- culture: vm.umbProperty?.culture ?? null,
- segment: vm.umbProperty?.segment ?? null,
- blockEditorApi: vm.blockEditorApi,
- parentForm: vm.propertyForm,
- valFormManager: vm.valFormManager,
- currentFormInput: $scope.rteForm.modelValue
- });
-
- };
-
- Utilities.extend(baseLineConfigObj, standardConfig);
-
- // Readonly mode
- baseLineConfigObj.toolbar = vm.readonly ? false : baseLineConfigObj.toolbar;
- baseLineConfigObj.readonly = vm.readonly ? 1 : baseLineConfigObj.readonly;
-
- // We need to wait for DOM to have rendered before we can find the element by ID.
- $timeout(function () {
- tinymce.init(baseLineConfigObj);
- }, 50);
-
- //listen for formSubmitting event (the result is callback used to remove the event subscription)
- unsubscribe.push($scope.$on("formSubmitting", function () {
- if (vm.tinyMceEditor != null && !vm.rteLoading) {
-
- // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup.
- var blockElements = vm.tinyMceEditor.dom.select(`umb-rte-block, umb-rte-block-inline`);
- const usedContentUdis = blockElements.map(blockElement => blockElement.getAttribute('data-content-udi'));
-
- const unusedBlocks = vm.layout.filter(x => usedContentUdis.indexOf(x.contentUdi) === -1);
- unusedBlocks.forEach(blockLayout => {
- deleteBlock(blockLayout.$block);
- });
-
-
- // Remove Angular Classes from markup:
- var parser = new DOMParser();
- var doc = parser.parseFromString(vm.model.value.markup, 'text/html');
-
- // Get all elements in the parsed document
- var elements = doc.querySelectorAll('*[class]');
- elements.forEach(element => {
- var classAttribute = element.getAttribute("class");
- if (classAttribute) {
- // Split the class attribute by spaces and remove "ng-scope" and "ng-isolate-scope"
- var classes = classAttribute.split(" ");
- var newClasses = classes.filter(function (className) {
- return className !== "ng-scope" && className !== "ng-isolate-scope";
+ vm.tinyMceEditor.on('init', function (e) {
+ $timeout(function () {
+ vm.rteLoading = false;
+ vm.updateLoading();
});
-
- // Update the class attribute with the remaining classes
- if (newClasses.length > 0) {
- element.setAttribute('class', newClasses.join(' '));
- } else {
- // If no remaining classes, remove the class attribute
- element.removeAttribute('class');
- }
- }
+ });
+ vm.tinyMceEditor.on("focus", function () {
+ $element[0].dispatchEvent(new CustomEvent('umb-rte-focus', {composed: true, bubbles: true}));
+ });
+ vm.tinyMceEditor.on("blur", function () {
+ $element[0].dispatchEvent(new CustomEvent('umb-rte-blur', {composed: true, bubbles: true}));
});
- vm.model.value.markup = doc.body.innerHTML;
+ //initialize the standard editor functionality for Umbraco
+ tinyMceService.initializeEditor({
+ //scope: $scope,
+ editor: editor,
+ toolbar: editorConfig.toolbar,
+ model: vm.model,
+ getValue: function () {
+ return vm.model.value.markup;
+ },
+ setValue: function (newVal) {
+ vm.model.value.markup = newVal;
+ $scope.$evalAsync();
+ },
+ culture: vm.umbProperty?.culture ?? null,
+ segment: vm.umbProperty?.segment ?? null,
+ blockEditorApi: vm.noBlocksMode ? undefined : vm.blockEditorApi,
+ parentForm: vm.propertyForm,
+ valFormManager: vm.valFormManager,
+ currentFormInput: $scope.rteForm.modelValue
+ });
- }
- }));
+ };
- vm.focusRTE = function () {
- vm.tinyMceEditor.focus();
- }
+ Utilities.extend(baseLineConfigObj, standardConfig);
+
+ // Readonly mode
+ baseLineConfigObj.toolbar = vm.readonly ? false : baseLineConfigObj.toolbar;
+ baseLineConfigObj.readonly = vm.readonly ? 1 : baseLineConfigObj.readonly;
+
+ // We need to wait for DOM to have rendered before we can find the element by ID.
+ $timeout(function () {
+ tinymce.init(baseLineConfigObj);
+ }, 50);
+
+ //listen for formSubmitting event (the result is callback used to remove the event subscription)
+ unsubscribe.push($scope.$on("formSubmitting", function () {
+ if (vm.tinyMceEditor != null && !vm.rteLoading) {
+
+ // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup.
+ var blockElements = vm.tinyMceEditor.dom.select(`umb-rte-block, umb-rte-block-inline`);
+ const usedContentUdis = blockElements.map(blockElement => blockElement.getAttribute('data-content-udi'));
+
+ const unusedBlocks = vm.layout.filter(x => usedContentUdis.indexOf(x.contentUdi) === -1);
+ unusedBlocks.forEach(blockLayout => {
+ deleteBlock(blockLayout.$block);
+ });
+
+
+ // Remove Angular Classes from markup:
+ var parser = new DOMParser();
+ var doc = parser.parseFromString(vm.model.value.markup, 'text/html');
+
+ // Get all elements in the parsed document
+ var elements = doc.querySelectorAll('*[class]');
+ elements.forEach(element => {
+ var classAttribute = element.getAttribute("class");
+ if (classAttribute) {
+ // Split the class attribute by spaces and remove "ng-scope" and "ng-isolate-scope"
+ var classes = classAttribute.split(" ");
+ var newClasses = classes.filter(function (className) {
+ return className !== "ng-scope" && className !== "ng-isolate-scope";
+ });
+
+ // Update the class attribute with the remaining classes
+ if (newClasses.length > 0) {
+ element.setAttribute('class', newClasses.join(' '));
+ } else {
+ // If no remaining classes, remove the class attribute
+ element.removeAttribute('class');
+ }
+ }
+ });
+
+ vm.model.value.markup = doc.body.innerHTML;
- // When the element is disposed we need to unsubscribe!
- // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
- // element might still be there even after the modal has been hidden.
- $scope.$on('$destroy', function () {
- if (vm.tinyMceEditor != null) {
- if($element) {
- $element[0]?.dispatchEvent(new CustomEvent('blur', {composed: true, bubbles: true}));
}
- vm.tinyMceEditor.destroy();
- vm.tinyMceEditor = null;
- }
- });
+ }));
- });
+ });
};
+ vm.focusRTE = function () {
+ if (vm.tinyMceEditor) {
+ vm.tinyMceEditor.focus();
+ }
+ }
+
// Called when we save the value, the server may return an updated data and our value is re-synced
// we need to deal with that here so that our model values are all in sync so we basically re-initialize.
function onServerValueChanged(newVal, oldVal) {
ensurePropertyValue(newVal);
- modelObject.update(vm.model.value.blocks, $scope);
+ if(modelObject) {
+ modelObject.update(vm.model.value.blocks, $scope);
+ }
onLoaded();
}
@@ -965,6 +964,17 @@
for (const subscription of unsubscribe) {
subscription();
}
+
+ // When the element is disposed we need to unsubscribe!
+ // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
+ // element might still be there even after the modal has been hidden.
+ if (vm.tinyMceEditor != null) {
+ if($element) {
+ $element[0]?.dispatchEvent(new CustomEvent('blur', {composed: true, bubbles: true}));
+ }
+ vm.tinyMceEditor.destroy();
+ vm.tinyMceEditor = null;
+ }
});
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs
index 0e027326a6..f56c974f34 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs
@@ -1,5 +1,4 @@
-using System.Linq;
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.IO;
@@ -16,6 +15,7 @@ public class DataValueEditorReuseTests
{
private Mock _dataValueEditorFactoryMock;
private PropertyEditorCollection _propertyEditorCollection;
+ private DataValueReferenceFactoryCollection _dataValueReferenceFactories;
[SetUp]
public void SetUp()
@@ -32,6 +32,7 @@ public class DataValueEditorReuseTests
Mock.Of()));
_propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty));
+ _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty);
_dataValueEditorFactoryMock
.Setup(m =>
@@ -40,6 +41,7 @@ public class DataValueEditorReuseTests
new DataEditorAttribute("a", "b", "c"),
new BlockListEditorDataConverter(Mock.Of()),
_propertyEditorCollection,
+ _dataValueReferenceFactories,
Mock.Of(),
Mock.Of(),
Mock.Of(),
@@ -95,7 +97,7 @@ public class DataValueEditorReuseTests
{
var blockListPropertyEditor = new BlockListPropertyEditor(
_dataValueEditorFactoryMock.Object,
- new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)),
+ _propertyEditorCollection,
Mock.Of(),
Mock.Of(),
Mock.Of(),
@@ -117,7 +119,7 @@ public class DataValueEditorReuseTests
{
var blockListPropertyEditor = new BlockListPropertyEditor(
_dataValueEditorFactoryMock.Object,
- new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)),
+ _propertyEditorCollection,
Mock.Of(),
Mock.Of(),
Mock.Of(),
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs
index cff072873f..38fc5125dc 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs
@@ -176,14 +176,11 @@ public class DataValueReferenceFactoryCollectionTests
{
var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty);
var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty));
- var properties = new PropertyCollection();
- var resultA = collection.GetAutomaticRelationTypesAliases(propertyEditors).ToArray();
- var resultB = collection.GetAutomaticRelationTypesAliases(properties, propertyEditors).ToArray();
+ var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray();
var expected = Constants.Conventions.RelationTypes.AutomaticRelationTypes;
- CollectionAssert.AreEquivalent(expected, resultA, "Result A does not contain the expected relation type aliases.");
- CollectionAssert.AreEquivalent(expected, resultB, "Result B does not contain the expected relation type aliases.");
+ CollectionAssert.AreEquivalent(expected, result, "Result does not contain the expected relation type aliases.");
}
[Test]
@@ -194,15 +191,11 @@ public class DataValueReferenceFactoryCollectionTests
var labelPropertyEditor = new LabelPropertyEditor(DataValueEditorFactory, IOHelper, EditorConfigurationParser);
var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => labelPropertyEditor.Yield()));
var serializer = new ConfigurationEditorJsonSerializer();
- var property = new Property(new PropertyType(ShortStringHelper, new DataType(labelPropertyEditor, serializer)));
- var properties = new PropertyCollection { property, property }; // Duplicate on purpose to test distinct aliases
- var resultA = collection.GetAutomaticRelationTypesAliases(propertyEditors).ToArray();
- var resultB = collection.GetAutomaticRelationTypesAliases(properties, propertyEditors).ToArray();
+ var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray();
var expected = Constants.Conventions.RelationTypes.AutomaticRelationTypes.Append("umbTest");
- CollectionAssert.AreEquivalent(expected, resultA, "Result A does not contain the expected relation type aliases.");
- CollectionAssert.AreEquivalent(expected, resultB, "Result B does not contain the expected relation type aliases.");
+ CollectionAssert.AreEquivalent(expected, result, "Result does not contain the expected relation type aliases.");
}
private class TestDataValueReferenceFactory : IDataValueReferenceFactory
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs
index 5394de5fd7..b80555bd43 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
+using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Net;
@@ -71,7 +72,8 @@ public class MemberSignInManagerTests
Mock.Of>(),
Mock.Of(),
Mock.Of(),
- Mock.Of>(x => x.Value == new SecuritySettings()));
+ Mock.Of>(x => x.Value == new SecuritySettings()),
+ new DictionaryAppCache());
}
private static Mock MockMemberManager()