diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 254e04d2d5..4020244733 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -27,6 +27,13 @@ namespace Umbraco.Core.Persistence.Repositories /// bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); + /// /// Returns true or false depending on whether content nodes have been created based on the provided content type id. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 6f714ff187..357798a8a9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1309,14 +1309,16 @@ WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", return test; } - /// - /// Given the path of a content item, this will return true if the content item exists underneath a list view content item - /// - /// - /// + /// public bool HasContainerInPath(string contentPath) { - var ids = contentPath.Split(',').Select(int.Parse); + var ids = contentPath.Split(',').Select(int.Parse).ToArray(); + return HasContainerInPath(ids); + } + + /// + public bool HasContainerInPath(params int[] ids) + { var sql = new Sql($@"SELECT COUNT(*) FROM cmsContentType INNER JOIN {Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Constants.DatabaseSchema.Tables.Content}.contentTypeId WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", new { ids, isContainer = true }); diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index 6ed3c85e91..82e5c6f171 100644 --- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs @@ -69,6 +69,13 @@ namespace Umbraco.Core.Services /// bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); + Attempt> CreateContainer(int parentContainerId, string name, int userId = Constants.Security.SuperUserId); Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId); EntityContainer GetContainer(int containerId); diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index da532e2765..fdd2d9ceae 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -321,6 +321,15 @@ namespace Umbraco.Core.Services.Implement } } + public bool HasContainerInPath(params int[] ids) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + // can use same repo for both content and media + return Repository.HasContainerInPath(ids); + } + } + public IEnumerable GetDescendants(int id, bool andSelf) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index be84b13a7e..d6def081e8 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -28,6 +28,7 @@ namespace Umbraco.Web.Models.Mapping private readonly ILocalizationService _localizationService; private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IEntityService _entityService; private readonly TabsAndPropertiesMapper _tabsAndPropertiesMapper; private readonly ContentSavedStateMapper _stateMapper; private readonly ContentBasicSavedStateMapper _basicStateMapper; @@ -35,7 +36,7 @@ namespace Umbraco.Web.Models.Mapping public ContentMapDefinition(CommonMapper commonMapper, ILocalizedTextService localizedTextService, IContentService contentService, IContentTypeService contentTypeService, IFileService fileService, IUmbracoContextAccessor umbracoContextAccessor, IPublishedRouter publishedRouter, ILocalizationService localizationService, ILogger logger, - IUserService userService) + IUserService userService, IEntityService entityService) { _commonMapper = commonMapper; _localizedTextService = localizedTextService; @@ -47,7 +48,7 @@ namespace Umbraco.Web.Models.Mapping _localizationService = localizationService; _logger = logger; _userService = userService; - + _entityService = entityService; _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(localizedTextService); _stateMapper = new ContentSavedStateMapper(); _basicStateMapper = new ContentBasicSavedStateMapper(); @@ -229,19 +230,46 @@ namespace Umbraco.Web.Models.Mapping /// private bool DetermineIsChildOfListView(IContent source, MapperContext context) { + var userStartNodes = Array.Empty(); + // In cases where a user's start node is below a list view, we will actually render // out the tree to that start node and in that case for that start node, we want to return // false here. if (context.HasItems && context.Items.TryGetValue("CurrentUser", out var usr) && usr is IUser currentUser) { - if (currentUser.StartContentIds.Contains(source.Id)) - return false; + userStartNodes = currentUser.CalculateContentStartNodeIds(_entityService); + if (!userStartNodes.Contains(Constants.System.Root)) + { + // return false if this is the user's actual start node, the node will be rendered in the tree + // regardless of if it's a list view or not + if (userStartNodes.Contains(source.Id)) + return false; + } } - - // map the IsChildOfListView (this is actually if it is a descendant of a list view!) var parent = _contentService.GetParent(source); - return parent != null && (parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(parent.Path)); + + if (parent == null) + return false; + + var pathParts = parent.Path.Split(',').Select(x => int.TryParse(x, out var i) ? i : 0).ToList(); + + // reduce the path parts so we exclude top level content items that + // are higher up than a user's start nodes + foreach (var n in userStartNodes) + { + var index = pathParts.IndexOf(n); + if (index != -1) + { + // now trim all top level start nodes to the found index + for (var i = 0; i < index; i++) + { + pathParts.RemoveAt(0); + } + } + } + + return parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(pathParts.ToArray()); } private DateTime? GetScheduledDate(IContent source, ContentScheduleAction action, MapperContext context)