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:
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
25
src/Umbraco.Core/Services/Filters/IContentTypeFilter.cs
Normal file
25
src/Umbraco.Core/Services/Filters/IContentTypeFilter.cs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user