Allow for filtering of document type allowed children and allowed at root when creating new content. (#18029)

* Creates IContentTypeFilterService with a "no-op" implementation for filtering the content types available for selection at root and as children.

* Rework to collection so packages and implementors can stack filters if they need to.
This commit is contained in:
Andy Butland
2025-01-20 12:26:07 +01:00
committed by GitHub
parent ef478cf5d1
commit 171ada26cc
9 changed files with 209 additions and 19 deletions

View File

@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Media.EmbedProviders;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.ServerEvents;
using Umbraco.Cms.Core.Services.Filters;
using Umbraco.Cms.Core.Snippets;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Webhooks;
@@ -92,6 +93,7 @@ public static partial class UmbracoBuilderExtensions
builder.SortHandlers().Add(() => builder.TypeLoader.GetTypes<ISortHandler>());
builder.ContentIndexHandlers().Add(() => builder.TypeLoader.GetTypes<IContentIndexHandler>());
builder.WebhookEvents().AddCms(true);
builder.ContentTypeFilters();
}
/// <summary>
@@ -246,4 +248,11 @@ public static partial class UmbracoBuilderExtensions
/// </summary>
public static ContentIndexHandlerCollectionBuilder ContentIndexHandlers(this IUmbracoBuilder builder)
=> builder.WithCollectionBuilder<ContentIndexHandlerCollectionBuilder>();
/// <summary>
/// Gets the content type filters collection builder.
/// </summary>
/// <param name="builder">The builder.</param>
public static ContentTypeFilterCollectionBuilder ContentTypeFilters(this IUmbracoBuilder builder)
=> builder.WithCollectionBuilder<ContentTypeFilterCollectionBuilder>();
}

View File

@@ -46,6 +46,7 @@ using Umbraco.Cms.Core.Telemetry;
using Umbraco.Cms.Core.Templates;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
using Umbraco.Cms.Core.Services.Filters;
namespace Umbraco.Cms.Core.DependencyInjection
{
@@ -444,7 +445,6 @@ namespace Umbraco.Cms.Core.DependencyInjection
// Routing
Services.AddUnique<IDocumentUrlService, DocumentUrlService>();
Services.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, DocumentUrlServiceInitializerNotificationHandler>();
}
}
}

View File

@@ -4,12 +4,11 @@ using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Services.Filters;
using Umbraco.Cms.Core.Services.Locking;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
@@ -28,7 +27,8 @@ public class ContentTypeService : ContentTypeServiceBase<IContentTypeRepository,
IDocumentTypeContainerRepository entityContainerRepository,
IEntityRepository entityRepository,
IEventAggregator eventAggregator,
IUserIdKeyResolver userIdKeyResolver)
IUserIdKeyResolver userIdKeyResolver,
ContentTypeFilterCollection contentTypeFilters)
: base(
provider,
loggerFactory,
@@ -38,7 +38,8 @@ public class ContentTypeService : ContentTypeServiceBase<IContentTypeRepository,
entityContainerRepository,
entityRepository,
eventAggregator,
userIdKeyResolver) =>
userIdKeyResolver,
contentTypeFilters) =>
ContentService = contentService;
[Obsolete("Use the ctor specifying all dependencies instead")]
@@ -65,6 +66,32 @@ public class ContentTypeService : ContentTypeServiceBase<IContentTypeRepository,
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>())
{ }
[Obsolete("Use the ctor specifying all dependencies instead")]
public ContentTypeService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IContentService contentService,
IContentTypeRepository repository,
IAuditRepository auditRepository,
IDocumentTypeContainerRepository entityContainerRepository,
IEntityRepository entityRepository,
IEventAggregator eventAggregator,
IUserIdKeyResolver userIdKeyResolver)
: this(
provider,
loggerFactory,
eventMessagesFactory,
contentService,
repository,
auditRepository,
entityContainerRepository,
entityRepository,
eventAggregator,
userIdKeyResolver,
StaticServiceProvider.Instance.GetRequiredService<ContentTypeFilterCollection>())
{ }
protected override int[] ReadLockIds => ContentTypeLocks.ReadLockIds;
protected override int[] WriteLockIds => ContentTypeLocks.WriteLockIds;

View File

