diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index a9f5dcf352..4c2d72fb8c 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2019")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] 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.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs index 254e04d2d5..4020244733 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/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.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index b716a121be..16b9d852fd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1312,14 +1312,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.Infrastructure/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Infrastructure/PropertyEditors/DataValueReferenceFactoryCollection.cs index 83f5badb9c..2737dcfef1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -16,35 +16,46 @@ namespace Umbraco.Core.PropertyEditors public IEnumerable GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors) { - var trackedRelations = new List(); + var trackedRelations = new HashSet(); foreach (var p in properties) { if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; - //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here - if (!p.PropertyType.VariesByNothing()) continue; - var val = p.GetValue(); // get the invariant value + //TODO: We will need to change this once we support tracking via variants/segments + // for now, we are tracking values from ALL variants - var valueEditor = editor.GetValueEditor(); - if (valueEditor is IDataValueReference reference) + foreach(var propertyVal in p.Values) { - var refs = reference.GetReferences(val); - trackedRelations.AddRange(refs); + var val = propertyVal.EditedValue; + + var valueEditor = editor.GetValueEditor(); + if (valueEditor is IDataValueReference reference) + { + var refs = reference.GetReferences(val); + foreach(var r in refs) + trackedRelations.Add(r); } - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (var item in this) - { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has referecnes to media/content items - if (item.IsForEditor(editor)) - trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (var item in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media/content items + if (item.IsForEditor(editor)) + { + foreach(var r in item.GetDataValueReference().GetReferences(val)) + trackedRelations.Add(r); + } + + } } + + } return trackedRelations; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index 4608b5c5da..4fdc13baa9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -1,12 +1,23 @@ using System; +using HeyRed.MarkdownSharp; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Strings; +using Umbraco.Web.Templates; namespace Umbraco.Core.PropertyEditors.ValueConverters { [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { + private readonly HtmlLocalLinkParser _localLinkParser; + private readonly HtmlUrlParser _urlParser; + + public MarkdownEditorValueConverter(HtmlLocalLinkParser localLinkParser, HtmlUrlParser urlParser) + { + _localLinkParser = localLinkParser; + _urlParser = urlParser; + } + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MarkdownEditor.Equals(propertyType.EditorAlias); @@ -15,20 +26,26 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + => PropertyCacheLevel.Snapshot; public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { - // in xml a string is: string - // in the database a string is: string - // default value is: null - return source; + if (source == null) return null; + var sourceString = source.ToString(); + + // ensures string is parsed for {localLink} and urls are resolved correctly + sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _urlParser.EnsureUrls(sourceString); + + return sourceString; } public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { + // convert markup to HTML for frontend rendering. // source should come from ConvertSource and be a string (or null) already - return new HtmlEncodedString(inter == null ? string.Empty : (string) inter); + var mark = new Markdown(); + return new HtmlEncodedString(inter == null ? string.Empty : mark.Transform((string)inter)); } public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 50f12ba73e..7e39894aa3 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -318,6 +318,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.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs new file mode 100644 index 0000000000..24ac9cdbf4 --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -0,0 +1,255 @@ +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.PropertyEditors; +using static Umbraco.Core.Models.Property; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class DataValueReferenceFactoryCollectionTests + { + IDataTypeService DataTypeService { get; } = Mock.Of(); + private IIOHelper IOHelper { get; } = TestHelper.IOHelper; + ILocalizedTextService LocalizedTextService { get; } = Mock.Of(); + ILocalizationService LocalizationService { get; } = Mock.Of(); + IShortStringHelper ShortStringHelper { get; } = Mock.Of(); + + [Test] + public void GetAllReferences_All_Variants_With_IDataValueReferenceFactory() + { + var collection = new DataValueReferenceFactoryCollection(new TestDataValueReferenceFactory().Yield()); + + + // label does not implement IDataValueReference + var labelEditor = new LabelPropertyEditor( + Mock.Of(), + IOHelper, + DataTypeService, + LocalizedTextService, + LocalizationService, + ShortStringHelper + ); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(labelEditor.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(ShortStringHelper, new DataType(labelEditor)) + { + Variations = ContentVariation.CultureAndSegment + }) + { + Values = new List + { + // Ignored (no culture) + new PropertyValue + { + EditedValue = trackedUdi1 + }, + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + // Ignored (no culture) + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Culture = "en-US", + Segment = "B", + EditedValue = trackedUdi3 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi2, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi3, result.ElementAt(1).Udi.ToString()); + } + + [Test] + public void GetAllReferences_All_Variants_With_IDataValueReference_Editor() + { + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty()); + + // mediaPicker does implement IDataValueReference + var mediaPicker = new MediaPickerPropertyEditor( + Mock.Of(), + DataTypeService, + LocalizationService, + IOHelper, + ShortStringHelper, + LocalizedTextService + ); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(mediaPicker.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(ShortStringHelper, new DataType(mediaPicker)) + { + Variations = ContentVariation.CultureAndSegment + }) + { + Values = new List + { + // Ignored (no culture) + new PropertyValue + { + EditedValue = trackedUdi1 + }, + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + // Ignored (no culture) + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Culture = "en-US", + Segment = "B", + EditedValue = trackedUdi3 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi2, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi3, result.ElementAt(1).Udi.ToString()); + } + + [Test] + public void GetAllReferences_Invariant_With_IDataValueReference_Editor() + { + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty()); + + // mediaPicker does implement IDataValueReference + var mediaPicker = new MediaPickerPropertyEditor( + Mock.Of(), + DataTypeService, + LocalizationService, + IOHelper, + ShortStringHelper, + LocalizedTextService + ); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(mediaPicker.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(ShortStringHelper, new DataType(mediaPicker)) + { + Variations = ContentVariation.Nothing | ContentVariation.Segment + }) + { + Values = new List + { + new PropertyValue + { + EditedValue = trackedUdi1 + }, + // Ignored (has culture) + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + // Ignored (has culture) + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Segment = "B", + EditedValue = trackedUdi4 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi1, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi4, result.ElementAt(1).Udi.ToString()); + } + + private class TestDataValueReferenceFactory : IDataValueReferenceFactory + { + public IDataValueReference GetDataValueReference() => new TestMediaDataValueReference(); + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias == Constants.PropertyEditors.Aliases.Label; + + private class TestMediaDataValueReference : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + // This is the same as the media picker, it will just try to parse the value directly as a UDI + + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (UdiParser.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } + } + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 9c8bba7631..9815c94728 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -149,6 +149,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg b/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg index 6c0515906f..75bf0d52af 100644 Binary files a/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg and b/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg differ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html index 93801f14b8..d4e75908bd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html @@ -9,6 +9,7 @@ ng-model="vm.model" ng-change="vm.onChange()" ng-keydown="vm.onKeyDown($event)" + ng-blur="vm.onBlur($event)" prevent-enter-submit no-dirty-check> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js index 994129708f..d7aee744e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js @@ -10,7 +10,8 @@ bindings: { model: "=", onStartTyping: "&?", - onSearch: "&?" + onSearch: "&?", + onBlur: "&?" } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 2e4313ec76..172f9b2249 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -147,7 +147,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en _.each($scope.model.value, function (item){ // we must reload the "document" link URLs to match the current editor culture - if (item.udi.indexOf("/document/") > 0) { + if (item.udi && item.udi.indexOf("/document/") > 0) { item.url = null; entityResource.getUrlByUdi(item.udi).then(function (data) { item.url = data; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 935b1bdfb1..f3c28fdb9d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -14,7 +14,7 @@ vm.userStates = []; vm.selection = []; vm.newUser = {}; - vm.usersOptions = {filter:null}; + vm.usersOptions = {}; vm.userSortData = [ { label: "Name (A-Z)", key: "Name", direction: "Ascending" }, { label: "Name (Z-A)", key: "Name", direction: "Descending" }, @@ -112,6 +112,7 @@ vm.selectAll = selectAll; vm.areAllSelected = areAllSelected; vm.searchUsers = searchUsers; + vm.onBlurSearch = onBlurSearch; vm.getFilterName = getFilterName; vm.setUserStatesFilter = setUserStatesFilter; vm.setUserGroupFilter = setUserGroupFilter; @@ -150,10 +151,12 @@ function initViewOptions() { // Start with default view options. + vm.usersOptions.filter = ""; vm.usersOptions.orderBy = "Name"; vm.usersOptions.orderDirection = "Ascending"; // Update from querystring if available. + initViewOptionFromQueryString("filter"); initViewOptionFromQueryString("orderBy"); initViewOptionFromQueryString("orderDirection"); initViewOptionFromQueryString("pageNumber"); @@ -451,7 +454,8 @@ var search = _.debounce(function () { $scope.$apply(function () { - changePageNumber(1); + vm.usersOptions.pageNumber = 1; + getUsers(); }); }, 500); @@ -459,6 +463,10 @@ search(); } + function onBlurSearch() { + updateLocation("filter", vm.usersOptions.filter); + } + function getFilterName(array) { var name = vm.labels.all; var found = false; @@ -547,6 +555,7 @@ } function updateLocation(key, value) { + $location.search("filter", vm.usersOptions.filter);// update filter, but first when something else requests a url update. $location.search(key, value); } @@ -657,7 +666,8 @@ function usersOptionsAsQueryString() { var qs = "?orderBy=" + vm.usersOptions.orderBy + "&orderDirection=" + vm.usersOptions.orderDirection + - "&pageNumber=" + vm.usersOptions.pageNumber; + "&pageNumber=" + vm.usersOptions.pageNumber + + "&filter=" + vm.usersOptions.filter; qs += addUsersOptionsFilterCollectionToQueryString("userStates", vm.usersOptions.userStates); qs += addUsersOptionsFilterCollectionToQueryString("userGroups", vm.usersOptions.userGroups); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 638e6376c3..bb53413060 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 12ff3952fd..0b63c94ba8 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1671,7 +1671,7 @@ namespace Umbraco.Web.Editors [HttpPost] public DomainSave PostSaveLanguageAndDomains(DomainSave model) { - foreach(var domain in model.Domains) + foreach (var domain in model.Domains) { try { @@ -2188,7 +2188,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 d418bf153d..36c117197b 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Routing; @@ -30,6 +31,7 @@ namespace Umbraco.Web.Models.Mapping private readonly ILocalizationService _localizationService; private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IEntityService _entityService; private readonly IVariationContextAccessor _variationContextAccessor; private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly UriUtility _uriUtility; @@ -38,9 +40,10 @@ namespace Umbraco.Web.Models.Mapping private readonly ContentBasicSavedStateMapper _basicStateMapper; private readonly ContentVariantMapper _contentVariantMapper; + public ContentMapDefinition(CommonMapper commonMapper, ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IContentService contentService, IContentTypeService contentTypeService, IFileService fileService, IUmbracoContextAccessor umbracoContextAccessor, IPublishedRouter publishedRouter, ILocalizationService localizationService, ILogger logger, - IUserService userService, IVariationContextAccessor variationContextAccessor, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, UriUtility uriUtility, IPublishedUrlProvider publishedUrlProvider) + IUserService userService, IVariationContextAccessor variationContextAccessor, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, UriUtility uriUtility, IPublishedUrlProvider publishedUrlProvider, IEntityService entityService) { _commonMapper = commonMapper; _cultureDictionary = cultureDictionary; @@ -53,6 +56,7 @@ namespace Umbraco.Web.Models.Mapping _localizationService = localizationService; _logger = logger; _userService = userService; + _entityService = entityService; _variationContextAccessor = variationContextAccessor; _uriUtility = uriUtility; _publishedUrlProvider = publishedUrlProvider; @@ -90,7 +94,7 @@ namespace Umbraco.Web.Models.Mapping target.Icon = source.ContentType.Icon; target.Id = source.Id; target.IsBlueprint = source.Blueprint; - target.IsChildOfListView = DetermineIsChildOfListView(source); + target.IsChildOfListView = DetermineIsChildOfListView(source, context); target.IsContainer = source.ContentType.IsContainer; target.IsElement = source.ContentType.IsElement; target.Key = source.Key; @@ -221,13 +225,66 @@ namespace Umbraco.Web.Models.Mapping return source.CultureInfos.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"({source.Name})"; } - private bool DetermineIsChildOfListView(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) { var culture = context.GetCulture() ?? string.Empty; diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 7646a71e7d..4549f47a2f 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -381,7 +381,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; }