From 45db8fd7c41ff1c24070a0d1b458c0359cc7df15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Thu, 29 Aug 2019 11:07:00 +0200 Subject: [PATCH 01/63] Add ISimpleContentType.VariesBySegment() --- src/Umbraco.Core/ContentVariationExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 5b157307ab..fe5a82047a 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -19,6 +19,11 @@ namespace Umbraco.Core /// public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); + /// + /// Determines whether the content type varies by segment. + /// + public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); + /// /// Determines whether the content type is invariant. /// From 74727a179cddcc64277d6db8faf99bc68c52f795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Thu, 29 Aug 2019 12:05:27 +0200 Subject: [PATCH 02/63] Adds Segment to ContentItemDisplay and update Variants to include segmented variants. --- .../ContentEditing/ContentPropertyBasic.cs | 12 +++- .../Mapping/ContentPropertyBasicMapper.cs | 8 ++- .../Models/Mapping/ContentVariantMapper.cs | 65 ++++++++++++++++--- .../Models/Mapping/MapperContextExtensions.cs | 19 +++++- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs index c5c22484ad..2b70a63035 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs @@ -53,11 +53,21 @@ namespace Umbraco.Web.Models.ContentEditing [ReadOnly(true)] public string Culture { get; set; } + /// + /// The segment of the property + /// + /// + /// The segment value of a property can always be null but can only have a non-null value + /// when the property can be varied by segment. + /// + [DataMember(Name = "segment")] + [ReadOnly(true)] + public string Segment { get; set; } + /// /// Used internally during model mapping /// [IgnoreDataMember] internal IDataEditor PropertyEditor { get; set; } - } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs index 36c1b360b2..aacafd2f2a 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs @@ -70,8 +70,14 @@ namespace Umbraco.Web.Models.Mapping dest.Culture = culture; + var segment = context.GetSegment(); + + // The current segment can always be null, even if the propertyType *can* be varied by segment + // so the nullcheck like with culture is not performed here. + dest.Segment = segment; + // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. - dest.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture); + dest.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture, segment); } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index c279ae2c70..848eaa0594 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -21,27 +21,35 @@ namespace Umbraco.Web.Models.Mapping public IEnumerable Map(IContent source, MapperContext context) { - var result = new List(); - if (!source.ContentType.VariesByCulture()) + var variants = new List(); + + var variesByCulture = source.ContentType.VariesByCulture(); + var variesBySegment = source.ContentType.VariesBySegment(); + + if (!variesByCulture && !variesBySegment) { //this is invariant so just map the IContent instance to ContentVariationDisplay - result.Add(context.Map(source)); + variants.Add(context.Map(source)); + return variants; } - else + + if (variesByCulture && !variesBySegment) { + // Culture only + var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); if (allLanguages.Count == 0) return Enumerable.Empty(); //this should never happen var langs = context.MapEnumerable(allLanguages).ToList(); //create a variant for each language, then we'll populate the values - var variants = langs.Select(x => + variants.AddRange(langs.Select(x => { //We need to set the culture in the mapping context since this is needed to ensure that the correct property values //are resolved during the mapping context.SetCulture(x.IsoCode); return context.Map(source); - }).ToList(); + })); for (int i = 0; i < langs.Count; i++) { @@ -63,10 +71,49 @@ namespace Umbraco.Web.Models.Mapping //Insert the default language as the first item variants.Insert(0, defaultLang); - - return variants; } - return result; + else if (variesBySegment && !variesByCulture) + { + // Segment only + throw new NotSupportedException("ContentVariantMapper not implemented for segment only!"); + } + else + { + // Culture and segment + + var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); + if (allLanguages.Count == 0) return Enumerable.Empty(); //this should never happen + + var langs = context.MapEnumerable(allLanguages).ToList(); + + // All segments, including the unsegmented (= NULL) segment. + // TODO: The NULl segment might have to be changed to be empty string? + var segments = source.Properties + .SelectMany(p => p.Values.Select(v => v.Segment)) + .Distinct(); + + // Add all variants + foreach (var language in langs) + { + foreach (var segment in segments) + { + context.SetCulture(language.IsoCode); + context.SetSegment(segment); + + var variantDisplay = context.Map(source); + + variantDisplay.Language = language; + variantDisplay.Segment = segment; + variantDisplay.Name = source.GetCultureName(language.IsoCode); + + variants.Add(variantDisplay); + } + } + + // TODO: Sorting + } + + return variants; } } } diff --git a/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs b/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs index 1538f1a987..20a387c679 100644 --- a/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/MapperContextExtensions.cs @@ -8,6 +8,7 @@ namespace Umbraco.Web.Models.Mapping internal static class MapperContextExtensions { private const string CultureKey = "Map.Culture"; + private const string SegmentKey = "Map.Segment"; private const string IncludedPropertiesKey = "Map.IncludedProperties"; /// @@ -18,6 +19,14 @@ namespace Umbraco.Web.Models.Mapping return context.HasItems && context.Items.TryGetValue(CultureKey, out var obj) && obj is string s ? s : null; } + /// + /// Gets the context segment. + /// + public static string GetSegment(this MapperContext context) + { + return context.HasItems && context.Items.TryGetValue(SegmentKey, out var obj) && obj is string s ? s : null; + } + /// /// Sets a context culture. /// @@ -26,6 +35,14 @@ namespace Umbraco.Web.Models.Mapping context.Items[CultureKey] = culture; } + /// + /// Sets a context segment. + /// + public static void SetSegment(this MapperContext context, string segment) + { + context.Items[SegmentKey] = segment; + } + /// /// Get included properties. /// @@ -42,4 +59,4 @@ namespace Umbraco.Web.Models.Mapping context.Items[IncludedPropertiesKey] = properties; } } -} \ No newline at end of file +} From 8f0ee6d71154bad2f389bc5aa6a04f19ad183045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Thu, 29 Aug 2019 14:38:46 +0200 Subject: [PATCH 03/63] Added support for all 4 cases in ContentVariantMapper --- .../Models/Mapping/ContentVariantMapper.cs | 155 ++++++++++-------- 1 file changed, 83 insertions(+), 72 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index 848eaa0594..d16f244a0a 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -21,99 +21,110 @@ namespace Umbraco.Web.Models.Mapping public IEnumerable Map(IContent source, MapperContext context) { - var variants = new List(); - var variesByCulture = source.ContentType.VariesByCulture(); var variesBySegment = source.ContentType.VariesBySegment(); + IList variants = new List(); + if (!variesByCulture && !variesBySegment) { - //this is invariant so just map the IContent instance to ContentVariationDisplay - variants.Add(context.Map(source)); - return variants; + // this is invariant so just map the IContent instance to ContentVariationDisplay + var variantDisplay = context.Map(source); + variants.Add(variantDisplay); } - - if (variesByCulture && !variesBySegment) + else if (variesByCulture && !variesBySegment) { - // Culture only - - var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (allLanguages.Count == 0) return Enumerable.Empty(); //this should never happen - - var langs = context.MapEnumerable(allLanguages).ToList(); - - //create a variant for each language, then we'll populate the values - variants.AddRange(langs.Select(x => - { - //We need to set the culture in the mapping context since this is needed to ensure that the correct property values - //are resolved during the mapping - context.SetCulture(x.IsoCode); - return context.Map(source); - })); - - for (int i = 0; i < langs.Count; i++) - { - var x = langs[i]; - var variant = variants[i]; - - variant.Language = x; - variant.Name = source.GetCultureName(x.IsoCode); - } - - //Put the default language first in the list & then sort rest by a-z - var defaultLang = variants.SingleOrDefault(x => x.Language.IsDefault); - - //Remove the default language from the list for now - variants.Remove(defaultLang); - - //Sort the remaining languages a-z - variants = variants.OrderBy(x => x.Language.Name).ToList(); - - //Insert the default language as the first item - variants.Insert(0, defaultLang); + var languages = GetLanguages(context); + variants = languages + .Select(language => CreateVariantDisplay(context, source, language, null)) + .ToList(); } else if (variesBySegment && !variesByCulture) { // Segment only - throw new NotSupportedException("ContentVariantMapper not implemented for segment only!"); + var segments = GetSegments(source); + variants = segments + .Select(segment => CreateVariantDisplay(context, source, null, segment)) + .ToList(); } else { // Culture and segment + var languages = GetLanguages(context).ToList(); + var segments = GetSegments(source).ToList(); - var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (allLanguages.Count == 0) return Enumerable.Empty(); //this should never happen - - var langs = context.MapEnumerable(allLanguages).ToList(); - - // All segments, including the unsegmented (= NULL) segment. - // TODO: The NULl segment might have to be changed to be empty string? - var segments = source.Properties - .SelectMany(p => p.Values.Select(v => v.Segment)) - .Distinct(); - - // Add all variants - foreach (var language in langs) + if (languages.Count == 0 || segments.Count == 0) { - foreach (var segment in segments) - { - context.SetCulture(language.IsoCode); - context.SetSegment(segment); - - var variantDisplay = context.Map(source); - - variantDisplay.Language = language; - variantDisplay.Segment = segment; - variantDisplay.Name = source.GetCultureName(language.IsoCode); - - variants.Add(variantDisplay); - } + // This should not happen + throw new ArgumentException("No languages or segments available"); } - // TODO: Sorting + variants = languages + .SelectMany(language => segments + .Select(segment => CreateVariantDisplay(context, source, language, segment))) + .ToList(); } - return variants; + return SortVariants(variants); + } + + private IList SortVariants(IList variants) + { + if (variants == null || variants.Count <= 1) + { + return variants; + } + + // Default variant first, then order by language, segment. + return variants + .OrderBy(v => IsDefaultLanguage(v) ? 0 : 1) + .ThenBy(v => IsDefaultSegment(v) ? 0 : 1) + .ThenBy(v => v?.Language?.Name) + .ThenBy(v => v.Segment) + .ToList(); + } + + private static bool IsDefaultSegment(ContentVariantDisplay variant) + { + return variant.Segment == null; + } + + private static bool IsDefaultLanguage(ContentVariantDisplay variant) + { + return variant.Language == null || variant.Language.IsDefault; + } + + private IEnumerable GetLanguages(MapperContext context) + { + var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); + if (allLanguages.Count == 0) + { + // This should never happen + return Enumerable.Empty(); + } + else + { + return context.MapEnumerable(allLanguages).ToList(); + } + } + + private IEnumerable GetSegments(IContent content) + { + return content.Properties.SelectMany(p => p.Values.Select(v => v.Segment)).Distinct(); + } + + private ContentVariantDisplay CreateVariantDisplay(MapperContext context, IContent content, Language language, string segment) + { + context.SetCulture(language?.IsoCode); + context.SetSegment(segment); + + var variantDisplay = context.Map(content); + + variantDisplay.Segment = segment; + variantDisplay.Language = language; + variantDisplay.Name = content.GetCultureName(language?.IsoCode); + + return variantDisplay; } } } From 813998a614c89dbcd9d11e6019aad535cd18009e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Fri, 11 Oct 2019 14:13:49 +0200 Subject: [PATCH 04/63] Support for saving edited segment values --- .../Editors/Binders/ContentItemBinder.cs | 9 +++++--- src/Umbraco.Web/Editors/ContentController.cs | 23 +++++++++++++++---- .../ContentEditing/ContentVariantSave.cs | 6 +++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs index 11a876345d..d9d56c584f 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web.Editors.Binders { foreach (var variant in model.Variants) { - if (variant.Culture.IsNullOrWhiteSpace()) + if (variant.Culture.IsNullOrWhiteSpace() && variant.Segment.IsNullOrWhiteSpace()) { //map the property dto collection (no culture is passed to the mapping context so it will be invariant) variant.PropertyCollectionDto = Current.Mapper.Map(model.PersistedContent); @@ -59,7 +59,11 @@ namespace Umbraco.Web.Editors.Binders //map the property dto collection with the culture of the current variant variant.PropertyCollectionDto = Current.Mapper.Map( model.PersistedContent, - context => context.SetCulture(variant.Culture)); + context => + { + context.SetCulture(variant.Culture); + context.SetSegment(variant.Segment); + }); } //now map all of the saved values to the dto @@ -87,6 +91,5 @@ namespace Umbraco.Web.Editors.Binders model.ParentId, contentType); } - } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 9d5af028e3..bc2b55833e 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1836,8 +1836,13 @@ namespace Umbraco.Web.Editors /// private void MapValuesForPersistence(ContentItemSave contentSave) { - // inline method to determine if a property type varies - bool Varies(Property property) => property.PropertyType.VariesByCulture(); + // inline method to determine the culture and segment to persist the property + (string culture, string segment) PropertyCultureAndSegment(Property property, ContentVariantSave variant) + { + var culture = property.PropertyType.VariesByCulture() ? variant.Culture : null; + var segment = property.PropertyType.VariesBySegment() ? variant.Segment : null; + return (culture, segment); + } var variantIndex = 0; @@ -1876,8 +1881,18 @@ namespace Umbraco.Web.Editors MapPropertyValuesForPersistence( contentSave, propertyCollection, - (save, property) => Varies(property) ? property.GetValue(variant.Culture) : property.GetValue(), //get prop val - (save, property, v) => { if (Varies(property)) property.SetValue(v, variant.Culture); else property.SetValue(v); }, //set prop val + (save, property) => + { + // Get property value + (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); + }, variant.Culture); variantIndex++; diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs index deadac949a..9a7555ad92 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariantSave.cs @@ -29,6 +29,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "culture")] public string Culture { get; set; } + /// + /// The segment of this variant, if this is invariant than this is null or empty + /// + [DataMember(Name = "segment")] + public string Segment { get; set; } + /// /// Indicates if the variant should be updated /// From 1da29b29b90145c389e9b1569386d62e743ddcb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Fri, 30 Aug 2019 11:37:44 +0200 Subject: [PATCH 05/63] Only set segment on property when property can be varied by segment --- .../Models/Mapping/ContentPropertyBasicMapper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs index aacafd2f2a..cf1bc3c253 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs @@ -70,10 +70,9 @@ namespace Umbraco.Web.Models.Mapping dest.Culture = culture; - var segment = context.GetSegment(); - - // The current segment can always be null, even if the propertyType *can* be varied by segment - // so the nullcheck like with culture is not performed here. + // Get the segment, which is always allowed to be null even if the propertyType *can* be varied by segment. + // There is therefore no need to perform the null check like with culture above. + var segment = !property.PropertyType.VariesBySegment() ? null : context.GetSegment(); dest.Segment = segment; // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. From f2d7a25e0f7b89e43df9a2999fcd868c196c6fd3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 31 Oct 2019 15:52:48 +0100 Subject: [PATCH 06/63] Make macro partial view mandatory --- src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html b/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html index d34cf1810d..7706734327 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html @@ -11,7 +11,7 @@ - + + Date: Thu, 31 Oct 2019 20:03:56 +0100 Subject: [PATCH 07/63] Angular API documentation: Adds styling for elements --- src/Umbraco.Web.UI.Docs/umb-docs.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.Web.UI.Docs/umb-docs.css b/src/Umbraco.Web.UI.Docs/umb-docs.css index eef0fcee39..931c22b255 100644 --- a/src/Umbraco.Web.UI.Docs/umb-docs.css +++ b/src/Umbraco.Web.UI.Docs/umb-docs.css @@ -76,3 +76,12 @@ a:hover { width: 50px; margin-top: 5px; } + +.content .methods code { + border: 1px solid #e1e1e8; + background-color: #f7f7f9; + padding: 0px 3px; + font-size: 85%; + color: #d14; + white-space: nowrap; +} From b0d834668d0631d14bd580ae615e08616ed33f07 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 1 Nov 2019 10:49:45 +0100 Subject: [PATCH 08/63] Fix search not skipping results --- src/Umbraco.Web/PublishedContentQuery.cs | 57 +++++++++++++----------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 2dbe4de4c5..72aec94e58 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -191,46 +191,52 @@ namespace Umbraco.Web /// public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null) { - indexName = string.IsNullOrEmpty(indexName) - ? Constants.UmbracoIndexes.ExternalIndexName - : indexName; + if (string.IsNullOrEmpty(indexName)) + { + indexName = Constants.UmbracoIndexes.ExternalIndexName; + } if (!_examineManager.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) + { throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); + } var searcher = umbIndex.GetSearcher(); - // default to max 500 results - var count = skip == 0 && take == 0 ? 500 : skip + take; - ISearchResults results; if (culture == "*") { - //search everything - - results = searcher.Search(term, count); - } - else if (culture.IsNullOrWhiteSpace()) - { - //only search invariant - - var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "n"); //must not vary by culture - qry = qry.And().ManagedQuery(term); - results = qry.Execute(count); + // Search everything + results = skip == 0 && take == 0 + ? searcher.Search(term) + : searcher.Search(term, skip + take); } else { - //search only the specified culture + IBooleanOperation query; + if (string.IsNullOrWhiteSpace(culture)) + { + // Only search invariant + query = searcher.CreateQuery() + .Field(UmbracoContentIndex.VariesByCultureFieldName, "n") // Must not vary by culture + .And().ManagedQuery(term); + } + else + { + // Only search the specified culture + var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied + query = searcher.CreateQuery() + .ManagedQuery(term, fields); + } - //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery().ManagedQuery(term, cultureFields); - results = qry.Execute(count); + results = skip == 0 && take == 0 + ? query.Execute() + : query.Execute(skip + take); } totalRecords = results.TotalItemCount; - return new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); + return new CultureContextualSearchResults(results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); } /// @@ -244,10 +250,11 @@ namespace Umbraco.Web { var results = skip == 0 && take == 0 ? query.Execute() - : query.Execute(maxResults: skip + take); + : query.Execute(skip + take); totalRecords = results.TotalItemCount; - return results.ToPublishedSearchResults(_publishedSnapshot.Content); + + return results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content); } /// From d3d8b1bd68fdb804dbae259e79dfb9dce4e50fda Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 1 Nov 2019 15:06:00 +0100 Subject: [PATCH 09/63] Ensure that listview status messages are vertically centered --- src/Umbraco.Web.UI.Client/src/less/main.less | 6 ++++++ .../src/views/components/property/umb-property.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 86a1acbeae..8578c22872 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -117,6 +117,12 @@ h5.-black { } .umb-control-group { position: relative; + + &.umb-control-group__listview { + // position: relative messes up the listview status messages (e.g. "no search results") + position: unset; + } + &::after { content: ''; display:block; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 927f677463..88a872e05b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -1,6 +1,6 @@
-
+
From 9b87b65537d92ca954db116f9948561fd55ea411 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 13 Nov 2019 14:39:11 +0100 Subject: [PATCH 10/63] Create Examine query that only searches content --- src/Umbraco.Web/PublishedContentQuery.cs | 40 ++++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 72aec94e58..8f4242c180 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -201,39 +201,31 @@ namespace Umbraco.Web throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); } - var searcher = umbIndex.GetSearcher(); + var query = umbIndex.GetSearcher().CreateQuery(IndexTypes.Content); - ISearchResults results; + IQueryExecutor queryExecutor; if (culture == "*") { // Search everything - results = skip == 0 && take == 0 - ? searcher.Search(term) - : searcher.Search(term, skip + take); + queryExecutor = query.ManagedQuery(term); + } + else if (string.IsNullOrWhiteSpace(culture)) + { + // Only search invariant + queryExecutor = query.Field(UmbracoContentIndex.VariesByCultureFieldName, "n") // Must not vary by culture + .And().ManagedQuery(term); } else { - IBooleanOperation query; - if (string.IsNullOrWhiteSpace(culture)) - { - // Only search invariant - query = searcher.CreateQuery() - .Field(UmbracoContentIndex.VariesByCultureFieldName, "n") // Must not vary by culture - .And().ManagedQuery(term); - } - else - { - // Only search the specified culture - var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied - query = searcher.CreateQuery() - .ManagedQuery(term, fields); - } - - results = skip == 0 && take == 0 - ? query.Execute() - : query.Execute(skip + take); + // Only search the specified culture + var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied + queryExecutor = query.ManagedQuery(term, fields); } + var results = skip == 0 && take == 0 + ? queryExecutor.Execute() + : queryExecutor.Execute(skip + take); + totalRecords = results.TotalItemCount; return new CultureContextualSearchResults(results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); From b64eb05e792abb9a3c44dda14f173541644deac1 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 13 Nov 2019 14:39:52 +0100 Subject: [PATCH 11/63] Update documentation of Search overloads --- src/Umbraco.Web/IPublishedContentQuery.cs | 48 +++++++++++++++-------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 8a8d678aba..351847da73 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -35,12 +35,15 @@ namespace Umbraco.Web /// /// Searches content. /// - /// Term to search. - /// Optional culture. - /// Optional index name. + /// The term to search. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// + /// The search results. + /// /// /// - /// When the is not specified or is *, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. /// To search for only invariant documents and fields use null. /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. /// @@ -51,15 +54,18 @@ namespace Umbraco.Web /// /// Searches content. /// - /// Term to search. - /// Numbers of items to skip. - /// Numbers of items to return. - /// Total number of matching items. - /// Optional culture. - /// Optional index name. + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// + /// The search results. + /// /// /// - /// When the is not specified or is *, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. /// To search for only invariant documents and fields use null. /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. /// @@ -68,18 +74,26 @@ namespace Umbraco.Web IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); /// - /// Executes the query and converts the results to PublishedSearchResult. + /// Executes the query and converts the results to using the . /// - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// + /// The query. + /// + /// The search results. + /// IEnumerable Search(IQueryExecutor query); /// - /// Executes the query and converts the results to PublishedSearchResult. + /// Executes the query and converts the results to using the . /// + /// The query. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// + /// The search results. + /// /// - /// While enumerating results, the ambient culture is changed to be the searched culture. + /// Make sure only content is searched (searcher.CreateQuery(IndexTypes.Content)), as this only returns content, but the could otherwise also include media records. /// IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } From 634f64851753f612b938475238aa687a0dc3c749 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Nov 2019 09:20:18 +0100 Subject: [PATCH 12/63] Require Examine 1.0.2 --- build/NuSpecs/UmbracoCms.Web.nuspec | 2 +- src/Umbraco.Examine/Umbraco.Examine.csproj | 4 ++-- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index c8374bc2f7..72b4df2ecb 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index db623ecddd..0e0ee62139 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -49,7 +49,7 @@ - + 1.0.0-beta2-19324-01 runtime; build; native; contentfiles; analyzers; buildtransitive @@ -112,4 +112,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 39826fcc38..47542484d8 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -79,7 +79,7 @@ - + 1.8.14 diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 6e766f578e..70039488ed 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -87,7 +87,7 @@ - + @@ -431,4 +431,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 56feaf52e7..9947a5e1de 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + 2.7.0.100 @@ -1279,4 +1279,4 @@ - + \ No newline at end of file From c206382fa105ebe4140fc724c5c987a4bf5bba22 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Nov 2019 09:23:18 +0100 Subject: [PATCH 13/63] Use snapshot to convert search results into content, media or members --- src/Umbraco.Web/ExamineExtensions.cs | 83 ++++++++++++++++++++--- src/Umbraco.Web/IPublishedContentQuery.cs | 7 +- src/Umbraco.Web/PublishedContentQuery.cs | 2 +- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web/ExamineExtensions.cs b/src/Umbraco.Web/ExamineExtensions.cs index 9a9fa98d95..8fead91194 100644 --- a/src/Umbraco.Web/ExamineExtensions.cs +++ b/src/Umbraco.Web/ExamineExtensions.cs @@ -2,32 +2,95 @@ using System.Collections.Generic; using System.Linq; using Examine; -using Umbraco.Core; +using Examine.LuceneEngine.Providers; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Examine; using Umbraco.Web.PublishedCache; namespace Umbraco.Web { /// - /// Extension methods for Examine + /// Extension methods for Examine. /// public static class ExamineExtensions { + /// + /// Creates an containing all content from the . + /// + /// The search results. + /// The cache to fetch the content from. + /// + /// An containing all content. + /// + /// cache + /// + /// Search results are skipped if it can't be fetched from the by its integer id. + /// public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedCache cache) { - var list = new List(); + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + var publishedSearchResults = new List(); foreach (var result in results.OrderByDescending(x => x.Score)) { - if (!int.TryParse(result.Id, out var intId)) continue; //invalid - var content = cache.GetById(intId); - if (content == null) continue; // skip if this doesn't exist in the cache - - list.Add(new PublishedSearchResult(content, result.Score)); - + if (int.TryParse(result.Id, out var contentId) && + cache.GetById(contentId) is IPublishedContent content) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); + } } - return list; + return publishedSearchResults; + } + + /// + /// Creates an containing all content, media or members from the . + /// + /// The search results. + /// The snapshot. + /// + /// An containing all content, media or members. + /// + /// snapshot + /// + /// Search results are skipped if it can't be fetched from the respective cache by its integer id. + /// + public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedSnapshot snapshot) + { + if (snapshot == null) throw new ArgumentNullException(nameof(snapshot)); + + var publishedSearchResults = new List(); + + foreach (var result in results.OrderByDescending(x => x.Score)) + { + if (int.TryParse(result.Id, out var contentId) && + result.Values.TryGetValue(LuceneIndex.CategoryFieldName, out var indexType)) + { + IPublishedContent content; + switch (indexType) + { + case IndexTypes.Content: + content = snapshot.Content.GetById(contentId); + break; + case IndexTypes.Media: + content = snapshot.Media.GetById(contentId); + break; + case IndexTypes.Member: + content = snapshot.Members.GetById(contentId); + break; + default: + continue; + } + + if (content != null) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); + } + } + } + + return publishedSearchResults; } } } diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 351847da73..c4d829968f 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -74,7 +74,7 @@ namespace Umbraco.Web IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); /// - /// Executes the query and converts the results to using the . + /// Executes the query and converts the results to . /// /// The query. /// @@ -83,7 +83,7 @@ namespace Umbraco.Web IEnumerable Search(IQueryExecutor query); /// - /// Executes the query and converts the results to using the . + /// Executes the query and converts the results to . /// /// The query. /// The amount of results to skip. @@ -92,9 +92,6 @@ namespace Umbraco.Web /// /// The search results. /// - /// - /// Make sure only content is searched (searcher.CreateQuery(IndexTypes.Content)), as this only returns content, but the could otherwise also include media records. - /// IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } } diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 8f4242c180..833df9971b 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -246,7 +246,7 @@ namespace Umbraco.Web totalRecords = results.TotalItemCount; - return results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content); + return results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot); } /// From d456051cbeef20aadc045c917e7f89641870342c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 2 Dec 2019 07:49:29 +0100 Subject: [PATCH 14/63] Do not save content when opening the content template in infinite editing --- .../src/views/components/content/umb-content-node-info.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index d72e977010..c35686acd1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -169,7 +169,7 @@ ng-change="updateTemplate(node.template)"> -
From aa158fa79fcac5e6e7924ca54a08956cde83ffc8 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 9 Dec 2019 22:27:38 +0100 Subject: [PATCH 15/63] Avoid UI "jumping" when sorting nested content items --- .../src/less/components/umb-nested-content.less | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 699496f5d3..4168ab3c39 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -41,11 +41,8 @@ } .umb-nested-content__item.ui-sortable-placeholder { - background: @gray-10; - border: 1px solid @gray-9; + margin-top: 1px; visibility: visible !important; - height: 55px; - margin-top: -1px; } .umb-nested-content__item--single > .umb-nested-content__content { From c6a7bfb1c9632d301625a4dfd9d7aa0cc4d49f68 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 10 Dec 2019 21:41:01 +0100 Subject: [PATCH 16/63] Don't add new Nested Content items on enter key --- .../nestedcontent/nestedcontent.propertyeditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index d24d3796f3..9eaafb9d80 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -41,7 +41,7 @@
From bfcc10e35eb1c22c4f8cae537186281c58c04d80 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 11 Dec 2019 09:06:12 +0100 Subject: [PATCH 17/63] Prevent the Nested Content item picker from crashing when hitting enter on "add content" twice --- .../propertyeditors/nestedcontent/nestedcontent.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 635a80dbe9..08f3c61174 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -134,7 +134,7 @@ }; vm.openNodeTypePicker = function ($event) { - if (vm.nodes.length >= vm.maxItems) { + if (vm.overlayMenu || vm.nodes.length >= vm.maxItems) { return; } From a8c55a4cb913421ec681abe9d7ba1481d808bd8d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 11 Dec 2019 09:53:20 +0100 Subject: [PATCH 18/63] Make the Multi URL Picker sorting behave the same as MNTP --- .../multiurlpicker/multiurlpicker.controller.js | 3 +++ 1 file changed, 3 insertions(+) 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 8381a53644..6b7d9dd7ae 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 @@ -19,7 +19,10 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en var currentForm = angularHelper.getCurrentForm($scope); $scope.sortableOptions = { + axis: "y", + containment: "parent", distance: 10, + opacity: 0.7, tolerance: "pointer", scroll: true, zIndex: 6000, From 448f3406ddec023be586374f20e00c7898669ec2 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 17 Dec 2019 08:50:09 +0100 Subject: [PATCH 19/63] Make sure MNTP config displays allowed items correctly when a start node is set --- .../prevalueeditors/treesource.controller.js | 15 ++++++++------- .../treesourcetypepicker.controller.js | 6 ++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js index 828763bc1c..f9c8ae8b0e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js @@ -19,15 +19,16 @@ angular.module('umbraco') }; } - if($scope.model.value.id && $scope.model.value.type !== "member"){ - entityResource.getById($scope.model.value.id, entityType()).then(function(item){ + if($scope.model.value.id && $scope.model.value.type !== "member"){ + entityResource.getById($scope.model.value.id, entityType()).then(function(item){ populate(item); - }); + }); + } + else { + $timeout(function () { + treeSourceChanged(); + }, 100); } - - $timeout(function () { - treeSourceChanged(); - }, 100); function entityType() { var ent = "Document"; diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js index ef781c6014..a87377c84b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js @@ -7,7 +7,6 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe var allItemTypes = null; var currentItemType = null; - var initialLoad = true; function init() { vm.loading = true; @@ -86,13 +85,12 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe } eventsService.on("treeSourceChanged", function (e, args) { - currentItemType = args.value; // reset the model value if we changed node type (but not on the initial load) - if (!initialLoad) { + if (!!currentItemType && currentItemType !== args.value) { vm.itemTypes = []; updateModel(); } - initialLoad = false; + currentItemType = args.value; init(); }); } From 39ae2fb258c9c115744bad734b67b4876564ac1c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 17 Dec 2019 12:30:45 +0100 Subject: [PATCH 20/63] Hide the Nested Content "add" button until the editor has loaded --- .../nestedcontent/nestedcontent.propertyeditor.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index d24d3796f3..6679ea6020 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -1,6 +1,6 @@ 
- + @@ -40,7 +40,7 @@
-