@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Services.Filters;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -25,6 +26,7 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
private readonly IEntityRepository _entityRepository;
private readonly IEventAggregator _eventAggregator;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly ContentTypeFilterCollection _contentTypeFilters;
protected ContentTypeServiceBase(
ICoreScopeProvider provider,
@@ -35,7 +37,8 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
IEntityContainerRepository containerRepository,
IEntityRepository entityRepository,
IEventAggregator eventAggregator,
IUserIdKeyResolver userIdKeyResolver)
IUserIdKeyResolver userIdKeyResolver,
ContentTypeFilterCollection contentTypeFilters)
: base(provider, loggerFactory, eventMessagesFactory)
{
Repository = repository;
@@ -44,6 +47,7 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
_entityRepository = entityRepository;
_eventAggregator = eventAggregator;
_userIdKeyResolver = userIdKeyResolver;
_contentTypeFilters = contentTypeFilters;
}
[Obsolete("Use the ctor specifying all dependencies instead")]
@@ -69,6 +73,31 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
{
}
[Obsolete("Use the ctor specifying all dependencies instead")]
protected ContentTypeServiceBase(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
TRepository repository,
IAuditRepository auditRepository,
IEntityContainerRepository containerRepository,
IEntityRepository entityRepository,
IEventAggregator eventAggregator,
IUserIdKeyResolver userIdKeyResolver)
: this(
provider,
loggerFactory,
eventMessagesFactory,
repository,
auditRepository,
containerRepository,
entityRepository,
eventAggregator,
userIdKeyResolver,
StaticServiceProvider.Instance.GetRequiredService<ContentTypeFilterCollection>())
{
}
protected TRepository Repository { get; }
protected abstract int[] WriteLockIds { get; }
protected abstract int[] ReadLockIds { get; }
@@ -1129,7 +1158,7 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
#region Allowed types
/// <inheritdoc />
public Task<PagedModel<TItem>> GetAllAllowedAsRootAsync(int skip, int take)
public async Task<PagedModel<TItem>> GetAllAllowedAsRootAsync(int skip, int take)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
@@ -1139,28 +1168,39 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
IQuery<TItem> query = ScopeProvider.CreateQuery<TItem>().Where(x => x.AllowedAsRoot);
IEnumerable<TItem> contentTypes = Repository.Get(query).ToArray();
foreach (IContentTypeFilter filter in _contentTypeFilters)
{
contentTypes = await filter.FilterAllowedAtRootAsync(contentTypes);
}
var pagedModel = new PagedModel<TItem>
{
Total = contentTypes.Count(),
Items = contentTypes.Skip(skip).Take(take)
};
return Task.FromResult(pagedModel);
return pagedModel;
}
/// <inheritdoc />
public Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take)
public async Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
TItem? parent = Get(key);
if (parent?.AllowedContentTypes is null)
{
return Task.FromResult(Attempt.FailWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotFound, null));
return Attempt.FailWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotFound, null);
}
IEnumerable<ContentTypeSort> allowedContentTypes = parent.AllowedContentTypes;
foreach (IContentTypeFilter filter in _contentTypeFilters)
{
allowedContentTypes = await filter.FilterAllowedChildrenAsync(allowedContentTypes, key);
}
PagedModel<TItem> result;
if (parent.AllowedContentTypes.Any() is false)
if (allowedContentTypes.Any() is false)
{
// no content types allowed under parent
result = new PagedModel<TItem>
@@ -1173,7 +1213,7 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
{
// Get the sorted keys. Whilst we can't guarantee the order that comes back from GetMany, we can use
// this to sort the resulting list of allowed children.
Guid[] sortedKeys = parent.AllowedContentTypes.OrderBy(x => x.SortOrder).Select(x => x.Key).ToArray();
Guid[] sortedKeys = allowedContentTypes.OrderBy(x => x.SortOrder).Select(x => x.Key).ToArray();
TItem[] allowedChildren = GetMany(sortedKeys).ToArray();
result = new PagedModel<TItem>
@@ -1183,7 +1223,7 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
};
}
return Task.FromResult(Attempt.SucceedWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result));
return Attempt.SucceedWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result);
}
#endregion

View File

@@ -0,0 +1,18 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Services.Filters;
/// <summary>
/// Defines an ordered collection of <see cref="IContentTypeFilter"/>.
/// </summary>
public class ContentTypeFilterCollection : BuilderCollectionBase<IContentTypeFilter>
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeFilterCollection"/> class.
/// </summary>
/// <param name="items">The collection items.</param>
public ContentTypeFilterCollection(Func<IEnumerable<IContentTypeFilter>> items)
: base(items)
{
}
}

View File

@@ -0,0 +1,12 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Services.Filters;
/// <summary>
/// Builds an ordered collection of <see cref="IContentTypeFilter"/>.
/// </summary>
public class ContentTypeFilterCollectionBuilder : OrderedCollectionBuilderBase<ContentTypeFilterCollectionBuilder, ContentTypeFilterCollection, IContentTypeFilter>
{
/// <inheritdoc/>
protected override ContentTypeFilterCollectionBuilder This => this;
}

View File

