From d99d5a0b36ebbcbfb53077bcfce9c73a98901bef Mon Sep 17 00:00:00 2001 From: Patrick Scott Date: Thu, 3 Nov 2016 14:30:57 +0000 Subject: [PATCH 001/310] Issue U4-5572 convert file name to friendly media item name --- src/Umbraco.Core/StringExtensions.cs | 30 +++++++++++++++++++--- src/Umbraco.Web/Editors/MediaController.cs | 10 +------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 036b5b979f..02f2193e20 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -105,8 +105,8 @@ namespace Umbraco.Core //if the resolution was success, return it, otherwise just return the path, we've detected // it's a path but maybe it's relative and resolution has failed, etc... in which case we're just // returning what was given to us. - return resolvedUrlResult.Success - ? resolvedUrlResult + return resolvedUrlResult.Success + ? resolvedUrlResult : Attempt.Succeed(input); } } @@ -128,7 +128,7 @@ namespace Umbraco.Core } internal static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled); - internal static readonly string[] JsonEmpties = new [] { "[]", "{}" }; + internal static readonly string[] JsonEmpties = new[] { "[]", "{}" }; internal static bool DetectIsEmptyJson(this string input) { return JsonEmpties.Contains(Whitespace.Replace(input, string.Empty)); @@ -1470,5 +1470,29 @@ namespace Umbraco.Core byte[] hash = md5.ComputeHash(myStringBytes); return new Guid(hash); } + + + /// + /// Converts a file name to a friendly name for a content item + /// + /// + /// + public static string friendlyNameFromFilename(this string fileName) + { + // strip the file extension + fileName = StripFileExtension(fileName); + + // underscores and dashes to spaces + fileName = ReplaceMany(fileName, new char[] { '_', '-' }, ' '); + + // any other conversions ? + + // Pascalcase (to be done last) + fileName = System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(fileName); + + + return fileName; + } + } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 08140f9c66..7f6c9f2580 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -534,15 +534,7 @@ namespace Umbraco.Web.Editors if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) mediaType = Constants.Conventions.MediaTypes.Image; - //TODO: make the media item name "nice" since file names could be pretty ugly, we have - // string extensions to do much of this but we'll need: - // * Pascalcase the name (use string extensions) - // * strip the file extension - // * underscores to spaces - // * probably remove 'ugly' characters - let's discuss - // All of this logic should exist in a string extensions method and be unit tested - // http://issues.umbraco.org/issue/U4-5572 - var mediaItemName = fileName; + var mediaItemName = fileName.friendlyNameFromFilename(); var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); From 3b231f5138e470fda9dfa3f7a48f231129a3164b Mon Sep 17 00:00:00 2001 From: Eyescream Date: Fri, 28 Jul 2017 15:49:07 +0200 Subject: [PATCH 002/310] Update fileupload.controller.js Uploading a file with a comma in it resulted in an error (because server side there was a split on the comma character). The comma later was replaced with a dash, so it's safe to replace it before sending it to the server. --- .../views/propertyeditors/fileupload/fileupload.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index 3c8170e54b..e2112bdc6a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -118,7 +118,7 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag for (var i = 0; i < args.files.length; i++) { //save the file object to the scope's files collection $scope.files.push({ alias: $scope.model.alias, file: args.files[i] }); - newVal += args.files[i].name + ","; + newVal += args.files[i].name.replace(',','-') + ","; } //this is required to re-validate From a8ea535bb2ef9d24ccd28edbd44b58c28909c75e Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Mon, 22 Jan 2018 18:52:54 +0100 Subject: [PATCH 003/310] "Select editor" items shouldn't have a max height --- src/Umbraco.Web.UI.Client/src/less/components/card.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index bceef1767b..ecbd36e1e4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -97,7 +97,6 @@ text-align: center; width: 100px; - height: 105px; box-sizing: border-box; } @@ -115,6 +114,7 @@ width: 100%; height: 100%; border-radius: 3px; + padding-bottom: 5px; } From 477761a81adf317c6f1b94d9d59d0c4ec296e698 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 22 Mar 2018 10:41:14 +0100 Subject: [PATCH 004/310] Added property editor alias to PreValueDisplayResolver warning and updated key lookup --- .../Models/Mapping/DataTypeModelMapper.cs | 2 +- .../Models/Mapping/PreValueDisplayResolver.cs | 36 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs index 60d9eb7d3b..c2f8d8e25d 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs @@ -116,7 +116,7 @@ namespace Umbraco.Web.Models.Mapping var fields = editor.PreValueEditor.Fields.Select(Mapper.Map).ToArray(); if (defaultVals != null) { - PreValueDisplayResolver.MapPreValueValuesToPreValueFields(fields, defaultVals); + PreValueDisplayResolver.MapPreValueValuesToPreValueFields(fields, defaultVals, editor.Alias); } return fields; }); diff --git a/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs b/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs index f4e7a78504..9d1331e691 100644 --- a/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs @@ -18,27 +18,31 @@ namespace Umbraco.Web.Models.Mapping public PreValueDisplayResolver(IDataTypeService dataTypeService) { _dataTypeService = dataTypeService; - } - + } + /// - /// Maps pre-values in the dictionary to the values for the fields + /// Maps pre-values in the dictionary to the values for the fields. /// - /// - /// - internal static void MapPreValueValuesToPreValueFields(PreValueFieldDisplay[] fields, IDictionary preValues) + /// The fields. + /// The pre-values. + /// The editor alias. + internal static void MapPreValueValuesToPreValueFields(PreValueFieldDisplay[] fields, IDictionary preValues, string editorAlias) { - if (fields == null) throw new ArgumentNullException("fields"); - if (preValues == null) throw new ArgumentNullException("preValues"); - //now we need to wire up the pre-values values with the actual fields defined + if (fields == null) throw new ArgumentNullException(nameof(fields)); + if (preValues == null) throw new ArgumentNullException(nameof(preValues)); + + // Now we need to wire up the pre-values values with the actual fields defined foreach (var field in fields) { - var found = preValues.Any(x => x.Key.InvariantEquals(field.Key)); - if (found == false) + // If the dictionary would be constructed with StringComparer.InvariantCultureIgnoreCase, we could just use TryGetValue + var preValue = preValues.SingleOrDefault(x => x.Key.InvariantEquals(field.Key)); + if (preValue.Key == null) { - LogHelper.Warn("Could not find persisted pre-value for field " + field.Key); + LogHelper.Warn("Could not find persisted pre-value for field {0} on property editor {1}", () => field.Key, () => editorAlias); continue; } - field.Value = preValues.Single(x => x.Key.InvariantEquals(field.Key)).Value; + + field.Value = preValue.Value; } } @@ -54,20 +58,20 @@ namespace Umbraco.Web.Models.Mapping } } - //set up the defaults + // Set up the defaults var dataTypeService = _dataTypeService; var preVals = dataTypeService.GetPreValuesCollectionByDataTypeId(source.Id); IDictionary dictionaryVals = preVals.FormatAsDictionary().ToDictionary(x => x.Key, x => (object)x.Value); var result = Enumerable.Empty().ToArray(); - //if we have a prop editor, then format the pre-values based on it and create it's fields. + // If we have a prop editor, then format the pre-values based on it and create it's fields if (propEd != null) { result = propEd.PreValueEditor.Fields.Select(Mapper.Map).ToArray(); dictionaryVals = propEd.PreValueEditor.ConvertDbToEditor(propEd.DefaultPreValues, preVals); } - MapPreValueValuesToPreValueFields(result, dictionaryVals); + MapPreValueValuesToPreValueFields(result, dictionaryVals, source.PropertyEditorAlias); return result; } From 8ddda5868e5a29bf13ab2ac20e5bf6750e5d6337 Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 5 Apr 2018 10:41:08 +0200 Subject: [PATCH 005/310] fixes U4-10816 Umbraco V7.7.1 - Media - File upload taking full path --- src/Umbraco.Web/Editors/MediaController.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index e22c83cbeb..d2f0d060e0 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -466,6 +466,17 @@ namespace Umbraco.Web.Editors [ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem) { + //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. + //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + //uploaded files to being *only* the actual file name (as it should be). + if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) + { + foreach (var file in contentItem.UploadedFiles) + { + file.FileName = Path.GetFileName(file.FileName); + } + } + //If we've reached here it means: // * Our model has been bound // * and validated From 08bf48b8b7bbf9b0cad1ac590bc9d46fe5fd6e14 Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 5 Apr 2018 11:04:56 +0200 Subject: [PATCH 006/310] adding the fix for content controller - to support when uploads are done via content. --- src/Umbraco.Web/Editors/ContentController.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index be78638a9e..3ef3b8a40b 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -543,6 +544,17 @@ namespace Umbraco.Web.Editors private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) { + //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. + //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + //uploaded files to being *only* the actual file name (as it should be). + if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) + { + foreach (var file in contentItem.UploadedFiles) + { + file.FileName = Path.GetFileName(file.FileName); + } + } + //If we've reached here it means: // * Our model has been bound // * and validated From 592de8bebcba97441a0996f32bbc09102bbadea3 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 5 Jul 2018 16:00:53 +0200 Subject: [PATCH 007/310] Model, retrieval, mapping and display for fall back language editing --- .../Upgrade/V_8_0_0/FallbackLanguage.cs | 24 +++++++++++++++++++ src/Umbraco.Core/Models/ILanguage.cs | 6 +++++ src/Umbraco.Core/Models/Language.cs | 8 +++++++ .../Persistence/Dtos/LanguageDto.cs | 9 +++++++ .../Implement/LanguageRepository.cs | 21 +++++++++++++--- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../src/views/languages/edit.controller.js | 14 +++++++++++ .../src/views/languages/edit.html | 15 ++++++++++-- .../views/languages/overview.controller.js | 2 ++ .../src/views/languages/overview.html | 1 + .../Umbraco/config/lang/en_us.xml | 1 + .../Models/ContentEditing/Language.cs | 3 +++ 12 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs new file mode 100644 index 0000000000..f0d7c02b82 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + /// + /// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for + /// a given language. + /// + public class FallbackLanguage : MigrationBase + { + public FallbackLanguage(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) + AddColumn("fallbackLanguageId"); + } + } +} diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index 7bf9e9b32c..f02bd33d2b 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -33,5 +33,11 @@ namespace Umbraco.Core.Models /// If true, a variant node cannot be published unless this language variant is created /// bool Mandatory { get; set; } + + /// + /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// content if the requested language does not have it published. + /// + ILanguage FallbackLanguage { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index fa1c9dc826..42c305d492 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Models private string _cultureName; private bool _isDefaultVariantLanguage; private bool _mandatory; + private ILanguage _fallbackLanguage; public Language(string isoCode) { @@ -32,6 +33,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); + public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguage); } /// @@ -71,5 +73,11 @@ namespace Umbraco.Core.Models get => _mandatory; set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } + + public ILanguage FallbackLanguage + { + get => _fallbackLanguage; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguage, Ps.Value.FallbackLanguageSelector); + } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index 12c9fd0bd4..f69caf6c91 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -38,5 +38,14 @@ namespace Umbraco.Core.Persistence.Dtos [Column("mandatory")] [Constraint(Default = "0")] public bool Mandatory { get; set; } + + /// + /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// content if the requested language does not have it published. + /// + [Column("fallbackLanguageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] + public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index db2e1124a2..9566247f96 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -52,7 +52,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql.OrderBy(dto => dto.Id); // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).ToList(); + var dtos = Database.Fetch(sql); + var languages = dtos.Select(ConvertFromDto).ToList(); + PopulateFallbackLanguages(dtos, languages); // initialize the code-id map lock (_codeIdMap) @@ -74,7 +76,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - return Database.Fetch(sql).Select(ConvertFromDto); + var dtos = Database.Fetch(sql); + var languages = dtos.Select(ConvertFromDto).ToList(); + PopulateFallbackLanguages(dtos, languages); + return languages; } #endregion @@ -199,7 +204,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = LanguageFactory.BuildEntity(dto); return entity; } - + + private static void PopulateFallbackLanguages(List dtos, IList languages) + { + foreach (var dto in dtos.Where(x => x.FallbackLanguageId.HasValue)) + { + var language = languages.Single(x => x.Id == dto.Id); + // ReSharper disable once PossibleInvalidOperationException (DTOs with fallback languages have already been filtered in the loop condition) + language.FallbackLanguage = languages.Single(x => x.Id == dto.FallbackLanguageId.Value); + } + } + public ILanguage GetByIsoCode(string isoCode) { TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6bd78044f8..67028568eb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -334,6 +334,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index afb5333ded..9b554b1785 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -56,6 +56,20 @@ }); + $scope.properties = { + fallbackLanguage: { + alias: "fallbackLanguage", + description: "To allow multi-lingual content to fall back to another language if not present in the requested language, select it here.", + label: "Fall back language" + } + }; + + vm.loading = true; + languageResource.getAll().then(function (languages) { + vm.availableLanguages = languages; + vm.loading = false; + }); + if(!$routeParams.create) { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 6aaf915960..adc38de05a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -14,11 +14,11 @@ hide-alias="true"> - + - + @@ -64,6 +64,17 @@ + +
+ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index c81f93c7d6..2f116ef3cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -22,12 +22,14 @@ "treeHeaders_languages", "general_mandatory", "general_default", + "languages_fallsbackToLabel" ]; localizationService.localizeMany(labelKeys).then(function (values) { vm.labels.languages = values[0]; vm.labels.mandatory = values[1]; vm.labels.general = values[2]; + vm.labels.fallsbackTo = values[3]; // set page name vm.page.name = vm.labels.languages; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index 90764d3f67..f53326a491 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -36,6 +36,7 @@ - {{vm.labels.general}} {{vm.labels.mandatory}} + {{vm.labels.fallsbackTo}}: {{language.fallbackLanguage.name}} Default language An Umbraco site can only have one default langugae set. Switching default language may result in default content missing. + Falls back to diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index f78d2bd28f..309e111e32 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -24,5 +24,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "isMandatory")] public bool Mandatory { get; set; } + + [DataMember(Name = "fallbackLanguage")] + public Language FallbackLanguage { get; set; } } } From 660fe2d7732f32b5c43999666f20141dc0fd1061 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 5 Jul 2018 19:37:59 +0200 Subject: [PATCH 008/310] Localised fallback language related messages. Filtered out current languge from list of available fall back languages for selection. --- .../src/views/languages/edit.controller.js | 28 +++++++++++-------- .../src/views/languages/edit.html | 2 +- .../Umbraco/config/lang/en_us.xml | 5 +++- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 9b554b1785..3a4961ca81 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -29,7 +29,10 @@ "languages_mandatoryLanguageHelp", "languages_defaultLanguage", "languages_defaultLanguageHelp", - "languages_addLanguage" + "languages_addLanguage", + "languages_noFallbackLanguageOption", + "languages_fallbackLanguageDescription", + "languages_fallbackLanguage" ]; localizationService.localizeMany(labelKeys).then(function (values) { @@ -39,6 +42,15 @@ vm.labels.defaultLanguage = values[3]; vm.labels.defaultLanguageHelp = values[4]; vm.labels.addLanguage = values[5]; + vm.labels.noFallbackLanguageOption = values[6]; + + $scope.properties = { + fallbackLanguage: { + alias: "fallbackLanguage", + description: values[7], + label: values[8] + } + }; if($routeParams.create) { vm.page.name = vm.labels.addLanguage; @@ -56,21 +68,15 @@ }); - $scope.properties = { - fallbackLanguage: { - alias: "fallbackLanguage", - description: "To allow multi-lingual content to fall back to another language if not present in the requested language, select it here.", - label: "Fall back language" - } - }; - vm.loading = true; languageResource.getAll().then(function (languages) { - vm.availableLanguages = languages; + vm.availableLanguages = languages.filter(function (l) { + return $routeParams.id != l.id; + }); vm.loading = false; }); - if(!$routeParams.create) { + if (!$routeParams.create) { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index adc38de05a..9ebbc5aede 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -70,7 +70,7 @@ ng-model="vm.language.fallbackLanguage.id" required ng-options="l.id as l.name for l in vm.availableLanguages"> - + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 555d4edfb4..ea1db802d5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1657,11 +1657,14 @@ To manage your website, simply open the Umbraco back office and start adding con Add language Mandatory - Properties on this language has to be filled out before the node can be published. + Properties on this language have to be filled out before the node can be published. Default language An Umbraco site can only have one default langugae set. Switching default language may result in default content missing. Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language From 53e96b25f686b01bac86c8a6f86c84b8df69434f Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 15:23:21 +0200 Subject: [PATCH 009/310] Save of fall-back language on create and update of language --- .../Persistence/Factories/LanguageFactory.cs | 7 +++++ .../Implement/LanguageRepository.cs | 2 -- .../src/views/languages/edit.controller.js | 18 ++++++++++-- .../src/views/languages/edit.html | 1 - .../views/languages/overview.controller.js | 2 +- src/Umbraco.Web/Editors/LanguageController.cs | 28 ++++++++++++++----- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index 7b24411498..c805ae7f5a 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -18,7 +18,14 @@ namespace Umbraco.Core.Persistence.Factories { var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory }; if (entity.HasIdentity) + { dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); + } + + if (entity.FallbackLanguage != null) + { + dto.FallbackLanguageId = entity.FallbackLanguage.Id; + } return dto; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 9566247f96..96bb088f2b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -144,14 +144,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement IsolatedCache.ClearAllCache(); } -; var dto = LanguageFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); entity.Id = id; entity.ResetDirtyProperties(); - } protected override void PersistUpdatedItem(ILanguage entity) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 3a4961ca81..bc5c421b7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -52,7 +52,7 @@ } }; - if($routeParams.create) { + if ($routeParams.create) { vm.page.name = vm.labels.addLanguage; languageResource.getCultures().then(function (culturesDictionary) { var cultures = []; @@ -65,7 +65,6 @@ vm.availableCultures = cultures; }); } - }); vm.loading = true; @@ -99,11 +98,26 @@ }); } + function setCultureForFallbackLanguage(lang) { + for (var i = 0; i < vm.availableLanguages.length; i++) { + if (vm.availableLanguages[i].id === lang.id) { + lang.culture = vm.availableLanguages[i].culture; + break; + } + } + } + function save() { if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; + // We need to attach the ISO code to the fall-back language to pass + // server-side validation. + if (vm.language.fallbackLanguage) { + setCultureForFallbackLanguage(vm.language.fallbackLanguage); + } + languageResource.save(vm.language).then(function (lang) { formHelper.resetForm({ scope: $scope }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 9ebbc5aede..5570b901e2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -68,7 +68,6 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index 2f116ef3cb..c8a728d3aa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -34,7 +34,7 @@ vm.page.name = vm.labels.languages; }); - languageResource.getAll().then(function(languages) { + languageResource.getAll().then(function (languages) { vm.languages = languages; vm.loading = false; }); diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 96019da702..418c8401ff 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -8,8 +7,6 @@ using System.Web.Http; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; @@ -136,21 +133,38 @@ namespace Umbraco.Web.Editors } //create it - var newLang = new Umbraco.Core.Models.Language(culture.Name) + var newLang = new Core.Models.Language(culture.Name) { CultureName = culture.DisplayName, IsDefaultVariantLanguage = language.IsDefaultVariantLanguage, - Mandatory = language.Mandatory + Mandatory = language.Mandatory, }; + + AssociateFallbackLanguage(language, newLang); Services.LocalizationService.Save(newLang); return Mapper.Map(newLang); } found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; + AssociateFallbackLanguage(language, found); Services.LocalizationService.Save(found); return Mapper.Map(found); } - + + private static void AssociateFallbackLanguage(Language submittedLanguage, ILanguage languageToCreateOrUpdate) + { + if (submittedLanguage.FallbackLanguage == null) + { + return; + } + + var fallbackLanguageCulture = CultureInfo.GetCultureInfo(submittedLanguage.FallbackLanguage.IsoCode); + languageToCreateOrUpdate.FallbackLanguage = new Core.Models.Language(fallbackLanguageCulture.Name) + { + Id = submittedLanguage.FallbackLanguage.Id, + CultureName = fallbackLanguageCulture.DisplayName + }; + } } } From 8a34ec886470146ffdbd34cd3060991ce277eb22 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 15:51:13 +0200 Subject: [PATCH 010/310] Prevented creation of circular fall-back language paths. Allowed for update to no fall-back language. --- .../src/views/languages/edit.controller.js | 5 ++++ src/Umbraco.Web/Editors/LanguageController.cs | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index bc5c421b7b..79972725fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -112,6 +112,11 @@ if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; + // Handle selection of no fall-back language (should pass null) + if (!vm.language.fallbackLanguage.id) { + vm.language.fallbackLanguage = null; + } + // We need to attach the ISO code to the fall-back language to pass // server-side validation. if (vm.language.fallbackLanguage) { diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 418c8401ff..76a40ac329 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -148,6 +148,13 @@ namespace Umbraco.Web.Editors found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; AssociateFallbackLanguage(language, found); + + if (UpdatedFallbackLanguageCreatesCircularPath(found)) + { + ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + Services.LocalizationService.Save(found); return Mapper.Map(found); } @@ -156,6 +163,7 @@ namespace Umbraco.Web.Editors { if (submittedLanguage.FallbackLanguage == null) { + languageToCreateOrUpdate.FallbackLanguage = null; return; } @@ -166,5 +174,27 @@ namespace Umbraco.Web.Editors CultureName = fallbackLanguageCulture.DisplayName }; } + + private bool UpdatedFallbackLanguageCreatesCircularPath(ILanguage language) + { + if (language.FallbackLanguage == null) + { + return false; + } + + var languages = Services.LocalizationService.GetAllLanguages().ToArray(); + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + if (fallbackLanguage.Id == language.Id) + { + return true; + } + + fallbackLanguage = languages.Single(x => x.Id == fallbackLanguage.Id).FallbackLanguage; + } + + return false; + } } } From 4e859cd88bd0e23e72cfbf3b169d6304d8ebf4cd Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 16:01:54 +0200 Subject: [PATCH 011/310] Prevent delete of a language used as a fall-back for another language --- src/Umbraco.Web/Editors/LanguageController.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 76a40ac329..0640d16129 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -85,13 +85,23 @@ namespace Umbraco.Web.Editors public IHttpActionResult DeleteLanguage(int id) { var language = Services.LocalizationService.GetLanguageById(id); - if (language == null) return NotFound(); + if (language == null) + { + return NotFound(); + } - var totalLangs = Services.LocalizationService.GetAllLanguages().Count(); + var langs = Services.LocalizationService.GetAllLanguages().ToArray(); + var totalLangs = langs.Length; if (language.IsDefaultVariantLanguage || totalLangs == 1) { - var message = $"Language '{language.IsoCode}' is currently set to 'default' or it is the only installed language and can not be deleted."; + var message = $"Language '{language.CultureName}' is currently set to 'default' or it is the only installed language and cannot be deleted."; + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); + } + + if (language.FallbackLanguage != null && langs.Any(x => x.FallbackLanguage?.Id == language.Id)) + { + var message = $"Language '{language.CultureName}' is defined as a fall-back language for one or more other languages, and so cannot be deleted."; throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); } From 3c13b7bd70e79338f2f00aba79104c59472b2a3e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 23:30:57 +0200 Subject: [PATCH 012/310] Added and updated tests for fall back languages --- .../Persistence/Dtos/LanguageDto.cs | 1 + .../Repositories/LanguageRepositoryTest.cs | 39 ++++++++++++++++--- src/Umbraco.Web/Editors/LanguageController.cs | 8 ++-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index f69caf6c91..25ca43f918 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -46,6 +46,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index cd1fe47f39..bda899789d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -47,6 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo("en-US")); Assert.That(language.IsoCode, Is.EqualTo("en-US")); + Assert.That(language.FallbackLanguage, Is.Null); } } @@ -61,7 +62,8 @@ namespace Umbraco.Tests.Persistence.Repositories var au = CultureInfo.GetCultureInfo("en-AU"); var language = (ILanguage)new Language(au.Name) { - CultureName = au.DisplayName + CultureName = au.DisplayName, + FallbackLanguage = repository.Get(1) }; repository.Save(language); @@ -73,6 +75,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo(au.DisplayName)); Assert.That(language.IsoCode, Is.EqualTo(au.Name)); + Assert.That(language.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -182,7 +185,7 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var languageBR = new Language("pt-BR") {CultureName = "pt-BR"}; + var languageBR = new Language("pt-BR") { CultureName = "pt-BR" }; repository.Save(languageBR); // Assert @@ -190,6 +193,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 Assert.IsFalse(languageBR.IsDefaultVariantLanguage); Assert.IsFalse(languageBR.Mandatory); + Assert.IsNull(languageBR.FallbackLanguage); } } @@ -211,6 +215,31 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 Assert.IsTrue(languageBR.IsDefaultVariantLanguage); Assert.IsTrue(languageBR.Mandatory); + Assert.IsNull(languageBR.FallbackLanguage); + } + } + + [Test] + public void Can_Perform_Add_On_LanguageRepository_With_Fallback_Language() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + // Act + var languageBR = new Language("pt-BR") + { + CultureName = "pt-BR", + FallbackLanguage = repository.Get(1) + }; + repository.Save(languageBR); + + // Assert + Assert.That(languageBR.HasIdentity, Is.True); + Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 + Assert.That(languageBR.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -232,13 +261,11 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.IsTrue(languageBR.Mandatory); // Act - var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefaultVariantLanguage = true, Mandatory = true }; repository.Save(languageNZ); languageBR = repository.Get(languageBR.Id); // Assert - Assert.IsFalse(languageBR.IsDefaultVariantLanguage); Assert.IsTrue(languageNZ.IsDefaultVariantLanguage); } @@ -257,6 +284,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = repository.Get(5); language.IsoCode = "pt-BR"; language.CultureName = "pt-BR"; + language.FallbackLanguage = repository.Get(1); repository.Save(language); @@ -266,6 +294,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageUpdated, Is.Not.Null); Assert.That(languageUpdated.IsoCode, Is.EqualTo("pt-BR")); Assert.That(languageUpdated.CultureName, Is.EqualTo("pt-BR")); + Assert.That(languageUpdated.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -314,7 +343,7 @@ namespace Umbraco.Tests.Persistence.Repositories base.TearDown(); } - public void CreateTestData() + private void CreateTestData() { var languageDK = new Language("da-DK") { CultureName = "da-DK" }; ServiceContext.LocalizationService.Save(languageDK);//Id 2 diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 0640d16129..9b73b50300 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -66,12 +66,11 @@ namespace Umbraco.Web.Editors } else if (allLangs.All(x => !x.IsDefaultVariantLanguage)) { - //if no language has the default flag, then the defaul language is the one with the lowest id + //if no language has the default flag, then the default language is the one with the lowest id model.IsDefaultVariantLanguage = allLangs[0].Id == lang.Id; model.Mandatory = allLangs[0].Id == lang.Id; } } - return model; } @@ -159,7 +158,7 @@ namespace Umbraco.Web.Editors found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; AssociateFallbackLanguage(language, found); - if (UpdatedFallbackLanguageCreatesCircularPath(found)) + if (DoesUpdatedFallbackLanguageCreateACircularPath(found)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); @@ -185,7 +184,7 @@ namespace Umbraco.Web.Editors }; } - private bool UpdatedFallbackLanguageCreatesCircularPath(ILanguage language) + private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language) { if (language.FallbackLanguage == null) { @@ -198,6 +197,7 @@ namespace Umbraco.Web.Editors { if (fallbackLanguage.Id == language.Id) { + // We've found the current language in the path of fall back languages, so we have a circular path. return true; } From aadd843c332c9120064503d9ac2104dd75b8b769 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 8 Jul 2018 15:17:38 +0200 Subject: [PATCH 013/310] Implemented published value fall back via language --- .../IPublishedValueFallback.cs | 10 +- .../NoopPublishedValueFallback.cs | 6 +- .../PublishedContent/PublishedContentTests.cs | 6 +- src/Umbraco.Tests/TestHelpers/BaseWebTest.cs | 2 +- .../PublishedValueFallback.cs | 14 +- .../PublishedValueLanguageFallback.cs | 219 ++++++++++++++++++ src/Umbraco.Web/PublishedContentExtensions.cs | 14 +- .../Runtime/WebRuntimeComponent.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 9 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 8e1dcfd543..f154d9ef27 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -2,6 +2,12 @@ namespace Umbraco.Core.Models.PublishedContent { + public enum PublishedValueFallbackPriority + { + RecursiveTree, + FallbackLanguage + } + /// /// Provides a fallback strategy for getting values. /// @@ -30,8 +36,8 @@ namespace Umbraco.Core.Models.PublishedContent T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index b99b4ad415..75ab9df35a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -21,9 +21,9 @@ public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) => defaultValue; + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 02f58a00a3..a09cf6d4ad 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -34,11 +34,11 @@ namespace Umbraco.Tests.PublishedContent Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); Container.RegisterSingleton(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new VoidEditor(logger)) { Id = 1}, + new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, new DataType(new RichTextPropertyEditor(logger)) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, @@ -323,7 +323,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] - public void GetPropertyValueRecursiveTest() + public void Get_Property_Value_Recursive() { var doc = GetNode(1174); var rVal = doc.Value("testRecursive", recurse: true); diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 5eea6bcf72..2f7fe8700b 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.TestHelpers { base.Compose(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); Container.RegisterSingleton(); } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 47e4b3d872..562b8e393b 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -11,45 +11,45 @@ namespace Umbraco.Web.Models.PublishedContent // kinda reproducing what was available in v7 /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) { // no fallback here return defaultValue; } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) { // no fallback here return defaultValue; } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) + public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) { // no fallback here if (!recurse) return defaultValue; // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, recurse); + return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority); } /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) + public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) { // no fallback here if (!recurse) return defaultValue; diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs new file mode 100644 index 0000000000..c404288a0e --- /dev/null +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -0,0 +1,219 @@ +using LightInject; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Models.PublishedContent +{ + /// + /// Provides a default implementation for that allows + /// for use of fall-back languages + /// + /// + /// Inherits from that implments what was available in v7. + /// + public class PublishedValueLanguageFallback : PublishedValueFallback + { + /// + /// Gets or sets the services context. + /// + [Inject] + public ServiceContext Services { get; set; } + + /// + public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + { + object value; + if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value)) + { + return value; + } + + return base.GetValue(property, culture, segment, defaultValue); + } + + /// + public override T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + { + T value; + if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value)) + { + return value; + } + + return base.GetValue(property, culture, segment, defaultValue); + } + + /// + public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + { + object value; + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value)) + { + return value; + } + + return base.GetValue(content, alias, culture, segment, defaultValue); + } + + /// + public override T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + { + T value; + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value)) + { + return value; + } + + return base.GetValue(content, alias, culture, segment, defaultValue); + } + + /// + public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + { + return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + } + + /// + public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + { + if (fallbackPriority == PublishedValueFallbackPriority.RecursiveTree) + { + var result = base.GetValue(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree); + if (ValueIsNotNullEmptyOrDefault(result, defaultValue)) + { + // We've prioritised recursive tree search and found a value, so can return it. + return result; + } + + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result)) + { + return result; + } + + return defaultValue; + } + + if (fallbackPriority == PublishedValueFallbackPriority.FallbackLanguage) + { + T result; + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result)) + { + return result; + } + } + + // No language fall back content found, so use base implementation + return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + } + + private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) + { + return value != null && + string.IsNullOrEmpty(value.ToString()) == false && + value.Equals(defaultValue) == false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private static ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage, ILocalizationService localizationService) + { + fallbackLanguage = localizationService.GetLanguageById(fallbackLanguage.Id); // Ensures reference to next fall-back language is loaded if it exists + return fallbackLanguage.FallbackLanguage; + } + } +} diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 1adfb55ca9..22a0bc8aca 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -159,8 +159,9 @@ namespace Umbraco.Web /// The property alias. /// The variation language. /// The variation segment. - /// A value indicating whether to recurse. /// The default value. + /// A value indicating whether to recurse. + /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -169,14 +170,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, bool recurse = false) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); } #endregion @@ -193,6 +194,7 @@ namespace Umbraco.Web /// The variation segment. /// The default value. /// A value indicating whether to recurse. + /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. /// The value of the content's property identified by the alias, converted to the specified type. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -201,18 +203,18 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); } // fixme - .Value() refactoring - in progress - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false) + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index a4e5db0767..501fb6445d 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -199,7 +199,7 @@ namespace Umbraco.Web.Runtime composition.Container.Register(_ => GlobalHost.ConnectionManager.GetHubContext(), new PerContainerLifetime()); // register properties fallback - composition.Container.RegisterSingleton(); + composition.Container.RegisterSingleton(); } internal void Initialize( diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f266213da0..e0f2554412 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -253,6 +253,7 @@ + From 11871dc4f7d6565022c0174b8ace37d1ef7a0563 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 8 Jul 2018 16:11:26 +0200 Subject: [PATCH 014/310] Changed the services reference in the published value fall back via language implementation to constructor injection --- .../PublishedValueLanguageFallback.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index c404288a0e..e6902c00db 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -1,5 +1,4 @@ -using LightInject; -using Umbraco.Core.Models; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; @@ -14,11 +13,12 @@ namespace Umbraco.Web.Models.PublishedContent /// public class PublishedValueLanguageFallback : PublishedValueFallback { - /// - /// Gets or sets the services context. - /// - [Inject] - public ServiceContext Services { get; set; } + private readonly ILocalizationService _localizationService; + + public PublishedValueLanguageFallback(ILocalizationService localizationService) + { + _localizationService = localizationService; + } /// public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) @@ -122,8 +122,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -139,7 +138,7 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; @@ -154,8 +153,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -171,7 +169,7 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; @@ -186,8 +184,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -203,16 +200,18 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; return false; } - private static ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage, ILocalizationService localizationService) + private ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage) { - fallbackLanguage = localizationService.GetLanguageById(fallbackLanguage.Id); // Ensures reference to next fall-back language is loaded if it exists + // Ensure reference to next fall-back language is loaded if it exists + fallbackLanguage = _localizationService.GetLanguageById(fallbackLanguage.Id); + return fallbackLanguage.FallbackLanguage; } } From f868bc9589e679aa8cc280f469fb1f73de1299b8 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 11 Jul 2018 09:23:26 +0200 Subject: [PATCH 015/310] Added fall back language database migration to UmbracoPlan --- src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index b7a77b10ce..9db4710241 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -123,7 +123,7 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{3608CD41-792A-4E9A-A97D-42A5E797EE31}"); // must chain to v8 final state (see at end of file) - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // UPGRADE FROM 7, MORE RECENT @@ -221,10 +221,13 @@ namespace Umbraco.Core.Migrations.Upgrade //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one - need a path to final state Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + // 8.0.0 + Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); + // FINAL STATE - MUST MATCH LAST ONE ABOVE ! // whenever this changes, update all references in this file! - Add(string.Empty, "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Add(string.Empty, "{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); } } } From 50c5b2ed9213425db1d6d9b1741868b0ae03458e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 11 Jul 2018 22:15:58 +0100 Subject: [PATCH 016/310] Added test for fallback languages --- .../PublishedContentLanuageVariantTests.cs | 130 ++++++++++ .../PublishedContentMoreTests.cs | 239 ++++++------------ .../PublishedContentSnapshotTestBase.cs | 100 ++++++++ .../SolidPublishedSnapshot.cs | 70 ++++- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../PublishedValueLanguageFallback.cs | 4 +- 6 files changed, 376 insertions(+), 169 deletions(-) create mode 100644 src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs create mode 100644 src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs new file mode 100644 index 0000000000..ea77310977 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using Umbraco.Tests.Testing; +using Umbraco.Web; + +namespace Umbraco.Tests.PublishedContent +{ + [TestFixture] + [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] + public class PublishedContentLanuageVariantTests : PublishedContentSnapshotTestBase + { + protected override void Compose() + { + base.Compose(); + + Container.RegisterSingleton(_ => GetServiceContext()); + } + + protected ServiceContext GetServiceContext() + { + var serviceContext = TestObjects.GetServiceContextMock(Container); + MockLocalizationService(serviceContext); + return serviceContext; + } + + private static void MockLocalizationService(ServiceContext serviceContext) + { + // Set up languages. + // Spanish falls back to English and Italian to Spanish (and then to English). + // French has no fall back. + var languages = new List + { + new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, + new Language("fr") { Id = 2, CultureName = "French" }, + new Language("es") { Id = 3, CultureName = "Spanish" }, + new Language("it") { Id = 4, CultureName = "Italian" }, + new Language("de") { Id = 5, CultureName = "German" } + }; + languages[2].FallbackLanguage = languages[0]; + languages[3].FallbackLanguage = languages[2]; + + var localizationService = Mock.Get(serviceContext.LocalizationService); + localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); + localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) + .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); + } + + internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache) + { + var props = new[] + { + factory.CreatePropertyType("prop1", 1), + }; + var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); + + var prop1 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText", + }; + prop1.SetSourceValue("en-US", "Welcome"); + prop1.SetValue("en-US", "Welcome"); + prop1.SetSourceValue("de", "Willkommen"); + prop1.SetValue("de", "Willkommen"); + + cache.Add(new SolidPublishedContent(contentType1) + { + Id = 1, + SortOrder = 0, + Name = "Content 1", + UrlSegment = "content-1", + Path = "/1", + Level = 1, + Url = "/content-1", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + prop1 + } + }); + } + + [Test] + public void Can_Get_Content_For_Populated_Requested_Language() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "en-US"); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_For_Populated_Requested_Non_Default_Language() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "de"); + Assert.AreEqual("Willkommen", value); + } + + [Test] + public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_Without_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "fr"); + Assert.IsNull(value); + } + + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "es"); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "it"); + Assert.AreEqual("Welcome", value); + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index 101c7827c3..22965ac141 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -1,91 +1,94 @@ -using System; +using System.Collections.ObjectModel; using System.Linq; -using System.Collections.ObjectModel; -using System.Web.Routing; -using Moq; using NUnit.Framework; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -using Umbraco.Core.Composing; -using Current = Umbraco.Core.Composing.Current; -using LightInject; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; -using Umbraco.Tests.Testing.Objects.Accessors; namespace Umbraco.Tests.PublishedContent { [TestFixture] [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] - public class PublishedContentMoreTests : PublishedContentTestBase + public class PublishedContentMoreTests : PublishedContentSnapshotTestBase { - // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet - // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx - // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx - - public override void SetUp() + internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache) { - base.SetUp(); + var props = new[] + { + factory.CreatePropertyType("prop1", 1), + }; + var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); + var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), props); + var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), props); - var umbracoContext = GetUmbracoContext(); - Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; - } + cache.Add(new SolidPublishedContent(contentType1) + { + Id = 1, + SortOrder = 0, + Name = "Content 1", + UrlSegment = "content-1", + Path = "/1", + Level = 1, + Url = "/content-1", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); - protected override void Compose() - { - base.Compose(); + cache.Add(new SolidPublishedContent(contentType2) + { + Id = 2, + SortOrder = 1, + Name = "Content 2", + UrlSegment = "content-2", + Path = "/2", + Level = 1, + Url = "/content-2", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); - Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); - } - - protected override TypeLoader CreatePluginManager(IServiceFactory f) - { - var pluginManager = base.CreatePluginManager(f); - - // this is so the model factory looks into the test assembly - pluginManager.AssembliesToScan = pluginManager.AssembliesToScan - .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) - .ToList(); - - return pluginManager; - } - - private UmbracoContext GetUmbracoContext() - { - RouteData routeData = null; - - var publishedSnapshot = CreatePublishedSnapshot(); - - var publishedSnapshotService = new Mock(); - publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot); - - var globalSettings = TestObjects.GetGlobalSettings(); - - var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext; - var umbracoContext = new UmbracoContext( - httpContext, - publishedSnapshotService.Object, - new WebSecurity(httpContext, Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), - Enumerable.Empty(), - globalSettings, - new TestVariationContextAccessor()); - - return umbracoContext; - } - - public override void TearDown() - { - base.TearDown(); - - Current.Reset(); + cache.Add(new SolidPublishedContent(contentType2Sub) + { + Id = 3, + SortOrder = 2, + Name = "Content 2Sub", + UrlSegment = "content-2sub", + Path = "/3", + Level = 1, + Url = "/content-2sub", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); } [Test] @@ -197,95 +200,5 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(1, result[0].Id); Assert.AreEqual(2, result[1].Id); } - - private static SolidPublishedSnapshot CreatePublishedSnapshot() - { - var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); - - var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); - var caches = new SolidPublishedSnapshot(); - var cache = caches.InnerContentCache; - - var props = new[] - { - factory.CreatePropertyType("prop1", 1), - }; - - var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); - var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), props); - var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), props); - - cache.Add(new SolidPublishedContent(contentType1) - { - Id = 1, - SortOrder = 0, - Name = "Content 1", - UrlSegment = "content-1", - Path = "/1", - Level = 1, - Url = "/content-1", - ParentId = -1, - ChildIds = new int[] {}, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - cache.Add(new SolidPublishedContent(contentType2) - { - Id = 2, - SortOrder = 1, - Name = "Content 2", - UrlSegment = "content-2", - Path = "/2", - Level = 1, - Url = "/content-2", - ParentId = -1, - ChildIds = new int[] { }, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - cache.Add(new SolidPublishedContent(contentType2Sub) - { - Id = 3, - SortOrder = 2, - Name = "Content 2Sub", - UrlSegment = "content-2sub", - Path = "/3", - Level = 1, - Url = "/content-2sub", - ParentId = -1, - ChildIds = new int[] { }, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - return caches; - } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs new file mode 100644 index 0000000000..623472a023 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Collections.ObjectModel; +using System.Web.Routing; +using Moq; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; +using Umbraco.Core.Composing; +using Current = Umbraco.Core.Composing.Current; +using LightInject; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects.Accessors; + +namespace Umbraco.Tests.PublishedContent +{ + public abstract class PublishedContentSnapshotTestBase : PublishedContentTestBase + { + // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet + // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx + // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx + + public override void SetUp() + { + base.SetUp(); + + var umbracoContext = GetUmbracoContext(); + Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; + } + + protected override void Compose() + { + base.Compose(); + + Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); + } + + protected override TypeLoader CreatePluginManager(IServiceFactory f) + { + var pluginManager = base.CreatePluginManager(f); + + // this is so the model factory looks into the test assembly + pluginManager.AssembliesToScan = pluginManager.AssembliesToScan + .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) + .ToList(); + + return pluginManager; + } + + private UmbracoContext GetUmbracoContext() + { + RouteData routeData = null; + + var publishedSnapshot = CreatePublishedSnapshot(); + + var publishedSnapshotService = new Mock(); + publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot); + + var globalSettings = TestObjects.GetGlobalSettings(); + + var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext; + var umbracoContext = new UmbracoContext( + httpContext, + publishedSnapshotService.Object, + new WebSecurity(httpContext, Current.Services.UserService, globalSettings), + TestObjects.GetUmbracoSettings(), + Enumerable.Empty(), + globalSettings, + new TestVariationContextAccessor()); + + return umbracoContext; + } + + public override void TearDown() + { + base.TearDown(); + + Current.Reset(); + } + + private SolidPublishedSnapshot CreatePublishedSnapshot() + { + var dataTypeService = new TestObjects.TestDataTypeService( + new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); + + var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); + var caches = new SolidPublishedSnapshot(); + var cache = caches.InnerContentCache; + PopulateCache(factory, cache); + return caches; + } + + internal abstract void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache); + } +} diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 4f63533693..33e315ebec 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -257,10 +257,72 @@ namespace Umbraco.Tests.PublishedContent public bool SolidHasValue { get; set; } public object SolidXPathValue { get; set; } - public object GetSourceValue(string culture = null, string segment = null) => SolidSourceValue; - public object GetValue(string culture = null, string segment = null) => SolidValue; - public object GetXPathValue(string culture = null, string segment = null) => SolidXPathValue; - public bool HasValue(string culture = null, string segment = null) => SolidHasValue; + public virtual object GetSourceValue(string culture = null, string segment = null) => SolidSourceValue; + public virtual object GetValue(string culture = null, string segment = null) => SolidValue; + public virtual object GetXPathValue(string culture = null, string segment = null) => SolidXPathValue; + public virtual bool HasValue(string culture = null, string segment = null) => SolidHasValue; + } + + internal class SolidPublishedPropertyWithLanguageVariants : SolidPublishedProperty + { + private readonly IDictionary _solidSourceValues = new Dictionary(); + private readonly IDictionary _solidValues = new Dictionary(); + private readonly IDictionary _solidXPathValues = new Dictionary(); + + public override object GetSourceValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetSourceValue(culture, segment); + } + + return _solidSourceValues.ContainsKey(culture) ? _solidSourceValues[culture] : null; + } + + public override object GetValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetValue(culture, segment); + } + + return _solidValues.ContainsKey(culture) ? _solidValues[culture] : null; + } + + public override object GetXPathValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetXPathValue(culture, segment); + } + + return _solidXPathValues.ContainsKey(culture) ? _solidXPathValues[culture] : null; + } + + public override bool HasValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.HasValue(culture, segment); + } + + return _solidSourceValues.ContainsKey(culture); + } + + public void SetSourceValue(string culture, object value) + { + _solidSourceValues.Add(culture, value); + } + + public void SetValue(string culture, object value) + { + _solidValues.Add(culture, value); + } + + public void SetXPathValue(string culture, object value) + { + _solidXPathValues.Add(culture, value); + } } [PublishedModel("ContentType2")] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c86cf5d2d0..592527a4b6 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -122,6 +122,8 @@ + + diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index e6902c00db..b6dc9f4244 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -15,9 +15,9 @@ namespace Umbraco.Web.Models.PublishedContent { private readonly ILocalizationService _localizationService; - public PublishedValueLanguageFallback(ILocalizationService localizationService) + public PublishedValueLanguageFallback(ServiceContext services) { - _localizationService = localizationService; + _localizationService = services.LocalizationService; } /// From d1a31ad8f8383ed1a860e33c8657901405826841 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 12 Jul 2018 08:12:54 +0100 Subject: [PATCH 017/310] Removed index from fallback language column --- src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index 25ca43f918..f87930269a 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -45,7 +45,6 @@ namespace Umbraco.Core.Persistence.Dtos /// [Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.NonClustered)] [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } } From 91a0ee2c93d0a136bad6b0ed8caf44368ea2f625 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 12 Jul 2018 20:52:02 +0100 Subject: [PATCH 018/310] Refactored to object graph reference of fallback language to just use id --- src/Umbraco.Core/Models/ILanguage.cs | 4 +- src/Umbraco.Core/Models/Language.cs | 10 ++-- .../Persistence/Factories/LanguageFactory.cs | 9 +--- .../Implement/LanguageRepository.cs | 15 +----- .../Repositories/LanguageRepositoryTest.cs | 18 +++---- .../PublishedContentLanuageVariantTests.cs | 8 ++-- .../src/views/languages/edit.controller.js | 20 -------- .../src/views/languages/edit.html | 2 +- .../views/languages/overview.controller.js | 10 ++++ .../src/views/languages/overview.html | 2 +- src/Umbraco.Web/Editors/LanguageController.cs | 47 ++++++++----------- .../Models/ContentEditing/Language.cs | 4 +- .../PublishedValueLanguageFallback.cs | 46 +++++++++--------- 13 files changed, 80 insertions(+), 115 deletions(-) diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index f02bd33d2b..8d1c092e13 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -35,9 +35,9 @@ namespace Umbraco.Core.Models bool Mandatory { get; set; } /// - /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// Defines the id of a fallback language that can be used in multi-lingual scenarios to provide /// content if the requested language does not have it published. /// - ILanguage FallbackLanguage { get; set; } + int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 42c305d492..5fcd5cd50e 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Models private string _cultureName; private bool _isDefaultVariantLanguage; private bool _mandatory; - private ILanguage _fallbackLanguage; + private int? _fallbackLanguageId; public Language(string isoCode) { @@ -33,7 +33,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); - public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguage); + public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguageId); } /// @@ -74,10 +74,10 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } - public ILanguage FallbackLanguage + public int? FallbackLanguageId { - get => _fallbackLanguage; - set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguage, Ps.Value.FallbackLanguageSelector); + get => _fallbackLanguageId; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, Ps.Value.FallbackLanguageSelector); } } } diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index c805ae7f5a..7ab36d15d6 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Factories { public static ILanguage BuildEntity(LanguageDto dto) { - var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, Mandatory = dto.Mandatory }; + var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, Mandatory = dto.Mandatory, FallbackLanguageId = dto.FallbackLanguageId }; // reset dirty initial properties (U4-1946) lang.ResetDirtyProperties(false); return lang; @@ -16,17 +16,12 @@ namespace Umbraco.Core.Persistence.Factories public static LanguageDto BuildDto(ILanguage entity) { - var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory }; + var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory, FallbackLanguageId = entity.FallbackLanguageId }; if (entity.HasIdentity) { dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); } - if (entity.FallbackLanguage != null) - { - dto.FallbackLanguageId = entity.FallbackLanguage.Id; - } - return dto; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 96bb088f2b..af5d28c18e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -54,7 +54,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get languages var dtos = Database.Fetch(sql); var languages = dtos.Select(ConvertFromDto).ToList(); - PopulateFallbackLanguages(dtos, languages); // initialize the code-id map lock (_codeIdMap) @@ -77,9 +76,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); var dtos = Database.Fetch(sql); - var languages = dtos.Select(ConvertFromDto).ToList(); - PopulateFallbackLanguages(dtos, languages); - return languages; + return dtos.Select(ConvertFromDto).ToList(); } #endregion @@ -203,16 +200,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return entity; } - private static void PopulateFallbackLanguages(List dtos, IList languages) - { - foreach (var dto in dtos.Where(x => x.FallbackLanguageId.HasValue)) - { - var language = languages.Single(x => x.Id == dto.Id); - // ReSharper disable once PossibleInvalidOperationException (DTOs with fallback languages have already been filtered in the loop condition) - language.FallbackLanguage = languages.Single(x => x.Id == dto.FallbackLanguageId.Value); - } - } - public ILanguage GetByIsoCode(string isoCode) { TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index bda899789d..a63bf5e08d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -47,7 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo("en-US")); Assert.That(language.IsoCode, Is.EqualTo("en-US")); - Assert.That(language.FallbackLanguage, Is.Null); + Assert.That(language.FallbackLanguageId, Is.Null); } } @@ -63,7 +63,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = (ILanguage)new Language(au.Name) { CultureName = au.DisplayName, - FallbackLanguage = repository.Get(1) + FallbackLanguageId = 1 }; repository.Save(language); @@ -75,7 +75,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo(au.DisplayName)); Assert.That(language.IsoCode, Is.EqualTo(au.Name)); - Assert.That(language.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(language.FallbackLanguageId, Is.EqualTo(1)); } } @@ -193,7 +193,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 Assert.IsFalse(languageBR.IsDefaultVariantLanguage); Assert.IsFalse(languageBR.Mandatory); - Assert.IsNull(languageBR.FallbackLanguage); + Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -215,7 +215,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 Assert.IsTrue(languageBR.IsDefaultVariantLanguage); Assert.IsTrue(languageBR.Mandatory); - Assert.IsNull(languageBR.FallbackLanguage); + Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -232,14 +232,14 @@ namespace Umbraco.Tests.Persistence.Repositories var languageBR = new Language("pt-BR") { CultureName = "pt-BR", - FallbackLanguage = repository.Get(1) + FallbackLanguageId = 1 }; repository.Save(languageBR); // Assert Assert.That(languageBR.HasIdentity, Is.True); Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 - Assert.That(languageBR.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(languageBR.FallbackLanguageId, Is.EqualTo(1)); } } @@ -284,7 +284,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = repository.Get(5); language.IsoCode = "pt-BR"; language.CultureName = "pt-BR"; - language.FallbackLanguage = repository.Get(1); + language.FallbackLanguageId = 1; repository.Save(language); @@ -294,7 +294,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageUpdated, Is.Not.Null); Assert.That(languageUpdated.IsoCode, Is.EqualTo("pt-BR")); Assert.That(languageUpdated.CultureName, Is.EqualTo("pt-BR")); - Assert.That(languageUpdated.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(languageUpdated.FallbackLanguageId, Is.EqualTo(1)); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs index ea77310977..22eb4bd799 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -39,15 +39,15 @@ namespace Umbraco.Tests.PublishedContent { new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, new Language("fr") { Id = 2, CultureName = "French" }, - new Language("es") { Id = 3, CultureName = "Spanish" }, - new Language("it") { Id = 4, CultureName = "Italian" }, + new Language("es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 }, + new Language("it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 }, new Language("de") { Id = 5, CultureName = "German" } }; - languages[2].FallbackLanguage = languages[0]; - languages[3].FallbackLanguage = languages[2]; var localizationService = Mock.Get(serviceContext.LocalizationService); localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); + localizationService.Setup(x => x.GetLanguageById(It.IsAny())) + .Returns((int id) => languages.SingleOrDefault(y => y.Id == id)); localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); } diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 79972725fc..523ef867cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -98,31 +98,11 @@ }); } - function setCultureForFallbackLanguage(lang) { - for (var i = 0; i < vm.availableLanguages.length; i++) { - if (vm.availableLanguages[i].id === lang.id) { - lang.culture = vm.availableLanguages[i].culture; - break; - } - } - } - function save() { if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; - // Handle selection of no fall-back language (should pass null) - if (!vm.language.fallbackLanguage.id) { - vm.language.fallbackLanguage = null; - } - - // We need to attach the ISO code to the fall-back language to pass - // server-side validation. - if (vm.language.fallbackLanguage) { - setCultureForFallbackLanguage(vm.language.fallbackLanguage); - } - languageResource.save(vm.language).then(function (lang) { formHelper.resetForm({ scope: $scope }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 5570b901e2..a2217a6649 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -67,7 +67,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index c8a728d3aa..a5c446dfb5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -13,6 +13,16 @@ vm.editLanguage = editLanguage; vm.deleteLanguage = deleteLanguage; + vm.getLanguageById = function(id) { + for (var i = 0; i < vm.languages.length; i++) { + if (vm.languages[i].id === id) { + return vm.languages[i]; + } + } + + return null; + }; + function init() { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index f53326a491..3b75fa62bd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -36,7 +36,7 @@ - {{vm.labels.general}} {{vm.labels.mandatory}} - {{vm.labels.fallsbackTo}}: {{language.fallbackLanguage.name}} + {{vm.labels.fallsbackTo}}: {{vm.getLanguageById(language.fallbackLanguageId).name}} x.FallbackLanguage?.Id == language.Id)) + if (langs.Any(x => x.FallbackLanguageId.HasValue && x.FallbackLanguageId.Value == language.Id)) { var message = $"Language '{language.CultureName}' is defined as a fall-back language for one or more other languages, and so cannot be deleted."; throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); @@ -147,20 +147,21 @@ namespace Umbraco.Web.Editors CultureName = culture.DisplayName, IsDefaultVariantLanguage = language.IsDefaultVariantLanguage, Mandatory = language.Mandatory, + FallbackLanguageId = language.FallbackLanguageId }; - AssociateFallbackLanguage(language, newLang); Services.LocalizationService.Save(newLang); return Mapper.Map(newLang); } found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; - AssociateFallbackLanguage(language, found); + found.FallbackLanguageId = language.FallbackLanguageId; - if (DoesUpdatedFallbackLanguageCreateACircularPath(found)) + string selectedFallbackLanguageCultureName; + if (DoesUpdatedFallbackLanguageCreateACircularPath(found, out selectedFallbackLanguageCultureName)) { - ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); + ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + selectedFallbackLanguageCultureName + "' would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } @@ -168,43 +169,35 @@ namespace Umbraco.Web.Editors return Mapper.Map(found); } - private static void AssociateFallbackLanguage(Language submittedLanguage, ILanguage languageToCreateOrUpdate) + private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language, out string selectedFallbackLanguageCultureName) { - if (submittedLanguage.FallbackLanguage == null) - { - languageToCreateOrUpdate.FallbackLanguage = null; - return; - } - - var fallbackLanguageCulture = CultureInfo.GetCultureInfo(submittedLanguage.FallbackLanguage.IsoCode); - languageToCreateOrUpdate.FallbackLanguage = new Core.Models.Language(fallbackLanguageCulture.Name) - { - Id = submittedLanguage.FallbackLanguage.Id, - CultureName = fallbackLanguageCulture.DisplayName - }; - } - - private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language) - { - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { + selectedFallbackLanguageCultureName = string.Empty; return false; } var languages = Services.LocalizationService.GetAllLanguages().ToArray(); - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { - if (fallbackLanguage.Id == language.Id) + if (fallbackLanguageId.Value == language.Id) { // We've found the current language in the path of fall back languages, so we have a circular path. + selectedFallbackLanguageCultureName = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).CultureName; return true; } - fallbackLanguage = languages.Single(x => x.Id == fallbackLanguage.Id).FallbackLanguage; + fallbackLanguageId = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).FallbackLanguageId; } + selectedFallbackLanguageCultureName = string.Empty; return false; } + + private static ILanguage GetLanguageFromCollectionById(IEnumerable languages, int id) + { + return languages.Single(x => x.Id == id); + } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index 309e111e32..7693ee836e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "isMandatory")] public bool Mandatory { get; set; } - [DataMember(Name = "fallbackLanguage")] - public Language FallbackLanguage { get; set; } + [DataMember(Name = "fallbackLanguageId")] + public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index b6dc9f4244..d6e8db83f4 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -107,13 +107,6 @@ namespace Umbraco.Web.Models.PublishedContent return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); } - private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) - { - return value != null && - string.IsNullOrEmpty(value.ToString()) == false && - value.Equals(defaultValue) == false; - } - private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) { if (string.IsNullOrEmpty(culture)) @@ -123,22 +116,23 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { + var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue); if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) { return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; @@ -154,22 +148,23 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { + var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue); if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) { return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; @@ -185,34 +180,39 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { + var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse); if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) { return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; return false; } - private ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage) + private ILanguage GetLanguageById(int id) { - // Ensure reference to next fall-back language is loaded if it exists - fallbackLanguage = _localizationService.GetLanguageById(fallbackLanguage.Id); + return _localizationService.GetLanguageById(id); + } - return fallbackLanguage.FallbackLanguage; + private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) + { + return value != null && + string.IsNullOrEmpty(value.ToString()) == false && + value.Equals(defaultValue) == false; } } } From 53cb9612348bbde95f8d9f0797faa049d64483f1 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Mon, 16 Jul 2018 14:36:15 +1000 Subject: [PATCH 019/310] for discussion... --- .../src/less/application/grid.less | 4 - src/Umbraco.Web.UI.Client/src/less/belle.less | 8 +- .../src/less/components/tree/umb-actions.less | 102 ++++ .../less/components/tree/umb-tree-item.less | 75 +++ .../less/components/tree/umb-tree-root.less | 27 + .../src/less/components/tree/umb-tree.less | 378 ++++++++++++ src/Umbraco.Web.UI.Client/src/less/tree.less | 553 ------------------ .../src/views/components/tree/umb-tree.html | 10 +- 8 files changed, 594 insertions(+), 563 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less delete mode 100644 src/Umbraco.Web.UI.Client/src/less/tree.less diff --git a/src/Umbraco.Web.UI.Client/src/less/application/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less index 4448bf2192..7ed2abc898 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -124,10 +124,6 @@ body.umb-drawer-is-visible #mainwrapper{ height: 100%; } -#tree .umb-tree { - padding: 0px 0px 20px 0px; -} - #search-results { z-index: 200; } diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index b18f2c942f..7122bad1fb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -71,8 +71,7 @@ @import "sections.less"; @import "helveticons.less"; @import "main.less"; -@import "tree.less"; -@import "listview.less"; +@import "listview.less"; @import "gridview.less"; @import "footer.less"; @import "dragdrop.less"; @@ -92,6 +91,11 @@ @import "components/html/umb-expansion-panel.less"; @import "components/html/umb-alert.less"; +@import "components/tree/umb-tree.less"; +@import "components/tree/umb-tree-root.less"; +@import "components/tree/umb-actions.less"; +@import "components/tree/umb-tree-item.less"; + @import "components/editor.less"; @import "components/overlays.less"; @import "components/card.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less new file mode 100644 index 0000000000..aca814b8dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less @@ -0,0 +1,102 @@ +// Tree context menu +// ------------------------- +.umb-actions { + margin: 0; + padding: 0px; + list-style: none; + user-select: none; + + li.sep { + display: block; + border-top: 1px solid @gray-9; + + &:first-child { + border-top: none; + } + } + + a { + white-space: nowrap; + font-size: 15px; + color: @black; + padding: 9px 25px 9px 20px; + text-decoration: none; + cursor: pointer; + display: flex; + align-items: center; + + body.touch & { + padding: 7px 25px 7px 20px; + font-size: 110%; + } + } + + a:hover, + a:focus, + li.selected { + color: @black !important; + background: @gray-10 !important; + } + + .menu-label { + display: inline-block; + vertical-align: middle; + padding-left: 15px; + } + + i { + font-size: 18px; + vertical-align: middle; + color: @gray-3; + } +} + +.umb-actions-child { + + li { + display: block; + + &.add { + margin-top: 20px; + border-top: 1px solid @gray-8; + padding-top: 20px; + + i { + opacity: 0.4; + } + } + + .menu-label { + font-size: 14px; + color: @black; + margin-left: 10px; + + small { + font-size: 12px; + display: block; + clear: right; + line-height: 14px; + color: @gray-6; + white-space: normal; + margin-top: 2px; + } + } + } + + a { + clear: both; + padding-left: 10px; + + &:hover .menuLabel small { + text-decoration: none !important + } + } + + i { + font-size: 30px; + min-width: 30px; + text-align: center; + line-height: 24px; + /* set line-height to ensure all icons use same line-height */ + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less new file mode 100644 index 0000000000..d650915eab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less @@ -0,0 +1,75 @@ +.umb-tree-item { + display: block; + min-width: 100%; + width: auto; + + &:hover ins { + visibility: visible; + cursor: pointer + } + + > div { + + &:hover { + a:not(.umb-options) { + overflow: hidden; + margin-right: 6px; + } + } + + // Loading Animation + // ------------------------ + &.l { + width: 100%; + height: 1px; + overflow: hidden; + position: absolute; + left: 0; + bottom: 0; + + div { + .umb-loader; + } + } + + a:not(.umb-options) { + padding: 11px 0 7px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 0 auto; + } + } + + &.current > div { + + background: @turquoise-d1; + + // override small icon color + // todo - check usage + &:before { + color: @turquoise-l2; + } + + .umb-options { + + &:hover i { + opacity: .7; + } + + i { + background: @white; + border-color: @turquoise-d1; + transition: opacity 120ms ease; + } + } + + a, + i.icon, + ins { + color: @white !important; + background-color: @turquoise-d1; + border-color: @turquoise-d1; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less new file mode 100644 index 0000000000..f6c19c1dbb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less @@ -0,0 +1,27 @@ +.umb-tree-root { + height:@editorHeaderHeight; + + div& { + align-items:inherit; + } + + &-link { + display: flex; + align-items:center; + width:100%; + padding:5px 0 0 20px; + color:@gray-2; + } + + h5, + h6 { + margin: 0; + width: 100%; + display: flex; + color: @gray-2; + } + + .umb-options { + align-self:center; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less new file mode 100644 index 0000000000..815e430eb9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -0,0 +1,378 @@ +// Tree +// ------------------------- +.umb-tree { + margin: 0; + min-width: 100%; + width: auto; + padding: 0 0 20px; + + * { + white-space: nowrap; + } + + div { + padding: 0; + position: relative; + overflow: hidden; + display: flex; + flex-wrap: nowrap; + align-items: center; + + &:hover { + background: @gray-10; + + > .umb-options { + visibility: visible; + } + } + } + + a, a:hover { + outline: none; + text-decoration: none; + + &.noSpr { + background-position: 0 + } + } + + ins { + margin: -4px 0 0 -16px; + width: 16px; + height: 16px; + visibility: hidden; + text-decoration: none; + font-size: 12px; + transition: opacity 120ms ease; + + &:hover { + opacity: .7; + } + } + + i.noSpr { + display: inline-block; + margin-top: 1px; + width: 16px; + height: 16px; + line-height: 16px; + } + + ul { + padding: 0; + margin: 0; + min-width: 100%; + width: 100%; + + &.collapsed { + display: none; + } + } // .umb-tree-item { + // display: block; + // min-width: 100%; + // width: auto; + // } + // + // li.current > div, + // div.selected { + // background: @turquoise-d1; + // } + // + // li.current > div a.umb-options i, + // div.selected i { + // background: @white; + // border-color: @turquoise-d1; + // transition: opacity 120ms ease; + // } + // + // li.current > div a.umb-options:hover i, + // div.selected i { + // opacity: .7; + // } + // + // li.current > div a, + // li.current > div i.icon, + // li.current > div ins { + // color: @white !important; + // background-color: @turquoise-d1; + // border-color: @turquoise-d1; + // } + // + //loader defaults + .umb-loader { + height: 10px; + margin: 10px 10px 10px 10px; + } + + .search-subtitle { + color: @gray-7; + display: block; + padding-left: 35px; + } +} + +body.touch .umb-tree { + ins { + font-size: 14px; + visibility: visible; + padding: 7px; + } + + .umb-tree-item > div { + padding-top: 8px; + padding-bottom: 8px; + font-size: 110%; + } + + // change height of this if touch devices should have a different height of preloader. + .umb-tree-item .l div { + padding: 0; + } +} + +.umb-tree-header { + display: flex; + padding: 20px 0 20px 20px; + box-sizing: border-box; + color: @gray-2; + font-weight: bold; + font-size: 15px; +} + +.umb-tree-icon, +.umb-tree-node-search { + cursor: pointer; +} + +.umb-tree .umb-search-group { + position: inherit; + display: inherit; + + h6 { + padding: 10px 0 10px 20px; + font-weight: inherit; + background: @gray-10; + font-size: 14px; + } + + &:hover { + background: inherit; + } + + &-item { + padding-left: 20px; + } + + &-link { + display: flex; + flex-wrap: wrap; + flex-direction: column; + font-weight: normal !important; + } +} + +.umb-tree .umb-tree-node-checked i[class^="icon-"], +.umb-tree .umb-tree-node-checked i[class*=" icon-"] { + font-family: 'icomoon' !important; + color: @green !important; + + &::before { + content: "\e165" !important; + font-family: inherit; + } +} + +.umb-options { + visibility: hidden; + display: flex; + flex: 0 0 auto; + justify-content: flex-end; + padding: 9px 5px; + text-align: center; + margin: 0 10px 0 auto; + cursor: pointer; + border-radius: @baseBorderRadius; + + &:hover { + background: @btnBackgroundHighlight; + } + + i { + height: 5px !important; + width: 5px !important; + border-radius: 20px; + background: @black; + display: inline-block; + margin: 0 2px 0 0; + + &:last-child { + margin: 0; + } + } + + .hide-options & { + display: none !important; + } +} + +// todo -> confirm not in use. not referenced in any other code +// +//.hide-header h5 { +// display: none !important +//} +//.umb-icon-item { +// padding: 2px; +// padding-left: 55px; +// display: block; +// position: relative; +// +// &:hover { +// background: @gray-10; +// +// .umb-options { +// visibility: visible +// } +// } +// +// a { +// color: @gray-3; +// padding-top: 3px; +// height: 15px; +// font-size: 12px; +// text-decoration: none; +// +// &:hover div { +// text-decoration: underline; +// } +// } +// +// small { +// color: @gray-6; +// font-size: 10px; +// display: block; +// } +// +// .icon { +// position: absolute; +// top: 8px; +// left: 19px; +// } +// +// .umb-spr { +// float: left +// } +//} + +// item-list +// todo -> verify not in use. not mentioned elsewhere in codebase... +// ------------------------- +//.umb-item-list { +// margin: 0; +// width: auto; +// display: block; +// +// li { +// display: block; +// width: auto; +// } +//} +// Tree item states +// ------------------------- +.not-published { + > i.icon, + a { + opacity: 0.6; + } +} + +.not-allowed { + > i.icon, + a { + cursor: not-allowed; + } +} + +.protected, +.has-unpublished-version, +.is-container, +.locked { + &::before { + font-family: 'icomoon'; + position: absolute; + font-size: 20px; + padding-left: 7px; + padding-top: 7px; + bottom: 0; + } +} + +.protected { + &::before { + content: "\e256"; + color: @red; + } +} + +.has-unpublished-version { + &::before { + content: "\e25a"; + color: @green; + } +} + +.is-container { + &::before { + content: "\e04e"; + color: @turquoise; + font-size: 8px; + padding-left: 13px; + padding-top: 8px; + pointer-events: none; + } +} + +.locked { + &::before { + content: "\e0a7"; + color: @red; + } +} + +.no-access { + .umb-tree-icon, + .root-link, + .umb-tree-item__label { + color: @gray-7; + cursor: not-allowed; + } +} + + +// Tree icons +// ------------------------- +.umb-tree-icon { + vertical-align: middle; + margin: 0 13px 0 0; + color: @gray-1; + font-size: 20px; + + &.blue { + color: @blue; + } + + &.green { + color: @green; + } + + &.purple { + color: @purple; + } + + &.orange { + color: @orange; + } + + &.red { + color: @red; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less deleted file mode 100644 index d5ef7ccec5..0000000000 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ /dev/null @@ -1,553 +0,0 @@ -// item-list -// ------------------------- - - -.umb-item-list { - margin: 0; - width: auto; - display: block -} -.umb-item-list li { - display: block; - width: auto; -} - - - - -// Tree -// ------------------------- - -.umb-tree { - margin: 0; - min-width: 100%; - width: auto; -} - -.umb-tree li { - display: block; - min-width: 100%; - width: auto; -} -.umb-tree li.current > div, -.umb-tree div.selected { - background: @turquoise-d1; -} -.umb-tree li.current > div a.umb-options i, -.umb-tree div.selected i { - background: @white; - border-color: @turquoise-d1; - transition: opacity 120ms ease; -} - -.umb-tree li.current > div a.umb-options:hover i, -.umb-tree div.selected i { - opacity: .7; -} - -.umb-tree li.current > div a, -.umb-tree li.current > div i.icon, -.umb-tree li.current > div ins { - color: @white !important; - background-color: @turquoise-d1; - border-color: @turquoise-d1; -} - -.umb-tree li.root > div:first-child { - padding: 0; -} - -.umb-tree li.root > div h5, .umb-tree li.root > div h6 { - margin: 0; - width: 100%; - display: flex; - align-items: center; -} - -.umb-tree li.root > div:first-child h5 > a, .umb-tree-header { - display: flex; - padding: 20px 0 20px 20px; - box-sizing: border-box; -} - -.umb-tree * { - white-space: nowrap -} -.umb-tree ul { - padding: 0; - margin: 0; - min-width: 100%; - width: 100%; - //display: table -} - -.umb-tree ul.collapsed { - display:none; -} - -.umb-tree a { - cursor:pointer; - text-decoration: none; - outline: none; -} - -.umb-tree a:hover { - text-decoration: none -} - -/*.umb-tree div.tree-node { - padding: 5px 0 5px 0; - position: relative; - overflow: hidden; - display: flex; - flex-wrap: nowrap; - align-items: center; -}*/ - -.umb-tree div { - padding: 5px 0 5px 0; - position: relative; - overflow: hidden; - display: flex; - flex-wrap: nowrap; - align-items: center; -} - -.umb-tree a.noSpr { - background-position: 0 -} - -.umb-tree div > a.umb-options { - visibility: hidden; - flex: 0 0 auto; - margin-left: auto; -} - -.umb-tree div:hover > a.umb-options { - visibility: visible; -} - -.umb-tree li.root > div a, -.umb-tree li.root h5, .umb-tree-header { - color: @gray-2; - font-weight: bold; - font-size: 15px; -} - -.umb-tree ins { - margin: -4px 0 0 -16px; - width: 16px; - height: 16px; - visibility: hidden; - text-decoration: none; - font-size: 12px; - transition: opacity 120ms ease; -} - -.umb-tree ins:hover { - opacity: .7; -} - -.umb-tree li:hover ins { - visibility: visible; - cursor: pointer -} - -.umb-tree li div { - padding: 0; -} - -.umb-tree li > div a:not(.umb-options) { - padding: 6px 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1 0 auto; -} - -.umb-tree li > div:hover a:not(.umb-options) { - overflow: hidden; - margin-right: 6px; -} - -.umb-tree .icon { - vertical-align: middle; - margin: 0 13px 0 0; - color: @gray-1; - font-size: 20px; -} - -.umb-tree-icon { - cursor: pointer; -} - -.umb-tree i.noSpr { - display: inline-block; - margin-top: 1px; - width: 16px; - height: 16px; - line-height: 16px; -} - -.umb-tree div:hover { - background: @gray-10; -} - -.umb-tree small.search-subtitle{ - color: @gray-7; - display: block; - padding-left: 35px; -} - -.umb-tree .umb-tree-node-search { - cursor:pointer; - /*color:@turquoise;*/ -} - -.umb-tree div.umb-search-group { - position: inherit; - display: inherit; -} - -.umb-tree div.umb-search-group:hover { - background: inherit; -} -.umb-tree div.umb-search-group h6 { - /*color: @gray-5;*/ - padding: 10px 0 10px 20px; - font-weight: inherit; - background: @gray-10; - font-size: 14px; - font-weight: bold; -} - -.umb-tree .umb-search-group-item { - padding-left: 20px; -} - -.umb-tree .umb-search-group-item-link { - display: flex; - flex-wrap: wrap; - flex-direction: column; - font-weight: normal !important; -} - -.icon-check:before { - content: "\e165"; -} - -.umb-tree .umb-tree-node-checked i[class^="icon-"], -.umb-tree .umb-tree-node-checked i[class*=" icon-"] { - font-family: 'icomoon' !important; - color:@green !important; -} -.umb-tree .umb-tree-node-checked i:before { - /*check box*/ - content: "\e165" !important; - font-family: inherit; -} - -a.umb-options { - visibility: hidden; - display: flex; - justify-content: flex-end; - padding: 9px 5px; - text-align: center; - cursor: pointer; - margin-right: 10px; -} - -a.umb-options i { - height: 5px !important; - width: 5px !important; - border-radius: 20px; - background: @black; - display: inline-block; - margin: 0 2px 0 0; -} - -a.umb-options i:last-child { - margin: 0; -} - -a.umb-options:hover { - background: @btnBackgroundHighlight; - .border-radius(@baseBorderRadius); -} - -li.root > div > a.umb-options { - top: 18px; - display: flex; - padding: 10px 5px; -} - -.hide-options a.umb-options{display: none !important} -.hide-header h5{display: none !important} - - -.umb-icon-item { - padding: 2px; - padding-left: 55px; - display: block; - position: relative; -} - -.umb-icon-item:hover { - background: @gray-10; -} -.umb-icon-item i.icon { - position: absolute; - top: 8px; - left: 19px; -} -.umb-icon-item a:hover div { - text-decoration: underline; -} - -.umb-icon-item a { - color: @gray-3; - padding-top: 3px; - height: 15px; - font-size: 12px; - text-decoration: none; -} -.umb-icon-item small { - color: @gray-6; - font-size: 10px; - display: block -} -.umb-icon-item:hover a.umb-options { - visibility: visible -} -.umb-icon-item .umb-spr { - float: left -} - - - -// Tree item states -// ------------------------- -div.not-published > i.icon,div.not-published > a{ - opacity: 0.6; -} -div.protected:before{ - content:"\e256"; - font-family: 'icomoon'; - color: @red; - position: absolute; - font-size: 20px; - padding-left: 7px; - padding-top: 7px; - bottom: 0; -} - -div.has-unpublished-version:before{ - content:"\e25a"; - font-family: 'icomoon'; - color: @green; - position: absolute; - font-size: 20px; - padding-left: 7px; - padding-top: 7px; - bottom: 0; -} - -div.not-allowed > i.icon,div.not-allowed > a{ - cursor: not-allowed; -} - -// override small icon color -.umb-tree li.current > div:before { - color: @turquoise-l2; -} -div.is-container:before{ - content:"\e04e"; - font-family: 'icomoon'; - color: @turquoise; - position: absolute; - font-size: 8px; - padding-left: 13px; - padding-top: 8px; - pointer-events: none; - bottom: 0; -} - -div.locked:before{ - content:"\e0a7"; - font-family: 'icomoon'; - color: @red; - position: absolute; - font-size: 20px; - padding-left: 7px; - padding-top: 7px; - bottom: 0; -} - -.umb-tree li div.no-access .umb-tree-icon, -.umb-tree li div.no-access .root-link, -.umb-tree li div.no-access .umb-tree-item__label { - color: @gray-7; - cursor: not-allowed; -} - -// Tree context menu -// ------------------------- -.umb-actions { - margin: 0; - padding: 0px; - list-style: none; - user-select: none; -} - -.umb-actions li.sep { - display: block; - border-top: 1px solid @gray-9; -} - -.umb-actions li.sep:first-child { - border-top: none; -} - -.umb-actions a { - white-space: nowrap; - display: block; - font-size: 15px; - color: @black; - padding: 9px 25px 9px 20px; - text-decoration: none; - cursor: pointer; - display: flex; - align-items: center; -} - -.umb-actions a:hover, .umb-actions a:focus, -.umb-actions li.selected { - color: @black !important; - background: @gray-10 !important; -} - -.umb-actions .menu-label { - display: inline-block; - vertical-align: middle; - padding-left: 15px; -} - -.umb-actions i { - color: @gray-6; - font-size: 18px; - vertical-align: middle; - color: @gray-3; -} - -.umb-actions-child { - list-style: none; - display: block; - margin: 0px; -} - -.umb-actions-child li { - display: block; -} - -.umb-actions-child a { - display: block; - clear: both; - text-decoration: none; - padding-left: 10px; -} -.umb-actions-child li .menu-label { - font-size: 14px; - color: @black; - margin-left: 10px; -} - -.umb-actions-child li .menu-label small { - font-size: 12px; - display: block; - clear: right; - line-height: 14px; - color: @gray-6; - white-space: normal; - margin-top: 2px; -} -.umb-actions-child li a:hover .menuLabel small { - text-decoration: none !important -} -.umb-actions-child i { - font-size: 30px; - min-width: 30px; - text-align: center; - line-height: 24px; /* set line-height to ensure all icons use same line-height */ -} - -.umb-actions-child li.add { - margin-top: 20px; - border-top: 1px solid @gray-8; - padding-top: 20px; -} -.umb-actions-child li.add i { - opacity: 0.4; -} - - -// Tree icon colors -// ------------------------- - -.umb-tree i.icon.blue { - color: @blue; -} -.umb-tree i.icon.green { - color: @green; -} -.umb-tree i.icon.purple { - color: @purple; -} -.umb-tree i.icon.orange { - color: @orange; -} -.umb-tree i.icon.red { - color: @red; -} - - - - -// Loading Animation -// ------------------------ - -.umb-tree li div.l{ - width:100%; - height:1px; - overflow:hidden; - position: absolute; - left: 0; - bottom: 0; -} - -.umb-tree li div.l div { - .umb-loader; -} - -//loader defaults -.umb-tree .umb-loader{ - height: 10px; margin: 10px 10px 10px 10px; -} - - -/*body.touch .umb-tree .icon{font-size: 19px;}*/ -body.touch .umb-tree ins{font-size: 14px; visibility: visible; padding: 7px;} -body.touch .umb-tree li > div { - padding-top: 8px; - padding-bottom: 8px; - font-size: 110%; -} - -// change height of this if touch devices should have a different height of preloader. -body.touch .umb-tree li div.l div { - padding: 0; -} - -body.touch .umb-actions a { - padding: 7px 25px 7px 20px; - font-size: 110%; -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html index a2caa245ad..74c1dc2c99 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html @@ -1,13 +1,15 @@
    -
  • -
    +
  • + Date: Wed, 18 Jul 2018 08:47:04 +1000 Subject: [PATCH 020/310] adjust tree node heights when language picker is rendered --- .../components/application/umb-language-picker.less | 10 ++++++++-- src/Umbraco.Web.UI.Client/src/less/main.less | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less index 5cf9ca21b3..79aa92f58b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less @@ -1,6 +1,12 @@ .umb-language-picker { position: relative; - z-index: @zindexDropdown; + z-index: @zindexDropdown; + + // align bottom of tree root with bottom of first tab header in the editor + // means that there's a hierarchy in the element heights in the left panel, based on their height + ~ #tree .umb-tree-root { + height:60px; + } } .umb-language-picker__toggle { @@ -10,7 +16,7 @@ padding: 0 20px; cursor: pointer; border-bottom: 1px solid @gray-9; - height: 50px; + height: @editorHeaderHeight; // match height to editor header box-sizing: border-box; } diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index d869d1d9af..225157633f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -116,7 +116,7 @@ h5.-black { /* FORM GRID */ .umb-pane { - margin: 30px 20px; + margin: 20px; } .umb-control-group { border-bottom: 1px solid @gray-10; From b1ef9c5aa81a20dcf7f1e14901d3556e75254dc5 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 18 Jul 2018 12:27:14 +0200 Subject: [PATCH 021/310] U4-11502 misc fixes --- src/Umbraco.Core/Models/ILanguage.cs | 32 ++++-- src/Umbraco.Core/Models/Language.cs | 23 ++-- .../IPublishedValueFallback.cs | 103 ++++++++++++++++-- .../Persistence/Dtos/LanguageDto.cs | 18 ++- .../Persistence/Factories/LanguageFactory.cs | 4 +- .../Implement/LanguageRepository.cs | 23 ++-- .../Services/Implement/ContentService.cs | 2 +- .../Repositories/LanguageRepositoryTest.cs | 22 ++-- ...> PublishedContentLanguageVariantTests.cs} | 4 +- .../Services/ContentServiceTests.cs | 6 +- .../Services/LocalizationServiceTests.cs | 10 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 2 +- src/Umbraco.Web/Editors/LanguageController.cs | 22 ++-- .../Models/ContentEditing/Language.cs | 5 +- .../ContentItemDisplayVariationResolver.cs | 4 +- 16 files changed, 188 insertions(+), 94 deletions(-) rename src/Umbraco.Tests/PublishedContent/{PublishedContentLanuageVariantTests.cs => PublishedContentLanguageVariantTests.cs} (97%) diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index 8d1c092e13..c0d2fed839 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -4,40 +4,54 @@ using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { + /// + /// Represents a language. + /// public interface ILanguage : IEntity, IRememberBeingDirty { /// - /// Gets or sets the Iso Code for the Language + /// Gets or sets the ISO code of the language. /// [DataMember] string IsoCode { get; set; } /// - /// Gets or sets the Culture Name for the Language + /// Gets or sets the culture name of the language. /// [DataMember] string CultureName { get; set; } /// - /// Returns a object for the current Language + /// Gets the object for the language. /// [IgnoreDataMember] CultureInfo CultureInfo { get; } /// - /// Defines if this language is the default variant language when language variants are in use + /// Gets or sets a value indicating whether the language is the default language. /// - bool IsDefaultVariantLanguage { get; set; } + [DataMember] + bool IsDefault { get; set; } /// - /// If true, a variant node cannot be published unless this language variant is created + /// Gets or sets a value indicating whether the language is mandatory. /// - bool Mandatory { get; set; } + /// + /// When a language is mandatory, a multi-lingual document cannot be published + /// without that language being published, and unpublishing that language unpublishes + /// the entire document. + /// + [DataMember] + bool IsMandatory { get; set; } /// - /// Defines the id of a fallback language that can be used in multi-lingual scenarios to provide - /// content if the requested language does not have it published. + /// Gets or sets the identifier of a fallback language. /// + /// + /// The fallback language can be used in multi-lingual scenarios, to help + /// define fallback strategies when a value does not exist for a requested language. + /// + [DataMember] int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 5fcd5cd50e..940648c4b9 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -31,14 +31,12 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo IsoCodeSelector = ExpressionHelper.GetPropertyInfo(x => x.IsoCode); public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); - public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); - public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); + public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefault); + public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.IsMandatory); public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguageId); } - /// - /// Gets or sets the Iso Code for the Language - /// + /// [DataMember] public string IsoCode { @@ -46,9 +44,7 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _isoCode, Ps.Value.IsoCodeSelector); } - /// - /// Gets or sets the Culture Name for the Language - /// + /// [DataMember] public string CultureName { @@ -56,24 +52,25 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector); } - /// - /// Returns a object for the current Language - /// + /// [IgnoreDataMember] public CultureInfo CultureInfo => CultureInfo.GetCultureInfo(IsoCode); - public bool IsDefaultVariantLanguage + /// + public bool IsDefault { get => _isDefaultVariantLanguage; set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, Ps.Value.IsDefaultVariantLanguageSelector); } - public bool Mandatory + /// + public bool IsMandatory { get => _mandatory; set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } + /// public int? FallbackLanguageId { get => _fallbackLanguageId; diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index f154d9ef27..afa70fbd47 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -2,6 +2,8 @@ namespace Umbraco.Core.Models.PublishedContent { + // fixme document + // fixme add values? public enum PublishedValueFallbackPriority { RecursiveTree, @@ -16,28 +18,105 @@ namespace Umbraco.Core.Models.PublishedContent // todo - understand caching vs fallback (recurse etc) public interface IPublishedValueFallback { - // note that at property level, property.GetValue() does NOT implement fallback, and one has - // to get property.Value() or property.Value() to trigger fallback - - // this method is called whenever property.Value(culture, segment, defaultValue) is called, and - // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). - + /// + /// Gets a fallback value for a property. + /// + /// The property. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// It can only fallback at property level (no recurse). + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); - // this method is called whenever property.Value(culture, segment, defaultValue) is called, and - // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). - + /// + /// Gets a fallback value for a property. + /// + /// The type of the value. + /// The property. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// It can only fallback at property level (no recurse). + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); - // these methods to be called whenever getting the property value for the specified alias, culture and segment, - // either returned no property at all, or a property that does not HasValue for the specified culture and segment. - + /// + /// Gets a fallback value for a published element property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// It can only fallback at element level (no recurse). + /// object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); + /// + /// Gets a fallback value for a published element property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// It can only fallback at element level (no recurse). + /// T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); + /// + /// Gets a fallback value for a published content property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// fixme explain & document priority + merge w/recurse? + /// object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); + /// + /// Gets a fallback value for a published content property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// fixme explain & document priority + merge w/recurse? + /// T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); } } diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index f87930269a..f389ab78c3 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -10,38 +10,46 @@ namespace Umbraco.Core.Persistence.Dtos { public const string TableName = Constants.DatabaseSchema.Tables.Language; + /// + /// Gets or sets the identifier of the language. + /// [Column("id")] [PrimaryKeyColumn(IdentitySeed = 2)] public short Id { get; set; } + /// + /// Gets or sets the ISO code of the language. + /// [Column("languageISOCode")] [Index(IndexTypes.UniqueNonClustered)] [NullSetting(NullSetting = NullSettings.Null)] [Length(10)] public string IsoCode { get; set; } + /// + /// Gets or sets the culture name of the language. + /// [Column("languageCultureName")] [NullSetting(NullSetting = NullSettings.Null)] [Length(100)] public string CultureName { get; set; } /// - /// Defines if this language is the default variant language when language variants are in use + /// Gets or sets a value indicating whether the language is the default language. /// [Column("isDefaultVariantLang")] [Constraint(Default = "0")] public bool IsDefaultVariantLanguage { get; set; } /// - /// If true, a variant node cannot be published unless this language variant is created + /// Gets or sets a value indicating whether the language is mandatory. /// [Column("mandatory")] [Constraint(Default = "0")] - public bool Mandatory { get; set; } + public bool IsMandatory { get; set; } /// - /// Defines the fallback language that can be used in multi-lingual scenarios to provide - /// content if the requested language does not have it published. + /// Gets or sets the identifier of a fallback language. /// [Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index 7ab36d15d6..db2927eea3 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Factories { public static ILanguage BuildEntity(LanguageDto dto) { - var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, Mandatory = dto.Mandatory, FallbackLanguageId = dto.FallbackLanguageId }; + var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefault = dto.IsDefaultVariantLanguage, IsMandatory = dto.IsMandatory, FallbackLanguageId = dto.FallbackLanguageId }; // reset dirty initial properties (U4-1946) lang.ResetDirtyProperties(false); return lang; @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence.Factories public static LanguageDto BuildDto(ILanguage entity) { - var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory, FallbackLanguageId = entity.FallbackLanguageId }; + var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefault, IsMandatory = entity.IsMandatory, FallbackLanguageId = entity.FallbackLanguageId }; if (entity.HasIdentity) { dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 787cbc1690..4753b131fe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -53,12 +53,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get languages var dtos = Database.Fetch(sql); - var languages = dtos.Select(ConvertFromDto).ToList(); + var languages = dtos.Select(ConvertFromDto).ToList(); // fixme - .OrderBy(x => x.Id) is gone? // fix inconsistencies: there has to be a default language, and it has to be mandatory - var defaultLanguage = languages.FirstOrDefault(x => x.IsDefaultVariantLanguage) ?? languages.First(); - defaultLanguage.IsDefaultVariantLanguage = true; - defaultLanguage.Mandatory = true; + var defaultLanguage = languages.FirstOrDefault(x => x.IsDefault) ?? languages.First(); + defaultLanguage.IsDefault = true; + defaultLanguage.IsMandatory = true; // initialize the code-id map lock (_codeIdMap) @@ -122,10 +122,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return list; } - protected override Guid NodeObjectTypeId - { - get { throw new NotImplementedException(); } - } + protected override Guid NodeObjectTypeId => throw new NotImplementedException(); #endregion @@ -138,7 +135,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ((EntityBase)entity).AddingEntity(); - if (entity.IsDefaultVariantLanguage) + if (entity.IsDefault) { //if this entity is flagged as the default, we need to set all others to false Database.Execute(Sql().Update(u => u.Set(x => x.IsDefaultVariantLanguage, false))); @@ -161,14 +158,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ((EntityBase)entity).UpdatingEntity(); - if (entity.IsDefaultVariantLanguage) + if (entity.IsDefault) { //if this entity is flagged as the default, we need to set all others to false Database.Execute(Sql().Update(u => u.Set(x => x.IsDefaultVariantLanguage, false))); //We need to clear the whole cache since all languages will be updated IsolatedCache.ClearAllCache(); } - + var dto = LanguageFactory.BuildDto(entity); Database.Update(dto); @@ -183,7 +180,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistDeletedItem(ILanguage entity) { //we need to validate that we can delete this language - if (entity.IsDefaultVariantLanguage) + if (entity.IsDefault) throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})"); var count = Database.ExecuteScalar(Sql().SelectCount().From()); @@ -268,7 +265,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var language in all) { // if one language is default, return - if (language.IsDefaultVariantLanguage) + if (language.IsDefault) return language; // keep track of language with lowest id if (first == null || language.Id < first.Id) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 30e76468a6..b6d3fece19 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1082,7 +1082,7 @@ namespace Umbraco.Core.Services.Implement var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published if (!cannotBePublished) { - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode); + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published } diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index a63bf5e08d..68d1f71ea8 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -191,8 +191,8 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert Assert.That(languageBR.HasIdentity, Is.True); Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 - Assert.IsFalse(languageBR.IsDefaultVariantLanguage); - Assert.IsFalse(languageBR.Mandatory); + Assert.IsFalse(languageBR.IsDefault); + Assert.IsFalse(languageBR.IsMandatory); Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -207,14 +207,14 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var languageBR = new Language("pt-BR") { CultureName = "pt-BR", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageBR = new Language("pt-BR") { CultureName = "pt-BR", IsDefault = true, IsMandatory = true }; repository.Save(languageBR); // Assert Assert.That(languageBR.HasIdentity, Is.True); Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 - Assert.IsTrue(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageBR.Mandatory); + Assert.IsTrue(languageBR.IsDefault); + Assert.IsTrue(languageBR.IsMandatory); Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -252,22 +252,22 @@ namespace Umbraco.Tests.Persistence.Repositories { var repository = CreateRepository(provider); - var languageBR = (ILanguage)new Language("pt-BR") { CultureName = "pt-BR", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageBR = (ILanguage)new Language("pt-BR") { CultureName = "pt-BR", IsDefault = true, IsMandatory = true }; repository.Save(languageBR); var languageEN = new Language("en-AU") { CultureName = "en-AU" }; repository.Save(languageEN); - Assert.IsTrue(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageBR.Mandatory); + Assert.IsTrue(languageBR.IsDefault); + Assert.IsTrue(languageBR.IsMandatory); // Act - var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefault = true, IsMandatory = true }; repository.Save(languageNZ); languageBR = repository.Get(languageBR.Id); // Assert - Assert.IsFalse(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageNZ.IsDefaultVariantLanguage); + Assert.IsFalse(languageBR.IsDefault); + Assert.IsTrue(languageNZ.IsDefault); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs similarity index 97% rename from src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs rename to src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 22eb4bd799..7f9e7ad954 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -14,7 +14,7 @@ namespace Umbraco.Tests.PublishedContent { [TestFixture] [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] - public class PublishedContentLanuageVariantTests : PublishedContentSnapshotTestBase + public class PublishedContentLanguageVariantTests : PublishedContentSnapshotTestBase { protected override void Compose() { @@ -37,7 +37,7 @@ namespace Umbraco.Tests.PublishedContent // French has no fall back. var languages = new List { - new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, + new Language("en-US") { Id = 1, CultureName = "English", IsDefault = true }, new Language("fr") { Id = 2, CultureName = "French" }, new Language("es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 }, new Language("it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 }, diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index d5003674af..cce264fb3d 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2489,7 +2489,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true }; + var langUk = new Language("en-UK") { IsDefault = true }; var langFr = new Language("fr-FR"); languageService.Save(langFr); @@ -2524,7 +2524,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true }; + var langUk = new Language("en-UK") { IsDefault = true }; var langFr = new Language("fr-FR"); languageService.Save(langFr); @@ -2562,7 +2562,7 @@ namespace Umbraco.Tests.Services var languageService = ServiceContext.LocalizationService; //var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true }; - var langXx = new Language("pt-PT") { IsDefaultVariantLanguage = true }; + var langXx = new Language("pt-PT") { IsDefault = true }; var langFr = new Language("fr-FR"); var langUk = new Language("en-UK"); var langDe = new Language("de-DE"); diff --git a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs index 033fa08d4a..cc97c98f5d 100644 --- a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs +++ b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs @@ -362,21 +362,21 @@ namespace Umbraco.Tests.Services { var localizationService = ServiceContext.LocalizationService; var language = new Core.Models.Language("en-AU"); - language.IsDefaultVariantLanguage = true; + language.IsDefault = true; localizationService.Save(language); var result = localizationService.GetLanguageById(language.Id); - Assert.IsTrue(result.IsDefaultVariantLanguage); + Assert.IsTrue(result.IsDefault); var language2 = new Core.Models.Language("en-NZ"); - language2.IsDefaultVariantLanguage = true; + language2.IsDefault = true; localizationService.Save(language2); var result2 = localizationService.GetLanguageById(language2.Id); //re-get result = localizationService.GetLanguageById(language.Id); - Assert.IsTrue(result2.IsDefaultVariantLanguage); - Assert.IsFalse(result.IsDefaultVariantLanguage); + Assert.IsTrue(result2.IsDefault); + Assert.IsFalse(result.IsDefault); } [Test] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 0b984b1167..bf9e9fefde 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -122,7 +122,7 @@ - + diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 650f0d082b..176e1bc461 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -766,7 +766,7 @@ namespace Umbraco.Web.Editors var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs.Values) .Where(x => otherVariantsToValidate.All(v => !v.Culture.InvariantEquals(x.IsoCode))) //don't include variants above .Where(x => !x.IsoCode.InvariantEquals(contentItem.Culture)) //don't include the current variant - .Where(x => x.Mandatory); + .Where(x => x.IsMandatory); foreach (var lang in mandatoryLangs) { //cannot continue publishing since a required language that is not currently being published isn't published diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 0de3aca634..7b4e1e36fe 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -57,18 +57,18 @@ namespace Umbraco.Web.Editors //if there's only one language, by default it is the default var allLangs = Services.LocalizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (!lang.IsDefaultVariantLanguage) + if (!lang.IsDefault) { if (allLangs.Count == 1) { - model.IsDefaultVariantLanguage = true; - model.Mandatory = true; + model.IsDefault = true; + model.IsMandatory = true; } - else if (allLangs.All(x => !x.IsDefaultVariantLanguage)) + else if (allLangs.All(x => !x.IsDefault)) { //if no language has the default flag, then the default language is the one with the lowest id - model.IsDefaultVariantLanguage = allLangs[0].Id == lang.Id; - model.Mandatory = allLangs[0].Id == lang.Id; + model.IsDefault = allLangs[0].Id == lang.Id; + model.IsMandatory = allLangs[0].Id == lang.Id; } } @@ -92,7 +92,7 @@ namespace Umbraco.Web.Editors var langs = Services.LocalizationService.GetAllLanguages().ToArray(); var totalLangs = langs.Length; - if (language.IsDefaultVariantLanguage || totalLangs == 1) + if (language.IsDefault || totalLangs == 1) { var message = $"Language '{language.CultureName}' is currently set to 'default' or it is the only installed language and cannot be deleted."; throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); @@ -145,8 +145,8 @@ namespace Umbraco.Web.Editors var newLang = new Core.Models.Language(culture.Name) { CultureName = culture.DisplayName, - IsDefaultVariantLanguage = language.IsDefaultVariantLanguage, - Mandatory = language.Mandatory, + IsDefault = language.IsDefault, + IsMandatory = language.IsMandatory, FallbackLanguageId = language.FallbackLanguageId }; @@ -154,8 +154,8 @@ namespace Umbraco.Web.Editors return Mapper.Map(newLang); } - found.Mandatory = language.Mandatory; - found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; + found.IsMandatory = language.IsMandatory; + found.IsDefault = language.IsDefault; found.FallbackLanguageId = language.FallbackLanguageId; string selectedFallbackLanguageCultureName; diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index 7693ee836e..75dd07bf09 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing @@ -20,10 +19,10 @@ namespace Umbraco.Web.Models.ContentEditing public string Name { get; set; } [DataMember(Name = "isDefault")] - public bool IsDefaultVariantLanguage { get; set; } + public bool IsDefault { get; set; } [DataMember(Name = "isMandatory")] - public bool Mandatory { get; set; } + public bool IsMandatory { get; set; } [DataMember(Name = "fallbackLanguageId")] public int? FallbackLanguageId { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs index cb6e2938be..21c27649bb 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Models.Mapping var variants = langs.Select(x => new ContentVariation { Language = x, - Mandatory = x.Mandatory, + Mandatory = x.IsMandatory, Name = source.GetCultureName(x.IsoCode), Exists = source.IsCultureAvailable(x.IsoCode), // segments ?? PublishedState = (source.PublishedState == PublishedState.Unpublished //if the entire document is unpublished, then flag every variant as unpublished @@ -61,7 +61,7 @@ namespace Umbraco.Web.Models.Mapping } } if (!foundCurrent) - variants.First(x => x.Language.IsDefaultVariantLanguage).IsCurrent = true; + variants.First(x => x.Language.IsDefault).IsCurrent = true; return variants; } From ecc75bc4c9ae9a12b21dcccded61e32cee3f85e8 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 21 Jul 2018 08:24:08 +0200 Subject: [PATCH 022/310] Allowed delete of langage via services even if used as a fall-back for other languages, by setting references to null before deleting --- .../Persistence/Dtos/LanguageDto.cs | 1 + .../Implement/LanguageRepository.cs | 7 ++++-- .../Services/Implement/LocalizationService.cs | 3 +-- .../Repositories/LanguageRepositoryTest.cs | 24 +++++++++++++++++++ .../Services/LocalizationServiceTests.cs | 14 +++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index f87930269a..25ca43f918 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -45,6 +45,7 @@ namespace Umbraco.Core.Persistence.Dtos ///
[Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index af5d28c18e..271e084969 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -177,7 +177,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistDeletedItem(ILanguage entity) { - //we need to validate that we can delete this language + // We need to validate that we can delete this language if (entity.IsDefaultVariantLanguage) throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})"); @@ -185,9 +185,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (count == 1) throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})"); + // We need to remove any references to the language if it's being used as a fall-back from other ones + Database.Execute(Sql().Update(u => u.Set(x => x.FallbackLanguageId, null)).Where(x => x.FallbackLanguageId == entity.Id)); + base.PersistDeletedItem(entity); - //Clear the cache entries that exist by key/iso + // Clear the cache entries that exist by key/iso IsolatedCache.ClearCacheItem(RepositoryCacheKeys.GetKey(entity.IsoCode)); IsolatedCache.ClearCacheItem(RepositoryCacheKeys.GetKey(entity.CultureName)); } diff --git a/src/Umbraco.Core/Services/Implement/LocalizationService.cs b/src/Umbraco.Core/Services/Implement/LocalizationService.cs index 663ecf586c..e63a3cbdbb 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizationService.cs @@ -393,8 +393,7 @@ namespace Umbraco.Core.Services.Implement return; } - //NOTE: There isn't any constraints in the db, so possible references aren't deleted - + // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted _languageRepository.Delete(language); deleteEventArgs.CanCancel = false; diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index a63bf5e08d..ed82d03c6e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -318,6 +318,30 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Delete_On_LanguageRepository_With_Language_Used_As_Fallback() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + // Add language to delete as a fall-back language to another one + var repository = CreateRepository(provider); + var languageToFallbackFrom = repository.Get(5); + languageToFallbackFrom.FallbackLanguageId = 1; + repository.Save(languageToFallbackFrom); + + // Act + var languageToDelete = repository.Get(1); + repository.Delete(languageToDelete); + + var exists = repository.Exists(1); + + // Assert + Assert.That(exists, Is.False); + } + } + [Test] public void Can_Perform_Exists_On_LanguageRepository() { diff --git a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs index 033fa08d4a..ff5aa8edc9 100644 --- a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs +++ b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs @@ -192,6 +192,20 @@ namespace Umbraco.Tests.Services Assert.Null(language); } + [Test] + public void Can_Delete_Language_Used_As_Fallback() + { + var danish = ServiceContext.LocalizationService.GetLanguageByIsoCode("da-DK"); + var norwegian = new Language("nb-NO") { CultureName = "Norwegian", FallbackLanguageId = danish.Id }; + ServiceContext.LocalizationService.Save(norwegian, 0); + var languageId = danish.Id; + + ServiceContext.LocalizationService.Delete(danish); + + var language = ServiceContext.LocalizationService.GetLanguageById(languageId); + Assert.Null(language); + } + [Test] public void Can_Create_DictionaryItem_At_Root() { From 695f21eadbe6aee6415f8983998686db2ffd49f8 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 21 Jul 2018 09:41:07 +0200 Subject: [PATCH 023/310] Removed unnecessary loop in looking up value from a fall-back language. Put in a check to abort fall-back if there's a loop in language fall-backs. --- .../IPublishedValueFallback.cs | 14 +- .../NoopPublishedValueFallback.cs | 16 ++- .../PublishedContentLanuageVariantTests.cs | 14 +- .../PublishedValueFallback.cs | 17 +-- .../PublishedValueLanguageFallback.cs | 129 +++++++++--------- src/Umbraco.Web/PublishedContentExtensions.cs | 15 +- .../PublishedContentPropertyExtension.cs | 9 +- src/Umbraco.Web/PublishedElementExtensions.cs | 11 +- 8 files changed, 127 insertions(+), 98 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index f154d9ef27..d9d9c0c298 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Composing; +using System.Collections.Generic; namespace Umbraco.Core.Models.PublishedContent { @@ -22,22 +22,22 @@ namespace Umbraco.Core.Models.PublishedContent // this method is called whenever property.Value(culture, segment, defaultValue) is called, and // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). - object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); + object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages); // this method is called whenever property.Value(culture, segment, defaultValue) is called, and // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). - T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); + T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages); // these methods to be called whenever getting the property value for the specified alias, culture and segment, // either returned no property at all, or a property that does not HasValue for the specified culture and segment. - object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); + object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages); - T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); + T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages); - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages); - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index 75ab9df35a..a7de0709e6 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Core.Models.PublishedContent +using System.Collections.Generic; + +namespace Umbraco.Core.Models.PublishedContent { /// /// Provides a noop implementation for . @@ -9,21 +11,21 @@ public class NoopPublishedValueFallback : IPublishedValueFallback { /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue; + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) => defaultValue; /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue; + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue; + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) => defaultValue; /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) => defaultValue; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) => defaultValue; } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs index 22eb4bd799..17ce032005 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -35,13 +35,17 @@ namespace Umbraco.Tests.PublishedContent // Set up languages. // Spanish falls back to English and Italian to Spanish (and then to English). // French has no fall back. + // Danish, Swedish and Norweigan create an invalid loop. var languages = new List { new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, new Language("fr") { Id = 2, CultureName = "French" }, new Language("es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 }, new Language("it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 }, - new Language("de") { Id = 5, CultureName = "German" } + new Language("de") { Id = 5, CultureName = "German" }, + new Language("da") { Id = 6, CultureName = "Danish", FallbackLanguageId = 8 }, + new Language("sv") { Id = 7, CultureName = "Swedish", FallbackLanguageId = 6 }, + new Language("no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 } }; var localizationService = Mock.Get(serviceContext.LocalizationService); @@ -126,5 +130,13 @@ namespace Umbraco.Tests.PublishedContent var value = content.Value("welcomeText", "it"); Assert.AreEqual("Welcome", value); } + + [Test] + public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "no"); + Assert.IsNull(value); + } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 562b8e393b..86823767fd 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models.PublishedContent; +using System.Collections.Generic; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Models.PublishedContent { @@ -11,45 +12,45 @@ namespace Umbraco.Web.Models.PublishedContent // kinda reproducing what was available in v7 /// - public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) { // no fallback here if (!recurse) return defaultValue; // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority); + return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority, visitedLanguages); } /// - public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) { // no fallback here if (!recurse) return defaultValue; diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index d6e8db83f4..d19ef80732 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models; +using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; @@ -21,72 +22,72 @@ namespace Umbraco.Web.Models.PublishedContent } /// - public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) { object value; - if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value)) + if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, visitedLanguages, out value)) { return value; } - return base.GetValue(property, culture, segment, defaultValue); + return base.GetValue(property, culture, segment, defaultValue, visitedLanguages); } /// - public override T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public override T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) { T value; - if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value)) + if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, visitedLanguages, out value)) { return value; } - return base.GetValue(property, culture, segment, defaultValue); + return base.GetValue(property, culture, segment, defaultValue, visitedLanguages); } /// - public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) { object value; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value)) + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, visitedLanguages, out value)) { return value; } - return base.GetValue(content, alias, culture, segment, defaultValue); + return base.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages); } /// - public override T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public override T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) { T value; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value)) + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, visitedLanguages, out value)) { return value; } - return base.GetValue(content, alias, culture, segment, defaultValue); + return base.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages); } /// - public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) { - return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); } /// - public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) { if (fallbackPriority == PublishedValueFallbackPriority.RecursiveTree) { - var result = base.GetValue(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree); + var result = base.GetValue(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree, visitedLanguages); if (ValueIsNotNullEmptyOrDefault(result, defaultValue)) { // We've prioritised recursive tree search and found a value, so can return it. return result; } - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result)) + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages, out result)) { return result; } @@ -97,112 +98,116 @@ namespace Umbraco.Web.Models.PublishedContent if (fallbackPriority == PublishedValueFallbackPriority.FallbackLanguage) { T result; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result)) + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages, out result)) { return result; } } // No language fall back content found, so use base implementation - return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); } - private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) + private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages, out T value) { + value = defaultValue; + if (string.IsNullOrEmpty(culture)) { - value = defaultValue; return false; } var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguageId.HasValue == false) { - value = defaultValue; return false; } - var fallbackLanguageId = language.FallbackLanguageId; - while (fallbackLanguageId.HasValue) + if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) { - var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); - value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - fallbackLanguageId = fallbackLanguage.FallbackLanguageId; + return false; + } + + visitedLanguages.Add(language.FallbackLanguageId.Value); + + var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); + value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue, visitedLanguages); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; } - value = defaultValue; return false; } - private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value) + private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages, out T value) { + value = defaultValue; + if (string.IsNullOrEmpty(culture)) { - value = defaultValue; return false; } var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguageId.HasValue == false) { - value = defaultValue; return false; } - var fallbackLanguageId = language.FallbackLanguageId; - while (fallbackLanguageId.HasValue) + if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) { - var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - fallbackLanguageId = fallbackLanguage.FallbackLanguageId; + return false; + } + + visitedLanguages.Add(language.FallbackLanguageId.Value); + + var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, visitedLanguages); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; } - value = defaultValue; return false; } - private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, out T value) + private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages, out T value) { + value = defaultValue; if (string.IsNullOrEmpty(culture)) { - value = defaultValue; return false; } var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguageId.HasValue == false) { - value = defaultValue; return false; } - var fallbackLanguageId = language.FallbackLanguageId; - while (fallbackLanguageId.HasValue) + if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) { - var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - fallbackLanguageId = fallbackLanguage.FallbackLanguageId; + return false; + } + + visitedLanguages.Add(language.FallbackLanguageId.Value); + + var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; } - value = defaultValue; return false; } + private static bool AlreadyVisitedLanguage(ICollection visitedLanguages, int fallbackLanguageId) + { + return visitedLanguages.Contains(fallbackLanguageId); + } + private ILanguage GetLanguageById(int id) { return _localizationService.GetLanguageById(id); diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 22a0bc8aca..4ab2d1dc79 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -162,6 +162,7 @@ namespace Umbraco.Web /// The default value. /// A value indicating whether to recurse. /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. + /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -170,14 +171,15 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, + bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages ?? new List()); } #endregion @@ -195,6 +197,7 @@ namespace Umbraco.Web /// The default value. /// A value indicating whether to recurse. /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. + /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, converted to the specified type. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -203,18 +206,20 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, + bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages ?? new List()); } // fixme - .Value() refactoring - in progress - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", + bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/PublishedContentPropertyExtension.cs b/src/Umbraco.Web/PublishedContentPropertyExtension.cs index fcbfc7f431..fdfd772ce7 100644 --- a/src/Umbraco.Web/PublishedContentPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedContentPropertyExtension.cs @@ -1,4 +1,5 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -15,19 +16,19 @@ namespace Umbraco.Web #region Value - public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default, ICollection visitedLanguages = null) { if (property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(property, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue, visitedLanguages ?? new List()); } #endregion #region Value - public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default) + public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default, ICollection visitedLanguages = null) { // for Value when defaultValue is not specified, and HasValue() is false, we still want to convert the result (see below) // but we have no way to tell whether default value is specified or not - we could do it with overloads, but then defaultValue diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index 945270cb9e..b0d2826df4 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -99,6 +100,7 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. + /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -106,14 +108,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages ?? new List()); } #endregion @@ -129,6 +131,7 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. + /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, converted to the specified type. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -136,14 +139,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default) + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages ?? new List()); } #endregion From 1be74589f2d3869f504f13a07ad6ce36f9664752 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 21 Jul 2018 15:58:49 +0200 Subject: [PATCH 024/310] Combined fallback parameters into an array that defines methods and priority to be used. --- src/Umbraco.Core/Constants-Content.cs | 22 +++ .../IPublishedValueFallback.cs | 10 +- .../NoopPublishedValueFallback.cs | 4 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../PublishedContentLanuageVariantTests.cs | 102 ++++++++++- .../PublishedContent/PublishedContentTests.cs | 4 +- .../SolidPublishedSnapshot.cs | 22 ++- .../PublishedValueFallback.cs | 78 ++++++--- .../PublishedValueLanguageFallback.cs | 163 ++---------------- src/Umbraco.Web/PublishedContentExtensions.cs | 39 ++--- src/Umbraco.Web/umbraco.presentation/item.cs | 4 +- 11 files changed, 226 insertions(+), 223 deletions(-) create mode 100644 src/Umbraco.Core/Constants-Content.cs diff --git a/src/Umbraco.Core/Constants-Content.cs b/src/Umbraco.Core/Constants-Content.cs new file mode 100644 index 0000000000..4b8c383e6f --- /dev/null +++ b/src/Umbraco.Core/Constants-Content.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + /// + /// Defines content retrieval related constants + /// + public static class Content + { + /// + /// Defines core supported content fall-back options when retrieving content property values. + /// Defined as constants rather than enum to allow solution or package defined fall-back methods. + /// + public static class FallbackMethods + { + public const int None = 0; + public const int RecursiveTree = 1; + public const int FallbackLanguage = 2; + } + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index d9d9c0c298..7a7b67a2d1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -2,12 +2,6 @@ namespace Umbraco.Core.Models.PublishedContent { - public enum PublishedValueFallbackPriority - { - RecursiveTree, - FallbackLanguage - } - /// /// Provides a fallback strategy for getting values. /// @@ -36,8 +30,8 @@ namespace Umbraco.Core.Models.PublishedContent T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages); - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages); - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index a7de0709e6..a8d55176a3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -23,9 +23,9 @@ namespace Umbraco.Core.Models.PublishedContent public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) => defaultValue; + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) => defaultValue; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) => defaultValue; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 67028568eb..0a24cc5286 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -286,6 +286,7 @@ + diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs index 17ce032005..7ee2e11209 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -45,7 +45,8 @@ namespace Umbraco.Tests.PublishedContent new Language("de") { Id = 5, CultureName = "German" }, new Language("da") { Id = 6, CultureName = "Danish", FallbackLanguageId = 8 }, new Language("sv") { Id = 7, CultureName = "Swedish", FallbackLanguageId = 6 }, - new Language("no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 } + new Language("no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 }, + new Language("nl") { Id = 9, CultureName = "Dutch", FallbackLanguageId = 1 } }; var localizationService = Mock.Get(serviceContext.LocalizationService); @@ -68,12 +69,28 @@ namespace Umbraco.Tests.PublishedContent { Alias = "welcomeText", }; - prop1.SetSourceValue("en-US", "Welcome"); - prop1.SetValue("en-US", "Welcome"); + prop1.SetSourceValue("en-US", "Welcome", true); + prop1.SetValue("en-US", "Welcome", true); prop1.SetSourceValue("de", "Willkommen"); prop1.SetValue("de", "Willkommen"); + prop1.SetSourceValue("nl", "Welkom"); + prop1.SetValue("nl", "Welkom"); - cache.Add(new SolidPublishedContent(contentType1) + var prop2 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText2", + }; + prop2.SetSourceValue("en-US", "Welcome", true); + prop2.SetValue("en-US", "Welcome", true); + + var prop3 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText", + }; + prop3.SetSourceValue("en-US", "Welcome", true); + prop3.SetValue("en-US", "Welcome", true); + + var item1 = new SolidPublishedContent(contentType1) { Id = 1, SortOrder = 0, @@ -83,12 +100,35 @@ namespace Umbraco.Tests.PublishedContent Level = 1, Url = "/content-1", ParentId = -1, + ChildIds = new[] { 2 }, + Properties = new Collection + { + prop1, prop2 + } + }; + + var item2 = new SolidPublishedContent(contentType1) + { + Id = 2, + SortOrder = 0, + Name = "Content 2", + UrlSegment = "content-2", + Path = "/1/2", + Level = 2, + Url = "/content-1/content-2", + ParentId = 1, ChildIds = new int[] { }, Properties = new Collection { - prop1 + prop3 } - }); + }; + + item1.Children = new List { item2 }; + item2.Parent = item1; + + cache.Add(item1); + cache.Add(item2); } [Test] @@ -116,10 +156,18 @@ namespace Umbraco.Tests.PublishedContent } [Test] - public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() + public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Unless_Requested() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); var value = content.Value("welcomeText", "es"); + Assert.IsNull(value); + } + + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "es", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); Assert.AreEqual("Welcome", value); } @@ -127,7 +175,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "it"); + var value = content.Value("welcomeText", "it", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); Assert.AreEqual("Welcome", value); } @@ -135,8 +183,44 @@ namespace Umbraco.Tests.PublishedContent public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "no"); + var value = content.Value("welcomeText", "no", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); Assert.IsNull(value); } + + [Test] + public void Do_Not_Get_Content_Recursively_Unless_Requested() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText2"); + Assert.IsNull(value); + } + + [Test] + public void Can_Get_Content_Recursively() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText2", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.RecursiveTree }); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_With_Recursive_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.RecursiveTree, Core.Constants.Content.FallbackMethods.FallbackLanguage }); + + // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. + Assert.AreEqual("Welkom", value); + } + + [Test] + public void Can_Get_Content_With_Fallback_Language_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage, Core.Constants.Content.FallbackMethods.RecursiveTree }); + + // No Dutch value is directly assigned. Check has fallen back to English value from language variant. + Assert.AreEqual("Welcome", value); + } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index a09cf6d4ad..330711a6ba 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -326,8 +326,8 @@ namespace Umbraco.Tests.PublishedContent public void Get_Property_Value_Recursive() { var doc = GetNode(1174); - var rVal = doc.Value("testRecursive", recurse: true); - var nullVal = doc.Value("DoNotFindThis", recurse: true); + var rVal = doc.Value("testRecursive", fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree } ); + var nullVal = doc.Value("DoNotFindThis", fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); Assert.AreEqual("This is the recursive val", rVal); Assert.AreEqual(null, nullVal); } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 33e315ebec..cbf7f3189d 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -248,7 +248,7 @@ namespace Umbraco.Tests.PublishedContent #endregion } - class SolidPublishedProperty : IPublishedProperty + internal class SolidPublishedProperty : IPublishedProperty { public PublishedPropertyType PropertyType { get; set; } public string Alias { get; set; } @@ -309,19 +309,33 @@ namespace Umbraco.Tests.PublishedContent return _solidSourceValues.ContainsKey(culture); } - public void SetSourceValue(string culture, object value) + public void SetSourceValue(string culture, object value, bool defaultValue = false) { _solidSourceValues.Add(culture, value); + if (defaultValue) + { + SolidSourceValue = value; + SolidHasValue = true; + } } - public void SetValue(string culture, object value) + public void SetValue(string culture, object value, bool defaultValue = false) { _solidValues.Add(culture, value); + if (defaultValue) + { + SolidValue = value; + SolidHasValue = true; + } } - public void SetXPathValue(string culture, object value) + public void SetXPathValue(string culture, object value, bool defaultValue = false) { _solidXPathValues.Add(culture, value); + if (defaultValue) + { + SolidXPathValue = value; + } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 86823767fd..85b10be480 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Models.PublishedContent @@ -8,54 +9,79 @@ namespace Umbraco.Web.Models.PublishedContent /// public class PublishedValueFallback : IPublishedValueFallback { - // this is our default implementation // kinda reproducing what was available in v7 /// - public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) { - // no fallback here - if (!recurse) return defaultValue; - // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority, visitedLanguages); + return GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages); } /// - public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) { - // no fallback here - if (!recurse) return defaultValue; + if (fallbackMethods == null) + { + return defaultValue; + } - // otherwise, implement recursion as it was implemented in PublishedContentBase + foreach (var fallbackMethod in fallbackMethods) + { + if (TryGetValueWithFallbackMethod(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages, fallbackMethod, out T value)) + { + return value; + } + } + + return defaultValue; + } + + protected virtual bool TryGetValueWithFallbackMethod(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, int fallbackMethod, out T value) + { + value = defaultValue; + switch (fallbackMethod) + { + case Core.Constants.Content.FallbackMethods.None: + return false; + case Core.Constants.Content.FallbackMethods.RecursiveTree: + return TryGetValueWithRecursiveTree(content, alias, culture, segment, defaultValue, out value); + default: + throw new NotSupportedException($"Fallback method with indentifying number {fallbackMethod} is not supported within {GetType().Name}."); + } + } + + protected static bool TryGetValueWithRecursiveTree(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + { + // Implement recursion as it was implemented in PublishedContentBase // fixme caching? // @@ -73,21 +99,31 @@ namespace Umbraco.Web.Models.PublishedContent { content = content.Parent; property = content?.GetProperty(alias); - if (property != null) noValueProperty = property; - } while (content != null && (property == null || property.HasValue(culture, segment) == false)); + if (property != null) + { + noValueProperty = property; + } + } + while (content != null && (property == null || property.HasValue(culture, segment) == false)); // if we found a content with the property having a value, return that property value if (property != null && property.HasValue(culture, segment)) - return property.Value(culture, segment); + { + value = property.Value(culture, segment); + return true; + } // if we found a property, even though with no value, return that property value // because the converter may want to handle the missing value. ie if defaultValue is default, // either specified or by default, the converter may want to substitute something else. if (noValueProperty != null) - return noValueProperty.Value(culture, segment, defaultValue: defaultValue); + { + value = noValueProperty.Value(culture, segment, defaultValue: defaultValue); + return true; + } - // else return default - return defaultValue; + value = defaultValue; + return false; } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index d19ef80732..e94a8559a0 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; @@ -21,158 +23,23 @@ namespace Umbraco.Web.Models.PublishedContent _localizationService = services.LocalizationService; } - /// - public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) - { - object value; - if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, visitedLanguages, out value)) - { - return value; - } - - return base.GetValue(property, culture, segment, defaultValue, visitedLanguages); - } - - /// - public override T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) - { - T value; - if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, visitedLanguages, out value)) - { - return value; - } - - return base.GetValue(property, culture, segment, defaultValue, visitedLanguages); - } - - /// - public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) - { - object value; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, visitedLanguages, out value)) - { - return value; - } - - return base.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages); - } - - /// - public override T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) - { - T value; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, visitedLanguages, out value)) - { - return value; - } - - return base.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages); - } - - /// - public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) - { - return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); - } - - /// - public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) - { - if (fallbackPriority == PublishedValueFallbackPriority.RecursiveTree) - { - var result = base.GetValue(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree, visitedLanguages); - if (ValueIsNotNullEmptyOrDefault(result, defaultValue)) - { - // We've prioritised recursive tree search and found a value, so can return it. - return result; - } - - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages, out result)) - { - return result; - } - - return defaultValue; - } - - if (fallbackPriority == PublishedValueFallbackPriority.FallbackLanguage) - { - T result; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages, out result)) - { - return result; - } - } - - // No language fall back content found, so use base implementation - return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); - } - - private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages, out T value) + protected override bool TryGetValueWithFallbackMethod(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, int fallbackMethod, out T value) { value = defaultValue; - - if (string.IsNullOrEmpty(culture)) + switch (fallbackMethod) { - return false; + case Core.Constants.Content.FallbackMethods.None: + return false; + case Core.Constants.Content.FallbackMethods.RecursiveTree: + return TryGetValueWithRecursiveTree(content, alias, culture, segment, defaultValue, out value); + case Core.Constants.Content.FallbackMethods.FallbackLanguage: + return TryGetValueWithFallbackLanguage(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages, out value); + default: + throw new NotSupportedException($"Fallback method with indentifying number {fallbackMethod} is not supported within {GetType().Name}."); } - - var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguageId.HasValue == false) - { - return false; - } - - if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) - { - return false; - } - - visitedLanguages.Add(language.FallbackLanguageId.Value); - - var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); - value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue, visitedLanguages); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - return false; } - private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages, out T value) - { - value = defaultValue; - - if (string.IsNullOrEmpty(culture)) - { - return false; - } - - var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguageId.HasValue == false) - { - return false; - } - - if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) - { - return false; - } - - visitedLanguages.Add(language.FallbackLanguageId.Value); - - var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, visitedLanguages); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - return false; - } - - private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages, out T value) + private bool TryGetValueWithFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, out T value) { value = defaultValue; if (string.IsNullOrEmpty(culture)) @@ -194,7 +61,7 @@ namespace Umbraco.Web.Models.PublishedContent visitedLanguages.Add(language.FallbackLanguageId.Value); var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, fallbackMethods.ToArray(), visitedLanguages); if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) { return true; diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 4ab2d1dc79..c4c745a087 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -151,43 +151,35 @@ namespace Umbraco.Web #endregion #region Value - + /// - /// Recursively the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. /// /// The content. /// The property alias. /// The variation language. /// The variation segment. /// The default value. - /// A value indicating whether to recurse. - /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. + /// Options for fall-back if content not found. /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// Recursively means: walking up the tree from , get the first value that can be found. - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, - bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, + int[] fallbackMethods = null, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages ?? new List()); } #endregion #region Value - + /// - /// Recursively gets the value of a content's property identified by its alias, converted to a specified type. + /// Gets the value of a content's property identified by its alias, converted to a specified type. /// /// The target property type. /// The content. @@ -195,31 +187,24 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// A value indicating whether to recurse. - /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. + /// Options for fall-back if content not found. /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, converted to the specified type. /// - /// Recursively means: walking up the tree from , get the first value that can be found. - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, - bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) + int[] fallbackMethods = null, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages ?? new List()); } // fixme - .Value() refactoring - in progress public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", - bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) + int[] fallbackMethods = null, ICollection visitedLanguages = null) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 3937b5675c..18437f5235 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -76,7 +76,7 @@ namespace umbraco //check for published content and get its value using that if (publishedContent != null && (publishedContent.HasProperty(_fieldName) || recursive)) { - var pval = publishedContent.Value(_fieldName, recurse: recursive); + var pval = publishedContent.Value(_fieldName, fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } @@ -96,7 +96,7 @@ namespace umbraco { if (publishedContent != null && (publishedContent.HasProperty(altFieldName) || recursive)) { - var pval = publishedContent.Value(altFieldName, recurse: recursive); + var pval = publishedContent.Value(altFieldName, fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } From a35f67ecef25af5befcbe52c62df6471ac66d460 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 24 Jul 2018 13:32:29 +0200 Subject: [PATCH 025/310] Refactoring --- src/Umbraco.Core/Constants-Content.cs | 24 +++- .../IPublishedValueFallback.cs | 53 ++++++-- .../NoopPublishedValueFallback.cs | 16 ++- .../PublishedContentLanguageVariantTests.cs | 28 +---- .../PublishedContent/PublishedContentTests.cs | 6 +- src/Umbraco.Tests/TestHelpers/BaseWebTest.cs | 2 +- .../PublishedValueFallback.cs | 116 +++++++++++------- .../PublishedValueLanguageFallback.cs | 90 -------------- src/Umbraco.Web/PublishedContentExtensions.cs | 20 ++- src/Umbraco.Web/PublishedElementExtensions.cs | 15 ++- ...nsion.cs => PublishedPropertyExtension.cs} | 4 +- .../Runtime/WebRuntimeComponent.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 3 +- src/Umbraco.Web/umbraco.presentation/item.cs | 4 +- 14 files changed, 168 insertions(+), 215 deletions(-) delete mode 100644 src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs rename src/Umbraco.Web/{PublishedContentPropertyExtension.cs => PublishedPropertyExtension.cs} (96%) diff --git a/src/Umbraco.Core/Constants-Content.cs b/src/Umbraco.Core/Constants-Content.cs index 4b8c383e6f..b9d0691454 100644 --- a/src/Umbraco.Core/Constants-Content.cs +++ b/src/Umbraco.Core/Constants-Content.cs @@ -11,11 +11,27 @@ /// Defines core supported content fall-back options when retrieving content property values. /// Defined as constants rather than enum to allow solution or package defined fall-back methods. /// - public static class FallbackMethods + public static class ValueFallback { - public const int None = 0; - public const int RecursiveTree = 1; - public const int FallbackLanguage = 2; + /// + /// No fallback at all. + /// + public const int None = -1; + + /// + /// Default fallback. + /// + public const int Default = 0; + + /// + /// Recurse up the tree. + /// + public const int Recurse = 1; + + /// + /// Fallback to other languages. + /// + public const int Language = 2; } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 59442e20bb..96a6c144fa 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -1,15 +1,44 @@ -using System.Collections.Generic; - -namespace Umbraco.Core.Models.PublishedContent +namespace Umbraco.Core.Models.PublishedContent { /// /// Provides a fallback strategy for getting values. /// - // fixme - IPublishedValueFallback is still WorkInProgress - // todo - properly document methods, etc - // todo - understand caching vs fallback (recurse etc) public interface IPublishedValueFallback { + // fixme discussions & challenges + // + // - what's with visitedLanguage? should be internal to fallback implementation + // so that should be the case now, with latest changes + // + // - should be as simple as + // model.Value("price", fallback: ValueFallback.Language); + // model.Value("name", fallback: ValueFallback.Recurse); + // + // so chaining things through an array of ints is not... convenient + // it feels like ppl could have ValueFallback.LanguageAndRecurse or something? + // + // - the fallback: parameter value must be open, so about anything can be passed to the IPublishedValueFallback + // we have it now, it's an integer + constants, cool + // + // - we need to be able to configure (via code for now) a default fallback policy? + // not! the default value of the fallback: parameter is 'default', not 'none', and if people + // want to implement a different default behavior, they have to override the fallback provider + // + // - currently, no policies on IPublishedProperty nor IPublishedElement, but some may apply (language) + // todo: implement + // + // - general defaultValue discussion: + // when HasValue is false, the converter may return something, eg an empty enumerable, even though + // defaultValue is null, so should we respect defaultValue only when it is not 'default'? + // todo: when defaultValue==default, and HasValue is false, still return GetValue to ensure this + // + // - (and...) + // ModelsBuilder model.Value(x => x.Price, ...) extensions need to be adjusted too + // + // - cache & perfs + // soon as ppl implement custom fallbacks, caching is a problem, so better just not cache + // OTOH we need to implement the readonly thing for languages + /// /// Gets a fallback value for a property. /// @@ -25,7 +54,7 @@ namespace Umbraco.Core.Models.PublishedContent /// At property level, property.GetValue() does *not* implement fallback, and one has to /// get property.Value() or property.Value{T}() to trigger fallback. /// - object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages); + object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); /// /// Gets a fallback value for a property. @@ -43,7 +72,7 @@ namespace Umbraco.Core.Models.PublishedContent /// At property level, property.GetValue() does *not* implement fallback, and one has to /// get property.Value() or property.Value{T}() to trigger fallback. /// - T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages); + T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); /// /// Gets a fallback value for a published element property. @@ -59,7 +88,7 @@ namespace Umbraco.Core.Models.PublishedContent /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// It can only fallback at element level (no recurse). /// - object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages); + object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); /// /// Gets a fallback value for a published element property. @@ -76,7 +105,7 @@ namespace Umbraco.Core.Models.PublishedContent /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// It can only fallback at element level (no recurse). /// - T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages); + T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); /// /// Gets a fallback value for a published content property. @@ -92,7 +121,7 @@ namespace Umbraco.Core.Models.PublishedContent /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// fixme explain & document priority + merge w/recurse? /// - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback); /// /// Gets a fallback value for a published content property. @@ -109,6 +138,6 @@ namespace Umbraco.Core.Models.PublishedContent /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// fixme explain & document priority + merge w/recurse? /// - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, int fallback); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index a8d55176a3..9d74c4d8a2 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Umbraco.Core.Models.PublishedContent +namespace Umbraco.Core.Models.PublishedContent { /// /// Provides a noop implementation for . @@ -11,21 +9,21 @@ namespace Umbraco.Core.Models.PublishedContent public class NoopPublishedValueFallback : IPublishedValueFallback { /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) => defaultValue; + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue; /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue; /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) => defaultValue; + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue; /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) => defaultValue; + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback) => defaultValue; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, int fallback) => defaultValue; } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 7108824602..0b0f4dea51 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -167,7 +167,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "es", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); + var value = content.Value("welcomeText", "es", fallback: Core.Constants.Content.ValueFallback.Language); Assert.AreEqual("Welcome", value); } @@ -175,7 +175,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "it", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); + var value = content.Value("welcomeText", "it", fallback: Core.Constants.Content.ValueFallback.Language); Assert.AreEqual("Welcome", value); } @@ -183,7 +183,7 @@ namespace Umbraco.Tests.PublishedContent public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "no", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); + var value = content.Value("welcomeText", "no", fallback: Core.Constants.Content.ValueFallback.Language); Assert.IsNull(value); } @@ -199,27 +199,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_Recursively() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); - var value = content.Value("welcomeText2", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.RecursiveTree }); - Assert.AreEqual("Welcome", value); - } - - [Test] - public void Can_Get_Content_With_Recursive_Priority() - { - var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); - var value = content.Value("welcomeText", "nl", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.RecursiveTree, Core.Constants.Content.FallbackMethods.FallbackLanguage }); - - // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. - Assert.AreEqual("Welkom", value); - } - - [Test] - public void Can_Get_Content_With_Fallback_Language_Priority() - { - var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); - var value = content.Value("welcomeText", "nl", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage, Core.Constants.Content.FallbackMethods.RecursiveTree }); - - // No Dutch value is directly assigned. Check has fallen back to English value from language variant. + var value = content.Value("welcomeText2", fallback: Core.Constants.Content.ValueFallback.Recurse); Assert.AreEqual("Welcome", value); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 950bbf2283..93f4f3f242 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.PublishedContent Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); Container.RegisterSingleton(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( @@ -336,8 +336,8 @@ namespace Umbraco.Tests.PublishedContent public void Get_Property_Value_Recursive() { var doc = GetNode(1174); - var rVal = doc.Value("testRecursive", fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree } ); - var nullVal = doc.Value("DoNotFindThis", fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); + var rVal = doc.Value("testRecursive", fallback: Constants.Content.ValueFallback.Recurse); + var nullVal = doc.Value("DoNotFindThis", fallback: Constants.Content.ValueFallback.Recurse); Assert.AreEqual("This is the recursive val", rVal); Assert.AreEqual(null, nullVal); } diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 2f7fe8700b..5eea6bcf72 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.TestHelpers { base.Compose(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); Container.RegisterSingleton(); } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 85b10be480..4136be40d8 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using ValueFallback = Umbraco.Core.Constants.Content.ValueFallback; namespace Umbraco.Web.Models.PublishedContent { @@ -9,90 +12,72 @@ namespace Umbraco.Web.Models.PublishedContent /// public class PublishedValueFallback : IPublishedValueFallback { - // kinda reproducing what was available in v7 + private readonly ILocalizationService _localizationService; + + /// + /// Initializes a new instance of the class. + /// + /// + public PublishedValueFallback(ILocalizationService localizationService) + { + _localizationService = localizationService; + } /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) { // no fallback here return defaultValue; } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) { // no fallback here return defaultValue; } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback) { // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages); + return GetValue(content, alias, culture, segment, defaultValue, fallback); } /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) + public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, int fallback) { - if (fallbackMethods == null) + switch (fallback) { - return defaultValue; - } - - foreach (var fallbackMethod in fallbackMethods) - { - if (TryGetValueWithFallbackMethod(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages, fallbackMethod, out T value)) - { - return value; - } - } - - return defaultValue; - } - - protected virtual bool TryGetValueWithFallbackMethod(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, int fallbackMethod, out T value) - { - value = defaultValue; - switch (fallbackMethod) - { - case Core.Constants.Content.FallbackMethods.None: - return false; - case Core.Constants.Content.FallbackMethods.RecursiveTree: - return TryGetValueWithRecursiveTree(content, alias, culture, segment, defaultValue, out value); + case ValueFallback.None: + case ValueFallback.Default: + return defaultValue; + case ValueFallback.Recurse: + return TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value1) ? value1 : defaultValue; + case ValueFallback.Language: + return TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value2) ? value2 : defaultValue; default: - throw new NotSupportedException($"Fallback method with indentifying number {fallbackMethod} is not supported within {GetType().Name}."); + throw new NotSupportedException($"Fallback {GetType().Name} does not support policy code '{fallback}'."); } } - protected static bool TryGetValueWithRecursiveTree(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + // tries to get a value, recursing the tree + protected static bool TryGetValueWithRecursiveFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) { - // Implement recursion as it was implemented in PublishedContentBase - - // fixme caching? - // - // all caches were using PublishedContentBase.GetProperty(alias, recurse) to get the property, - // then, - // NuCache.PublishedContent was storing the property in GetAppropriateCache() with key "NuCache.Property.Recurse[" + DraftOrPub(previewing) + contentUid + ":" + typeAlias + "]"; - // XmlPublishedContent was storing the property in _cacheProvider with key $"XmlPublishedCache.PublishedContentCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; - // DictionaryPublishedContent was storing the property in _cacheProvider with key $"XmlPublishedCache.PublishedMediaCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; - // - // at the moment, caching has been entirely removed, until we better understand caching + fallback - IPublishedProperty property = null; // if we are here, content's property has no value IPublishedProperty noValueProperty = null; do @@ -125,5 +110,44 @@ namespace Umbraco.Web.Models.PublishedContent value = defaultValue; return false; } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + // fixme + // _localizationService.GetXxx() is expensive, it deep clones objects + // we want _localizationService.GetReadOnlyXxx() returning IReadOnlyLanguage which cannot be saved back = no need to clone + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(alias, culture2, segment); + return true; + } + + language = language2; + } + } + } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs deleted file mode 100644 index e94a8559a0..0000000000 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; - -namespace Umbraco.Web.Models.PublishedContent -{ - /// - /// Provides a default implementation for that allows - /// for use of fall-back languages - /// - /// - /// Inherits from that implments what was available in v7. - /// - public class PublishedValueLanguageFallback : PublishedValueFallback - { - private readonly ILocalizationService _localizationService; - - public PublishedValueLanguageFallback(ServiceContext services) - { - _localizationService = services.LocalizationService; - } - - protected override bool TryGetValueWithFallbackMethod(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, int fallbackMethod, out T value) - { - value = defaultValue; - switch (fallbackMethod) - { - case Core.Constants.Content.FallbackMethods.None: - return false; - case Core.Constants.Content.FallbackMethods.RecursiveTree: - return TryGetValueWithRecursiveTree(content, alias, culture, segment, defaultValue, out value); - case Core.Constants.Content.FallbackMethods.FallbackLanguage: - return TryGetValueWithFallbackLanguage(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages, out value); - default: - throw new NotSupportedException($"Fallback method with indentifying number {fallbackMethod} is not supported within {GetType().Name}."); - } - } - - private bool TryGetValueWithFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, out T value) - { - value = defaultValue; - if (string.IsNullOrEmpty(culture)) - { - return false; - } - - var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguageId.HasValue == false) - { - return false; - } - - if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) - { - return false; - } - - visitedLanguages.Add(language.FallbackLanguageId.Value); - - var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, fallbackMethods.ToArray(), visitedLanguages); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - return false; - } - - private static bool AlreadyVisitedLanguage(ICollection visitedLanguages, int fallbackLanguageId) - { - return visitedLanguages.Contains(fallbackLanguageId); - } - - private ILanguage GetLanguageById(int id) - { - return _localizationService.GetLanguageById(id); - } - - private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) - { - return value != null && - string.IsNullOrEmpty(value.ToString()) == false && - value.Equals(defaultValue) == false; - } - } -} diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 0bea71358a..64738aa923 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -153,18 +153,16 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// Options for fall-back if content not found. - /// A list of cultures already visited in looking for a value via a fall-back method. + /// Optional fallback strategy. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, - int[] fallbackMethods = null, ICollection visitedLanguages = null) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, int fallback = 0) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallback); } #endregion @@ -180,24 +178,20 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// Options for fall-back if content not found. - /// A list of cultures already visited in looking for a value via a fall-back method. + /// Optional fallback strategy. /// The value of the content's property identified by the alias, converted to the specified type. - /// - public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, - int[] fallbackMethods = null, ICollection visitedLanguages = null) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, int fallback = 0) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallback); } // fixme - .Value() refactoring - in progress - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", - int[] fallbackMethods = null, ICollection visitedLanguages = null) + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", int fallback = 0) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index b0d2826df4..51f0b6c0dc 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -100,7 +100,6 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -108,14 +107,19 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default, ICollection visitedLanguages = null) + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages ?? new List()); + // fixme defaultValue is a problem here + // assuming the value may return as an IEnumerable and no defaultValue is provided, then defaultValue is null + // and if HasValue is false, what we get is 'null' - but the converter may instead have been able to return an + // empty enumerable, which would be way nicer - so we need a way to tell that 'no defaultValue has been provided'? + + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); } #endregion @@ -131,7 +135,6 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, converted to the specified type. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -139,14 +142,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default, ICollection visitedLanguages = null) + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); } #endregion diff --git a/src/Umbraco.Web/PublishedContentPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs similarity index 96% rename from src/Umbraco.Web/PublishedContentPropertyExtension.cs rename to src/Umbraco.Web/PublishedPropertyExtension.cs index fdfd772ce7..bce13d30b4 100644 --- a/src/Umbraco.Web/PublishedContentPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -16,12 +16,12 @@ namespace Umbraco.Web #region Value - public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default, ICollection visitedLanguages = null) + public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default) { if (property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(property, culture, segment, defaultValue, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue); } #endregion diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index 78ddb935a0..03ba763527 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -199,7 +199,7 @@ namespace Umbraco.Web.Runtime composition.Container.Register(_ => GlobalHost.ConnectionManager.GetHubContext(), new PerContainerLifetime()); // register properties fallback - composition.Container.RegisterSingleton(); + composition.Container.RegisterSingleton(); } internal void Initialize( diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index af2170123a..1d56ed5c7c 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -263,7 +263,6 @@ - @@ -838,7 +837,7 @@ - + diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 14835b278f..d89733044d 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -78,7 +78,7 @@ namespace umbraco //check for published content and get its value using that if (publishedContent != null && (publishedContent.HasProperty(_fieldName) || recursive)) { - var pval = publishedContent.Value(_fieldName, fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); + var pval = publishedContent.Value(_fieldName, fallback: Constants.Content.ValueFallback.Recurse); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } @@ -98,7 +98,7 @@ namespace umbraco { if (publishedContent != null && (publishedContent.HasProperty(altFieldName) || recursive)) { - var pval = publishedContent.Value(altFieldName, fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); + var pval = publishedContent.Value(altFieldName, fallback: Constants.Content.ValueFallback.Recurse); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } From 85bdc8f14d4f092c96a25de140ff1b0d9aa3c913 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 28 Jul 2018 07:56:23 +0200 Subject: [PATCH 026/310] Updated XML documentation on IPublisedValueCallback methods --- .../Models/PublishedContent/IPublishedValueFallback.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 96a6c144fa..0c826e6bd5 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -115,6 +115,7 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever getting the property value for the specified alias, culture and @@ -132,6 +133,7 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever getting the property value for the specified alias, culture and From b608ea832277d645664621ddec1f87211ecb282b Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 28 Jul 2018 07:58:22 +0200 Subject: [PATCH 027/310] Restored service context as being DIed into PublishedValueFallback. Whilst only the LocalizationService service is needed, not injecting this via the service context was breaking the unit tests. If there's a way around this so we can use the mocked ILocalizationService in tests then this change can be reverted. --- .../Models/PublishedContent/PublishedValueFallback.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 4136be40d8..71bbe9577a 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -17,10 +17,10 @@ namespace Umbraco.Web.Models.PublishedContent /// /// Initializes a new instance of the class. /// - /// - public PublishedValueFallback(ILocalizationService localizationService) + /// + public PublishedValueFallback(ServiceContext serviceContext) { - _localizationService = localizationService; + _localizationService = serviceContext.LocalizationService; } /// From 49c6212cb63547f1530b8da931853ef938d5374e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 28 Jul 2018 08:11:48 +0200 Subject: [PATCH 028/310] Implemented constants, tests and functionality for trying one fallback method and then another --- src/Umbraco.Core/Constants-Content.cs | 10 ++++ .../PublishedContentLanguageVariantTests.cs | 48 +++++++++++++++---- .../PublishedValueFallback.cs | 8 ++++ 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Constants-Content.cs b/src/Umbraco.Core/Constants-Content.cs index b9d0691454..3f12ece6dc 100644 --- a/src/Umbraco.Core/Constants-Content.cs +++ b/src/Umbraco.Core/Constants-Content.cs @@ -32,6 +32,16 @@ /// Fallback to other languages. /// public const int Language = 2; + + /// + /// Recurse up the tree. If content not found, fallback to other languages. + /// + public const int RecurseThenLanguage = 3; + + /// + /// Fallback to other languages. If content not found, recurse up the tree. + /// + public const int LanguageThenRecurse = 4; } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 0b0f4dea51..d5e01fd424 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Moq; @@ -66,9 +67,9 @@ namespace Umbraco.Tests.PublishedContent var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); var prop1 = new SolidPublishedPropertyWithLanguageVariants - { - Alias = "welcomeText", - }; + { + Alias = "welcomeText", + }; prop1.SetSourceValue("en-US", "Welcome", true); prop1.SetValue("en-US", "Welcome", true); prop1.SetSourceValue("de", "Willkommen"); @@ -77,16 +78,16 @@ namespace Umbraco.Tests.PublishedContent prop1.SetValue("nl", "Welkom"); var prop2 = new SolidPublishedPropertyWithLanguageVariants - { - Alias = "welcomeText2", - }; + { + Alias = "welcomeText2", + }; prop2.SetSourceValue("en-US", "Welcome", true); prop2.SetValue("en-US", "Welcome", true); var prop3 = new SolidPublishedPropertyWithLanguageVariants - { - Alias = "welcomeText", - }; + { + Alias = "welcomeText", + }; prop3.SetSourceValue("en-US", "Welcome", true); prop3.SetValue("en-US", "Welcome", true); @@ -202,5 +203,32 @@ namespace Umbraco.Tests.PublishedContent var value = content.Value("welcomeText2", fallback: Core.Constants.Content.ValueFallback.Recurse); Assert.AreEqual("Welcome", value); } + + [Test] + public void Can_Get_Content_With_Recursive_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallback: Core.Constants.Content.ValueFallback.RecurseThenLanguage); + + // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. + Assert.AreEqual("Welkom", value); + } + + [Test] + public void Can_Get_Content_With_Fallback_Language_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallback: Core.Constants.Content.ValueFallback.LanguageThenRecurse); + + // No Dutch value is directly assigned. Check has fallen back to English value from language variant. + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Throws_For_Non_Supported_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + Assert.Throws(() => content.Value("welcomeText", "nl", fallback: 999)); + } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 71bbe9577a..02e809025a 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -70,6 +70,14 @@ namespace Umbraco.Web.Models.PublishedContent return TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value1) ? value1 : defaultValue; case ValueFallback.Language: return TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value2) ? value2 : defaultValue; + case ValueFallback.RecurseThenLanguage: + return TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value3) + ? value3 + : TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value4) ? value4 : defaultValue; + case ValueFallback.LanguageThenRecurse: + return TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value5) + ? value5 + : TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value6) ? value6 : defaultValue; default: throw new NotSupportedException($"Fallback {GetType().Name} does not support policy code '{fallback}'."); } From c61f041d069ea8a1be9e939a47da8316eb6ec885 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 28 Jul 2018 08:39:02 +0200 Subject: [PATCH 029/310] Added fallback by language option to IPublishedElement and IPublishedProperty --- .../IPublishedValueFallback.cs | 12 +- .../NoopPublishedValueFallback.cs | 8 +- .../PublishedValueFallback.cs | 116 +++++++++++++++--- src/Umbraco.Web/PublishedElementExtensions.cs | 10 +- src/Umbraco.Web/PublishedPropertyExtension.cs | 8 +- 5 files changed, 123 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 0c826e6bd5..c67df36c4f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -46,6 +46,7 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and @@ -54,7 +55,7 @@ /// At property level, property.GetValue() does *not* implement fallback, and one has to /// get property.Value() or property.Value{T}() to trigger fallback. /// - object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); + object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback); /// /// Gets a fallback value for a property. @@ -64,6 +65,7 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and @@ -72,7 +74,7 @@ /// At property level, property.GetValue() does *not* implement fallback, and one has to /// get property.Value() or property.Value{T}() to trigger fallback. /// - T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); + T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, int fallback); /// /// Gets a fallback value for a published element property. @@ -82,13 +84,14 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever getting the property value for the specified alias, culture and /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// It can only fallback at element level (no recurse). /// - object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); + object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, int fallback); /// /// Gets a fallback value for a published element property. @@ -99,13 +102,14 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever getting the property value for the specified alias, culture and /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// It can only fallback at element level (no recurse). /// - T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); + T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, int fallback); /// /// Gets a fallback value for a published content property. diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index 9d74c4d8a2..d920cefb24 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -9,16 +9,16 @@ public class NoopPublishedValueFallback : IPublishedValueFallback { /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue; + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback) => defaultValue; /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue; + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, int fallback) => defaultValue; /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue; + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, int fallback) => defaultValue; /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, int fallback) => defaultValue; /// public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback) => defaultValue; diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 02e809025a..bfcd339650 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -24,31 +24,45 @@ namespace Umbraco.Web.Models.PublishedContent } /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback) { - // no fallback here - return defaultValue; + return GetValue(property, culture, segment, defaultValue, fallback); } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, int fallback) { - // no fallback here - return defaultValue; + switch (fallback) + { + case ValueFallback.None: + case ValueFallback.Default: + return defaultValue; + case ValueFallback.Language: + return TryGetValueWithLanguageFallback(property, culture, segment, defaultValue, out var value2) ? value2 : defaultValue; + default: + throw NotSupportedFallbackMethod(fallback); + } } /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, int fallback) { - // no fallback here - return defaultValue; + return GetValue(content, alias, culture, segment, defaultValue, fallback); } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, int fallback) { - // no fallback here - return defaultValue; + switch (fallback) + { + case ValueFallback.None: + case ValueFallback.Default: + return defaultValue; + case ValueFallback.Language: + return TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value2) ? value2 : defaultValue; + default: + throw NotSupportedFallbackMethod(fallback); + } } /// @@ -79,12 +93,17 @@ namespace Umbraco.Web.Models.PublishedContent ? value5 : TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value6) ? value6 : defaultValue; default: - throw new NotSupportedException($"Fallback {GetType().Name} does not support policy code '{fallback}'."); + throw NotSupportedFallbackMethod(fallback); } } + private NotSupportedException NotSupportedFallbackMethod(int fallback) + { + return new NotSupportedException($"Fallback {GetType().Name} does not support policy code '{fallback}'."); + } + // tries to get a value, recursing the tree - protected static bool TryGetValueWithRecursiveFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + private static bool TryGetValueWithRecursiveFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) { IPublishedProperty property = null; // if we are here, content's property has no value IPublishedProperty noValueProperty = null; @@ -119,6 +138,74 @@ namespace Umbraco.Web.Models.PublishedContent return false; } + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (property.HasValue(culture2, segment)) + { + value = property.Value(culture2, segment); + return true; + } + + language = language2; + } + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(alias, culture2, segment); + return true; + } + + language = language2; + } + } + // tries to get a value, falling back onto other languages private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) { @@ -156,6 +243,5 @@ namespace Umbraco.Web.Models.PublishedContent language = language2; } } - } } diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index 51f0b6c0dc..eea92f4c6c 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -100,6 +100,7 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. + /// Optional fallback strategy. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -107,7 +108,7 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default, int fallback = 0) { var property = content.GetProperty(alias); @@ -119,7 +120,7 @@ namespace Umbraco.Web // and if HasValue is false, what we get is 'null' - but the converter may instead have been able to return an // empty enumerable, which would be way nicer - so we need a way to tell that 'no defaultValue has been provided'? - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallback); } #endregion @@ -135,6 +136,7 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. + /// Optional fallback strategy. /// The value of the content's property identified by the alias, converted to the specified type. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -142,14 +144,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default) + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default, int fallback = 0) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallback); } #endregion diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index bce13d30b4..6632076022 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -16,19 +16,19 @@ namespace Umbraco.Web #region Value - public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default, int fallback = 0) { if (property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(property, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue, fallback); } #endregion #region Value - public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default, ICollection visitedLanguages = null) + public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default, int fallback = 0) { // for Value when defaultValue is not specified, and HasValue() is false, we still want to convert the result (see below) // but we have no way to tell whether default value is specified or not - we could do it with overloads, but then defaultValue @@ -65,7 +65,7 @@ namespace Umbraco.Web //convert = source.TryConvertTo(); //if (convert.Success) return convert.Result; - return defaultValue; + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue, fallback); } #endregion From 79eb9fc2e7c70c7549e9e0d76af20a4e8228b5f0 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 8 Aug 2018 10:34:08 +0100 Subject: [PATCH 030/310] Removed w3cvalidator.browser ref: http://issues.umbraco.org/issue/U4-11555 --- .../App_Browsers/w3cvalidator.browser | 26 ------------------- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 - 2 files changed, 27 deletions(-) delete mode 100644 src/Umbraco.Web.UI/App_Browsers/w3cvalidator.browser diff --git a/src/Umbraco.Web.UI/App_Browsers/w3cvalidator.browser b/src/Umbraco.Web.UI/App_Browsers/w3cvalidator.browser deleted file mode 100644 index ab466c828e..0000000000 --- a/src/Umbraco.Web.UI/App_Browsers/w3cvalidator.browser +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index c90722c08c..69347d0520 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -474,7 +474,6 @@ - From 0869a9be693c6da89dc4bfa14276452f57a683b4 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 8 Aug 2018 11:32:19 +0100 Subject: [PATCH 031/310] Removed Form.browser ref: http://issues.umbraco.org/issue/U4-11555 Also removed the referenced classes... - FormRewriterControlAdapter - UrlRewriterFormWriter - LegacyRequestInitializer These were being used to rewrite the `action` value on rendered `
` tags, (a la WebForms). --- src/Umbraco.Web.UI/App_Browsers/Form.browser | 10 --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 - .../Routing/LegacyRequestInitializer.cs | 36 ---------- src/Umbraco.Web/Umbraco.Web.csproj | 2 - src/Umbraco.Web/UmbracoModule.cs | 5 -- .../urlRewriter/UrlRewriterFormWriter.cs | 69 ------------------- 6 files changed, 123 deletions(-) delete mode 100644 src/Umbraco.Web.UI/App_Browsers/Form.browser delete mode 100644 src/Umbraco.Web/Routing/LegacyRequestInitializer.cs delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/urlRewriter/UrlRewriterFormWriter.cs diff --git a/src/Umbraco.Web.UI/App_Browsers/Form.browser b/src/Umbraco.Web.UI/App_Browsers/Form.browser deleted file mode 100644 index a15cd2c4c1..0000000000 --- a/src/Umbraco.Web.UI/App_Browsers/Form.browser +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 69347d0520..2a37f62462 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -474,7 +474,6 @@ - Designer diff --git a/src/Umbraco.Web/Routing/LegacyRequestInitializer.cs b/src/Umbraco.Web/Routing/LegacyRequestInitializer.cs deleted file mode 100644 index d84ff25d73..0000000000 --- a/src/Umbraco.Web/Routing/LegacyRequestInitializer.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Web; - -namespace Umbraco.Web.Routing -{ - /// - /// A legacy class for old style handling of URL requests - /// - internal class LegacyRequestInitializer - { - private readonly Uri _requestUrl; - private readonly HttpContextBase _httpContext; - - public LegacyRequestInitializer(Uri requestUrl, HttpContextBase httpContext) - { - _requestUrl = requestUrl; - _httpContext = httpContext; - } - - public void InitializeRequest() - { - var uri = _requestUrl; - - // legacy - umbOriginalUrl used by default.aspx to rewritepath so forms are happy - // legacy - umbOriginalUrl used by presentation/umbraco/urlRewriter/UrlRewriterFormWriter which handles - @@ -1347,7 +1346,6 @@ - CheckForUpgrade.asmx diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index bc252dda70..1eff1f2000 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -98,11 +98,6 @@ namespace Umbraco.Web // ok, process - // create the LegacyRequestInitializer - // and initialize legacy stuff - var legacyRequestInitializer = new LegacyRequestInitializer(httpContext.Request.Url, httpContext); - legacyRequestInitializer.InitializeRequest(); - // create the UmbracoContext singleton, one per request, and assign // replace existing if any (eg during app startup, a temp one is created) UmbracoContext.EnsureContext( diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/urlRewriter/UrlRewriterFormWriter.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/urlRewriter/UrlRewriterFormWriter.cs deleted file mode 100644 index 9e1845db6c..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/urlRewriter/UrlRewriterFormWriter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Data; -using System.Configuration; -using System.Web; -using System.Web.Security; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Web.UI.WebControls.WebParts; -using System.Web.UI.HtmlControls; - -namespace umbraco.presentation.urlRewriter -{ - - //TODO: Do we need these????????? - - - public class FormRewriterControlAdapter : System.Web.UI.Adapters.ControlAdapter - { - protected override void Render(HtmlTextWriter writer) - { - base.Render(new UrlRewriterFormWriter(writer)); - } - } - - public class UrlRewriterFormWriter : HtmlTextWriter - { - - public UrlRewriterFormWriter(HtmlTextWriter writer) : base(writer) - { - base.InnerWriter = writer.InnerWriter; - - } - - public UrlRewriterFormWriter(System.IO.TextWriter writer) - : base(writer) - { - - base.InnerWriter = writer; - - } - public override void WriteAttribute(string name, string value, bool fEncode) - { - if (name == "action") - { - HttpContext Context; - Context = HttpContext.Current; - if (Context.Items["ActionAlreadyWritten"] == null) - { - string formAction = ""; - if (Context.Items["VirtualUrl"] != null && !String.IsNullOrEmpty(Context.Items["VirtualUrl"].ToString())) - { - formAction = Context.Items["VirtualUrl"].ToString(); - } - else - { - formAction = Context.Items["umbOriginalUrl"].ToString(); - if (!String.IsNullOrEmpty(Context.Request.Url.Query)) - { - formAction += Context.Request.Url.Query; - } - } - value = formAction; - Context.Items["ActionAlreadyWritten"] = true; - } - } - base.WriteAttribute(name, value, fEncode); - } - } -} From 8f0112c113de9fdbcb3c456408996e42cca1bb6c Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 16 Aug 2018 12:10:04 +1000 Subject: [PATCH 032/310] revert sizing changes - needs more thought... --- .../less/components/application/umb-language-picker.less | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less index 79aa92f58b..8a9daf79c9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less @@ -1,12 +1,7 @@ .umb-language-picker { position: relative; z-index: @zindexDropdown; - - // align bottom of tree root with bottom of first tab header in the editor - // means that there's a hierarchy in the element heights in the left panel, based on their height - ~ #tree .umb-tree-root { - height:60px; - } + } .umb-language-picker__toggle { @@ -16,7 +11,7 @@ padding: 0 20px; cursor: pointer; border-bottom: 1px solid @gray-9; - height: @editorHeaderHeight; // match height to editor header + height: 50px; box-sizing: border-box; } From d16650d395f799f4d6d116019ae4e533842838e1 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 16 Aug 2018 12:10:39 +1000 Subject: [PATCH 033/310] modal needs white background when iframed --- src/Umbraco.Web.UI.Client/src/less/modals.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 5a85c19124..8ef2c8c859 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -75,6 +75,7 @@ bottom: 0px; position: absolute; padding: 0px; + background: #fff; } .umb-dialog .umb-btn-toolbar .umb-control-group{ From 7bc43cb43072b07555508f15bdcdd63d3d712e21 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 16 Aug 2018 12:11:29 +1000 Subject: [PATCH 034/310] sane nesting, adds classes to replace element selectors, remove unused bits --- .../src/less/components/tree/umb-actions.less | 94 +++++---- .../less/components/tree/umb-tree-item.less | 115 ++++++----- .../less/components/tree/umb-tree-root.less | 6 +- .../src/less/components/tree/umb-tree.less | 180 +++++------------- .../application/umb-contextmenu.html | 4 +- .../views/components/tree/umb-tree-item.html | 2 +- .../src/views/content/create.html | 12 +- 7 files changed, 156 insertions(+), 257 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less index aca814b8dd..f52258333d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less @@ -2,11 +2,10 @@ // ------------------------- .umb-actions { margin: 0; - padding: 0px; list-style: none; user-select: none; - li.sep { + .sep { display: block; border-top: 1px solid @gray-9; @@ -15,45 +14,46 @@ } } - a { - white-space: nowrap; - font-size: 15px; - color: @black; - padding: 9px 25px 9px 20px; - text-decoration: none; - cursor: pointer; - display: flex; - align-items: center; - - body.touch & { - padding: 7px 25px 7px 20px; - font-size: 110%; - } - } - - a:hover, - a:focus, - li.selected { - color: @black !important; - background: @gray-10 !important; - } - .menu-label { display: inline-block; vertical-align: middle; padding-left: 15px; } - i { + .icon { font-size: 18px; vertical-align: middle; color: @gray-3; } } +.umb-action-link { + white-space: nowrap; + font-size: 15px; + color: @black; + padding: 9px 25px 9px 20px; + text-decoration: none; + cursor: pointer; + display: flex; + align-items: center; + + body.touch & { + padding: 7px 25px 7px 20px; + font-size: 110%; + } +} + +.umb-action-link:hover, +.umb-action-link:focus, +.umb-action.selected { + color: @black !important; + background: @gray-10 !important; + text-decoration: none; +} + .umb-actions-child { - li { + .umb-action { display: block; &.add { @@ -65,38 +65,34 @@ opacity: 0.4; } } - - .menu-label { - font-size: 14px; - color: @black; - margin-left: 10px; - - small { - font-size: 12px; - display: block; - clear: right; - line-height: 14px; - color: @gray-6; - white-space: normal; - margin-top: 2px; - } - } } - a { + .umb-action-link { clear: both; padding-left: 10px; - - &:hover .menuLabel small { - text-decoration: none !important - } } - i { + .icon { font-size: 30px; min-width: 30px; text-align: center; line-height: 24px; /* set line-height to ensure all icons use same line-height */ } + + .menu-label { + font-size: 14px; + color: @black; + margin-left: 10px; + } + + small { + font-size: 12px; + display: block; + clear: right; + line-height: 14px; + color: @gray-6; + white-space: normal; + margin-top: 2px; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less index d650915eab..002d076461 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less @@ -2,74 +2,71 @@ display: block; min-width: 100%; width: auto; - + &:hover ins { visibility: visible; cursor: pointer } +} - > div { +.umb-tree-item > .umb-tree-item__inner { - &:hover { - a:not(.umb-options) { - overflow: hidden; - margin-right: 6px; - } - } + &:hover .umb-tree-item__label { + overflow: hidden; + margin-right: 6px; + } + + // Loading Animation + // ------------------------ + .l { + width: 100%; + height: 1px; + overflow: hidden; + position: absolute; + left: 0; + bottom: 0; - // Loading Animation - // ------------------------ - &.l { - width: 100%; - height: 1px; - overflow: hidden; - position: absolute; - left: 0; - bottom: 0; - - div { - .umb-loader; - } - } - - a:not(.umb-options) { - padding: 11px 0 7px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1 0 auto; + div { + .umb-loader; } } - &.current > div { - - background: @turquoise-d1; - - // override small icon color - // todo - check usage - &:before { - color: @turquoise-l2; - } - - .umb-options { - - &:hover i { - opacity: .7; - } - - i { - background: @white; - border-color: @turquoise-d1; - transition: opacity 120ms ease; - } - } - - a, - i.icon, - ins { - color: @white !important; - background-color: @turquoise-d1; - border-color: @turquoise-d1; - } + .umb-tree-item__label { + padding: 7px 0 5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 0 auto; + } +} + +.umb-tree-item.current > .umb-tree-item__inner { + + background: @turquoise-d1; + + // override small icon color. TODO => check usage +// &:before { +// color: @turquoise-l2; +// } + + .umb-options { + + &:hover i { + opacity: .7; + } + + i { + background: @white; + border-color: @turquoise-d1; + transition: opacity 120ms ease; + } + } + + a, + .umb-tree-icon, + ins { + color: @white !important; + background-color: @turquoise-d1; + border-color: @turquoise-d1; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less index f6c19c1dbb..f2b26fa925 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less @@ -1,9 +1,9 @@ .umb-tree-root { height:@editorHeaderHeight; - div& { - align-items:inherit; - } +// div& { +// align-items:inherit; +// } &-link { display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 815e430eb9..4bb789856f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -5,35 +5,20 @@ min-width: 100%; width: auto; padding: 0 0 20px; - + list-style-type: none; + * { white-space: nowrap; } - div { - padding: 0; - position: relative; - overflow: hidden; - display: flex; - flex-wrap: nowrap; - align-items: center; - - &:hover { - background: @gray-10; - - > .umb-options { - visibility: visible; - } - } - } - a, a:hover { outline: none; text-decoration: none; - &.noSpr { - background-position: 0 - } + // TODO => confirm not in use +// &.noSpr { +// background-position: 0 +// } } ins { @@ -67,37 +52,8 @@ &.collapsed { display: none; } - } // .umb-tree-item { - // display: block; - // min-width: 100%; - // width: auto; - // } - // - // li.current > div, - // div.selected { - // background: @turquoise-d1; - // } - // - // li.current > div a.umb-options i, - // div.selected i { - // background: @white; - // border-color: @turquoise-d1; - // transition: opacity 120ms ease; - // } - // - // li.current > div a.umb-options:hover i, - // div.selected i { - // opacity: .7; - // } - // - // li.current > div a, - // li.current > div i.icon, - // li.current > div ins { - // color: @white !important; - // background-color: @turquoise-d1; - // border-color: @turquoise-d1; - // } - // + } + //loader defaults .umb-loader { height: 10px; @@ -118,7 +74,7 @@ body.touch .umb-tree { padding: 7px; } - .umb-tree-item > div { + .umb-tree-item > .umb-tree-item__inner { padding-top: 8px; padding-bottom: 8px; font-size: 110%; @@ -130,6 +86,23 @@ body.touch .umb-tree { } } +.umb-tree-root, .umb-tree-item__inner { + padding: 0; + position: relative; + overflow: hidden; + display: flex; + flex-wrap: nowrap; + align-items: center; + + &:hover { + background: @gray-10; + + > .umb-options { + visibility: visible; + } + } +} + .umb-tree-header { display: flex; padding: 20px 0 20px 20px; @@ -215,67 +188,7 @@ body.touch .umb-tree { } } -// todo -> confirm not in use. not referenced in any other code -// -//.hide-header h5 { -// display: none !important -//} -//.umb-icon-item { -// padding: 2px; -// padding-left: 55px; -// display: block; -// position: relative; -// -// &:hover { -// background: @gray-10; -// -// .umb-options { -// visibility: visible -// } -// } -// -// a { -// color: @gray-3; -// padding-top: 3px; -// height: 15px; -// font-size: 12px; -// text-decoration: none; -// -// &:hover div { -// text-decoration: underline; -// } -// } -// -// small { -// color: @gray-6; -// font-size: 10px; -// display: block; -// } -// -// .icon { -// position: absolute; -// top: 8px; -// left: 19px; -// } -// -// .umb-spr { -// float: left -// } -//} -// item-list -// todo -> verify not in use. not mentioned elsewhere in codebase... -// ------------------------- -//.umb-item-list { -// margin: 0; -// width: auto; -// display: block; -// -// li { -// display: block; -// width: auto; -// } -//} // Tree item states // ------------------------- .not-published { @@ -306,36 +219,29 @@ body.touch .umb-tree { } } -.protected { - &::before { - content: "\e256"; - color: @red; - } +.protected::before { + content: "\e256"; + color: @red; } -.has-unpublished-version { - &::before { - content: "\e25a"; - color: @green; - } +.has-unpublished-version::before { + content: "\e25a"; + color: @green; } -.is-container { - &::before { - content: "\e04e"; - color: @turquoise; - font-size: 8px; - padding-left: 13px; - padding-top: 8px; - pointer-events: none; - } +.is-container::before { + content: "\e04e"; + color: @turquoise; + font-size: 8px; + padding-left: 13px; + padding-top: 8px; + pointer-events: none; } -.locked { - &::before { - content: "\e0a7"; - color: @red; - } + +.locked::before { + content: "\e0a7"; + color: @red; } .no-access { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html index 0bb4b1ed2e..32dd57ade3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html @@ -5,8 +5,8 @@
    -
  • - +
  • + {{action.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html index 5e6fe6d662..7492caa8d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html @@ -1,5 +1,5 @@
  • -
    +
    -
  • - +
  • + {{docType.name}} @@ -25,8 +25,8 @@