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; }