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)