Merge remote-tracking branch 'origin/v11/dev' into v12/dev
# Conflicts: # src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs # src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs # src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs # src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Notification that is send out when a Content item has been scaffolded from an original item and basic cleaning has been performed
|
||||
/// </summary>
|
||||
public sealed class ContentScaffoldedNotification : ScaffoldedNotification<IContent>
|
||||
{
|
||||
public ContentScaffoldedNotification(IContent original, IContent scaffold, int parentId, EventMessages messages)
|
||||
: base(original, scaffold, parentId, messages)
|
||||
{
|
||||
}
|
||||
}
|
||||
23
src/Umbraco.Core/Notifications/ScaffoldedNotification.cs
Normal file
23
src/Umbraco.Core/Notifications/ScaffoldedNotification.cs
Normal file
@@ -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<T> : CancelableObjectNotification<T>
|
||||
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; }
|
||||
}
|
||||
@@ -32,15 +32,23 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
|
||||
{
|
||||
var references = new HashSet<UmbracoEntityReference>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Only use edited value for now
|
||||
references.UnionWith(GetReferences(dataEditor, property.Values.Select(x => x.EditedValue)));
|
||||
// Use distinct values to avoid duplicate parsing of the same value
|
||||
var values = new HashSet<object?>(properties.Count);
|
||||
foreach (IPropertyValue propertyValue in propertyValuesByPropertyEditorAlias.SelectMany(x => x))
|
||||
{
|
||||
values.Add(propertyValue.EditedValue);
|
||||
values.Add(propertyValue.PublishedValue);
|
||||
}
|
||||
|
||||
references.UnionWith(GetReferences(dataEditor, values));
|
||||
}
|
||||
|
||||
return references;
|
||||
|
||||
@@ -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<IVariationContextAccessor>(), previewDefault)
|
||||
{
|
||||
}
|
||||
|
||||
public PublishedCacheBase(IVariationContextAccessor variationContextAccessor, bool previewDefault)
|
||||
{
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
PreviewDefault = previewDefault;
|
||||
}
|
||||
|
||||
public bool PreviewDefault { get; }
|
||||
|
||||
|
||||
@@ -347,10 +347,13 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder
|
||||
.AddNotificationHandler<ContentSavingNotification, BlockListPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentCopyingNotification, BlockListPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentScaffoldedNotification, BlockListPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentSavingNotification, BlockGridPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentCopyingNotification, BlockGridPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentScaffoldedNotification, BlockGridPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentSavingNotification, NestedContentPropertyHandler>()
|
||||
.AddNotificationHandler<ContentCopyingNotification, NestedContentPropertyHandler>()
|
||||
.AddNotificationHandler<ContentScaffoldedNotification, NestedContentPropertyHandler>()
|
||||
.AddNotificationHandler<ContentCopiedNotification, FileUploadPropertyEditor>()
|
||||
.AddNotificationHandler<ContentDeletedNotification, FileUploadPropertyEditor>()
|
||||
.AddNotificationHandler<MediaDeletedNotification, FileUploadPropertyEditor>()
|
||||
|
||||
@@ -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<UmbracoEntityReference> references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors);
|
||||
|
||||
// First delete all auto-relations for this entity
|
||||
ISet<string> automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(PropertyEditors);
|
||||
RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
|
||||
|
||||
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<int>())
|
||||
.Where(x => automaticRelationTypeAliases.Contains(x.Alias))
|
||||
.ToDictionary(x => x.Alias, x => x.Id);
|
||||
|
||||
// Lookup node IDs for all GUID based UDIs
|
||||
IEnumerable<Guid> keys = references.Select(x => x.Udi).OfType<GuidUdi>().Select(x => x.Guid);
|
||||
var keysLookup = Database.FetchByGroups<NodeIdKey, Guid>(keys, Constants.Sql.MaxParameterCount, guids =>
|
||||
@@ -1106,15 +1110,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, guids);
|
||||
}).ToDictionary(x => x.UniqueId, x => x.NodeId);
|
||||
|
||||
// Lookup all relation type IDs
|
||||
var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty<int>()).ToDictionary(x => x.Alias, x => x.Id);
|
||||
|
||||
// Get all valid relations
|
||||
var relations = new List<ReadOnlyRelation>(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<IRelation>().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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -48,14 +48,16 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
||||
{
|
||||
foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value))
|
||||
// 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))
|
||||
{
|
||||
if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor))
|
||||
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, propertyValue.Value))
|
||||
// Use distinct values to avoid duplicate parsing of the same value
|
||||
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
|
||||
{
|
||||
yield return reference;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,16 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public abstract class ComplexPropertyEditorContentNotificationHandler :
|
||||
INotificationHandler<ContentSavingNotification>,
|
||||
INotificationHandler<ContentCopyingNotification>
|
||||
INotificationHandler<ContentCopyingNotification>,
|
||||
INotificationHandler<ContentScaffoldedNotification>
|
||||
{
|
||||
protected abstract string EditorAlias { get; }
|
||||
|
||||
@@ -31,6 +38,12 @@ public abstract class ComplexPropertyEditorContentNotificationHandler :
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(ContentScaffoldedNotification notification)
|
||||
{
|
||||
IEnumerable<IProperty> props = notification.Scaffold.GetPropertiesByEditor(EditorAlias);
|
||||
UpdatePropertyValues(props, false);
|
||||
}
|
||||
|
||||
protected abstract string FormatPropertyValue(string rawJson, bool onlyMissingKeys);
|
||||
|
||||
private void UpdatePropertyValues(IEnumerable<IProperty> props, bool onlyMissingKeys)
|
||||
|
||||
@@ -146,14 +146,16 @@ public class NestedContentPropertyEditor : DataEditor
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
||||
{
|
||||
foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value))
|
||||
// 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))
|
||||
{
|
||||
if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor))
|
||||
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, propertyValue.Value))
|
||||
// Use distinct values to avoid duplicate parsing of the same value
|
||||
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
|
||||
{
|
||||
yield return reference;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab
|
||||
IDomainCache domainCache,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
: base(previewDefault)
|
||||
: base(variationContextAccessor, previewDefault)
|
||||
{
|
||||
_snapshot = snapshot;
|
||||
_snapshotCache = snapshotCache;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -683,17 +684,33 @@ public class ContentController : ContentControllerBase
|
||||
[OutgoingEditorModelEvent]
|
||||
public ActionResult<ContentItemDisplay?> 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<ContentItemDisplay>(blueprint);
|
||||
|
||||
ContentItemDisplay? mapped = _umbracoMapper.Map<ContentItemDisplay>(scaffold);
|
||||
|
||||
if (mapped is not null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user