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/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs
index f5d72894ca..521626f666 100644
--- a/src/Umbraco.Web/Editors/ContentController.cs
+++ b/src/Umbraco.Web/Editors/ContentController.cs
@@ -869,7 +869,7 @@ namespace Umbraco.Web.Editors
return true;
}
-
+
///
/// Helper method to perform the saving of the content and add the notifications to the result
@@ -1161,14 +1161,14 @@ namespace Umbraco.Web.Editors
//validate if we can publish based on the mandatory language requirements
var canPublish = ValidatePublishingMandatoryLanguages(
cultureErrors,
- contentItem, cultureVariants, mandatoryCultures,
+ contentItem, cultureVariants, mandatoryCultures,
mandatoryVariant => mandatoryVariant.Publish);
//Now check if there are validation errors on each variant.
//If validation errors are detected on a variant and it's state is set to 'publish', then we
//need to change it to 'save'.
//It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages.
-
+
foreach (var variant in contentItem.Variants)
{
if (cultureErrors.Contains(variant.Culture))
@@ -1656,14 +1656,14 @@ namespace Umbraco.Web.Editors
[HttpPost]
public DomainSave PostSaveLanguageAndDomains(DomainSave model)
{
- foreach(var domain in model.Domains)
+ foreach (var domain in model.Domains)
{
try
{
var uri = DomainUtilities.ParseUriFromDomainName(domain.Name, Request.RequestUri);
}
catch (UriFormatException)
- {
+ {
var response = Request.CreateValidationErrorResponse(Services.TextService.Localize("assignDomain/invalidDomain"));
throw new HttpResponseException(response);
}
@@ -1829,7 +1829,7 @@ namespace Umbraco.Web.Editors
base.HandleInvalidModelState(display);
}
-
+
///
/// Maps the dto property values and names to the persisted model
///
@@ -1842,7 +1842,7 @@ namespace Umbraco.Web.Editors
var culture = property.PropertyType.VariesByCulture() ? variant.Culture : null;
var segment = property.PropertyType.VariesBySegment() ? variant.Segment : null;
return (culture, segment);
- }
+ }
var variantIndex = 0;
@@ -1884,15 +1884,15 @@ namespace Umbraco.Web.Editors
(save, property) =>
{
// Get property value
- (var culture, var segment) = PropertyCultureAndSegment(property, variant);
- return property.GetValue(culture, segment);
+ (var culture, var segment) = PropertyCultureAndSegment(property, variant);
+ return property.GetValue(culture, segment);
},
(save, property, v) =>
{
// Set property value
(var culture, var segment) = PropertyCultureAndSegment(property, variant);
- property.SetValue(v, culture, segment);
- },
+ property.SetValue(v, culture, segment);
+ },
variant.Culture);
variantIndex++;
@@ -2172,7 +2172,10 @@ namespace Umbraco.Web.Editors
///
private ContentItemDisplay MapToDisplay(IContent content)
{
- var display = Mapper.Map(content);
+ var display = Mapper.Map(content, context =>
+ {
+ context.Items["CurrentUser"] = Security.CurrentUser;
+ });
display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false;
return display;
}
diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs
index dc0df4ca96..d6def081e8 100644
--- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs
+++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs
@@ -5,6 +5,7 @@ using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
+using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Routing;
@@ -27,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;
@@ -34,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;
@@ -46,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();
@@ -80,7 +82,7 @@ namespace Umbraco.Web.Models.Mapping
target.Icon = source.ContentType.Icon;
target.Id = source.Id;
target.IsBlueprint = source.Blueprint;
- target.IsChildOfListView = DermineIsChildOfListView(source);
+ target.IsChildOfListView = DetermineIsChildOfListView(source, context);
target.IsContainer = source.ContentType.IsContainer;
target.IsElement = source.ContentType.IsElement;
target.Key = source.Key;
@@ -211,11 +213,63 @@ namespace Umbraco.Web.Models.Mapping
return source.CultureInfos.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"({source.Name})";
}
- private bool DermineIsChildOfListView(IContent source)
+ ///
+ /// Checks if the content item is a descendant of a list view
+ ///
+ ///
+ ///
+ ///
+ /// Returns true if the content item is a descendant of a list view and where the content is
+ /// not a current user's start node.
+ ///
+ ///
+ /// We must check if it's the current user's start node because in that case we will actually be
+ /// rendering the tree node underneath the list view to visually show context. In this case we return
+ /// false because the item is technically not being rendered as part of a list view but instead as a
+ /// real tree node. If we didn't perform this check then tree syncing wouldn't work correctly.
+ ///
+ private bool DetermineIsChildOfListView(IContent source, MapperContext context)
{
- // map the IsChildOfListView (this is actually if it is a descendant of a list view!)
+ 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)
+ {
+ 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;
+ }
+ }
+
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)
diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
index 1069df0ec4..6d156e3fc8 100644
--- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
+++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
@@ -364,7 +364,12 @@ namespace Umbraco.Web.Trees
var startNodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes);
//if any of these start nodes' parent is current, then we need to render children normally so we need to switch some logic and tell
// the UI that this node does have children and that it isn't a container
- if (startNodes.Any(x => x.ParentId == e.Id))
+
+ if (startNodes.Any(x =>
+ {
+ var pathParts = x.Path.Split(',');
+ return pathParts.Contains(e.Id.ToInvariantString());
+ }))
{
renderChildren = true;
}