@@ -0,0 +1,25 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services.Filters;
/// <summary>
/// Defines methods for filtering content types after retrieval from the database.
/// </summary>
public interface IContentTypeFilter
{
/// <summary>
/// Filters the content types retrieved for being allowed at the root.
/// </summary>
/// <param name="contentTypes">Retrieved collection of content types.</param>
/// <returns>Filtered collection of content types.</returns>
Task<IEnumerable<TItem>> FilterAllowedAtRootAsync<TItem>(IEnumerable<TItem> contentTypes)
where TItem : IContentTypeComposition;
/// <summary>
/// Filters the content types retrieved for being allowed as children of a parent content type.
/// </summary>
/// <param name="contentTypes">Retrieved collection of content types.</param>
/// <param name="parentKey">The parent content type key.</param>
/// <returns>Filtered collection of content types.</returns>
Task<IEnumerable<ContentTypeSort>> FilterAllowedChildrenAsync(IEnumerable<ContentTypeSort> contentTypes, Guid parentKey);
}

View File

@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Services.Filters;
using Umbraco.Cms.Core.Services.Locking;
using Umbraco.Extensions;
@@ -24,7 +25,8 @@ public class MediaTypeService : ContentTypeServiceBase<IMediaTypeRepository, IMe
IMediaTypeContainerRepository entityContainerRepository,
IEntityRepository entityRepository,
IEventAggregator eventAggregator,
IUserIdKeyResolver userIdKeyResolver)
IUserIdKeyResolver userIdKeyResolver,
ContentTypeFilterCollection contentTypeFilters)
: base(
provider,
loggerFactory,
@@ -34,7 +36,8 @@ public class MediaTypeService : ContentTypeServiceBase<IMediaTypeRepository, IMe
entityContainerRepository,
entityRepository,
eventAggregator,
userIdKeyResolver) => MediaService = mediaService;
userIdKeyResolver,
contentTypeFilters) => MediaService = mediaService;
[Obsolete("Use the constructor with all dependencies instead")]
public MediaTypeService(
@@ -61,6 +64,32 @@ public class MediaTypeService : ContentTypeServiceBase<IMediaTypeRepository, IMe
{
}
[Obsolete("Use the constructor with all dependencies instead")]
public MediaTypeService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IMediaService mediaService,
IMediaTypeRepository mediaTypeRepository,
IAuditRepository auditRepository,
IMediaTypeContainerRepository entityContainerRepository,
IEntityRepository entityRepository,
IEventAggregator eventAggregator,
IUserIdKeyResolver userIdKeyResolver)
: this(
provider,
loggerFactory,
eventMessagesFactory,
mediaService,
mediaTypeRepository,
auditRepository,
entityContainerRepository,
entityRepository,
eventAggregator,
userIdKeyResolver,
StaticServiceProvider.Instance.GetRequiredService<ContentTypeFilterCollection>())
{
}
protected override int[] ReadLockIds => MediaTypeLocks.ReadLockIds;

View File

@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Services.Filters;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services;
@@ -15,6 +16,34 @@ public class MemberTypeService : ContentTypeServiceBase<IMemberTypeRepository, I
{
private readonly IMemberTypeRepository _memberTypeRepository;
public MemberTypeService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IMemberService memberService,
IMemberTypeRepository memberTypeRepository,
IAuditRepository auditRepository,
IMemberTypeContainerRepository entityContainerRepository,
IEntityRepository entityRepository,
IEventAggregator eventAggregator,
IUserIdKeyResolver userIdKeyResolver,
ContentTypeFilterCollection contentTypeFilters)
: base(
provider,
loggerFactory,
eventMessagesFactory,
memberTypeRepository,
auditRepository,
entityContainerRepository,
entityRepository,
eventAggregator,
userIdKeyResolver,
contentTypeFilters)
{
MemberService = memberService;
_memberTypeRepository = memberTypeRepository;
}
[Obsolete("Please use the constructor taking all parameters. This constructor will be removed in V16.")]
public MemberTypeService(
ICoreScopeProvider provider,
@@ -40,6 +69,7 @@ public class MemberTypeService : ContentTypeServiceBase<IMemberTypeRepository, I
{
}
[Obsolete("Please use the constructor taking all parameters. This constructor will be removed in V16.")]
public MemberTypeService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
@@ -51,19 +81,19 @@ public class MemberTypeService : ContentTypeServiceBase<IMemberTypeRepository, I
IEntityRepository entityRepository,
IEventAggregator eventAggregator,
IUserIdKeyResolver userIdKeyResolver)
: base(
: this(
provider,
loggerFactory,
eventMessagesFactory,
memberService,
memberTypeRepository,
auditRepository,
entityContainerRepository,
entityRepository,
eventAggregator,
userIdKeyResolver)
userIdKeyResolver,
StaticServiceProvider.Instance.GetRequiredService<ContentTypeFilterCollection>())
{
MemberService = memberService;
_memberTypeRepository = memberTypeRepository;
}
// beware! order is important to avoid deadlocks