Starts wiring up the back office to the c# bits, updates controllers, mappers, models, property editors to support getting and saving data by language. The content editor now "works" with multi-lingual properties

This commit is contained in:
Shannon
2018-04-04 01:59:51 +10:00
parent e7bc4986a5
commit 7a73175aa0
39 changed files with 463 additions and 203 deletions

View File

@@ -1,33 +0,0 @@
using System;
using AutoMapper;
namespace Umbraco.Web.Models.Mapping
{
/// <summary>
/// Extends AutoMapper's <see cref="Mapper"/> class to handle Umbraco's context.
/// </summary>
internal static class ContextMapper
{
private const string UmbracoContextKey = "ContextMapper.UmbracoContext";
public static TDestination Map<TSource, TDestination>(TSource obj, UmbracoContext umbracoContext)
=> Mapper.Map<TSource, TDestination>(obj, opt => opt.Items[UmbracoContextKey] = umbracoContext);
public static UmbracoContext GetUmbracoContext(this ResolutionContext resolutionContext, bool throwIfMissing = true)
{
if (resolutionContext.Options.Items.TryGetValue(UmbracoContextKey, out var obj) && obj is UmbracoContext umbracoContext)
return umbracoContext;
// fixme - not a good idea at all
// because this falls back to magic singletons
// so really we should remove this line, but then some tests+app breaks ;(
return Umbraco.Web.Composing.Current.UmbracoContext;
// better fail fast
if (throwIfMissing)
throw new InvalidOperationException("AutoMapper ResolutionContext does not contain an UmbracoContext.");
return null;
}
}
}

View File

@@ -3,9 +3,11 @@ using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Umbraco.Web.Models.ContentEditing;
using ContentVariation = Umbraco.Core.Models.ContentVariation;
namespace Umbraco.Web.Models.Mapping
{
@@ -15,10 +17,14 @@ namespace Umbraco.Web.Models.Mapping
internal class ContentPropertyBasicConverter<TDestination> : ITypeConverter<Property, TDestination>
where TDestination : ContentPropertyBasic, new()
{
private readonly ILogger _logger;
private readonly PropertyEditorCollection _propertyEditors;
protected IDataTypeService DataTypeService { get; }
public ContentPropertyBasicConverter(IDataTypeService dataTypeService)
public ContentPropertyBasicConverter(IDataTypeService dataTypeService, ILogger logger, PropertyEditorCollection propertyEditors)
{
_logger = logger;
_propertyEditors = propertyEditors;
DataTypeService = dataTypeService;
}
@@ -28,19 +34,28 @@ namespace Umbraco.Web.Models.Mapping
/// <returns></returns>
public virtual TDestination Convert(Property property, TDestination dest, ResolutionContext context)
{
var editor = Current.PropertyEditors[property.PropertyType.PropertyEditorAlias];
var editor = _propertyEditors[property.PropertyType.PropertyEditorAlias];
if (editor == null)
{
Current.Logger.Error<ContentPropertyBasicConverter<TDestination>>(
_logger.Error<ContentPropertyBasicConverter<TDestination>>(
"No property editor found, converting to a Label",
new NullReferenceException("The property editor with alias " + property.PropertyType.PropertyEditorAlias + " does not exist"));
editor = Current.PropertyEditors[Constants.PropertyEditors.Aliases.NoEdit];
editor = _propertyEditors[Constants.PropertyEditors.Aliases.NoEdit];
}
var languageId = context.GetLanguageId();
if (!languageId.HasValue && property.PropertyType.Variations == ContentVariation.CultureNeutral)
{
//a language Id needs to be set for a property type that can be varried by language
throw new InvalidOperationException($"No languageId found in mapping operation when one is required for the culture neutral property type {property.PropertyType.Alias}");
}
var result = new TDestination
{
Id = property.Id,
Value = editor.GetValueEditor().ToEditor(property, DataTypeService),
Value = editor.GetValueEditor().ToEditor(property, DataTypeService, languageId),
Alias = property.Alias,
PropertyEditor = editor,
Editor = editor.Alias

View File

@@ -3,6 +3,7 @@ using System.Linq;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
@@ -17,8 +18,8 @@ namespace Umbraco.Web.Models.Mapping
{
private readonly ILocalizedTextService _textService;
public ContentPropertyDisplayConverter(IDataTypeService dataTypeService, ILocalizedTextService textService)
: base(dataTypeService)
public ContentPropertyDisplayConverter(IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors)
: base(dataTypeService, logger, propertyEditors)
{
_textService = textService;
}

View File

@@ -1,6 +1,8 @@
using System;
using AutoMapper;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
@@ -11,8 +13,8 @@ namespace Umbraco.Web.Models.Mapping
/// </summary>
internal class ContentPropertyDtoConverter : ContentPropertyBasicConverter<ContentPropertyDto>
{
public ContentPropertyDtoConverter(IDataTypeService dataTypeService)
: base(dataTypeService)
public ContentPropertyDtoConverter(IDataTypeService dataTypeService, ILogger logger, PropertyEditorCollection propertyEditors)
: base(dataTypeService, logger, propertyEditors)
{ }
public override ContentPropertyDto Convert(Property originalProperty, ContentPropertyDto dest, ResolutionContext context)

View File

@@ -1,5 +1,7 @@
using AutoMapper;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
@@ -11,11 +13,11 @@ namespace Umbraco.Web.Models.Mapping
/// </summary>
internal class ContentPropertyMapperProfile : Profile
{
public ContentPropertyMapperProfile(IDataTypeService dataTypeService, ILocalizedTextService textService)
public ContentPropertyMapperProfile(IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors)
{
var contentPropertyBasicConverter = new ContentPropertyBasicConverter<ContentPropertyBasic>(dataTypeService);
var contentPropertyDtoConverter = new ContentPropertyDtoConverter(dataTypeService);
var contentPropertyDisplayConverter = new ContentPropertyDisplayConverter(dataTypeService, textService);
var contentPropertyBasicConverter = new ContentPropertyBasicConverter<ContentPropertyBasic>(dataTypeService, logger, propertyEditors);
var contentPropertyDtoConverter = new ContentPropertyDtoConverter(dataTypeService, logger, propertyEditors);
var contentPropertyDisplayConverter = new ContentPropertyDisplayConverter(dataTypeService, textService, logger, propertyEditors);
//FROM Property TO ContentPropertyBasic
CreateMap<PropertyGroup, Tab<ContentPropertyDisplay>>()
@@ -25,13 +27,13 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(tab => tab.Alias, expression => expression.Ignore());
//FROM Property TO ContentPropertyBasic
CreateMap<Property, ContentPropertyBasic>().ConvertUsing(contentPropertyBasicConverter);
CreateMap<Property, ContentPropertyBasic>().ConvertUsing((property, basic, arg3) => contentPropertyBasicConverter.Convert(property, basic, arg3));
//FROM Property TO ContentPropertyDto
CreateMap<Property, ContentPropertyDto>().ConvertUsing(contentPropertyDtoConverter);
//FROM Property TO ContentPropertyDisplay
CreateMap<Property, ContentPropertyDisplay>().ConvertUsing(contentPropertyDisplayConverter);
CreateMap<Property, ContentPropertyDisplay>().ConvertUsing((property, basic, arg3) => contentPropertyDisplayConverter.Convert(property, basic, arg3));
}
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using AutoMapper;
using ClientDependency.Core;
namespace Umbraco.Web.Models.Mapping
{
/// <summary>
/// Extends AutoMapper's <see cref="Mapper"/> class to handle Umbraco's context and other contextual data
/// </summary>
internal static class ContextMapper
{
public const string UmbracoContextKey = "ContextMapper.UmbracoContext";
public const string LanguageKey = "ContextMapper.LanguageId";
public static TDestination Map<TSource, TDestination>(TSource obj, UmbracoContext umbracoContext)
=> Mapper.Map<TSource, TDestination>(obj, opt => opt.Items[UmbracoContextKey] = umbracoContext);
public static TDestination Map<TSource, TDestination>(TSource obj, UmbracoContext umbracoContext, IDictionary<string, object> contextVals)
=> Mapper.Map<TSource, TDestination>(obj, opt =>
{
//set the umb ctx
opt.Items[UmbracoContextKey] = umbracoContext;
//set other supplied context vals
if (contextVals != null)
{
foreach (var contextVal in contextVals)
{
opt.Items[contextVal.Key] = contextVal.Value;
}
}
});
public static TDestination Map<TSource, TDestination>(TSource obj, UmbracoContext umbracoContext, object contextVals)
=> Mapper.Map<TSource, TDestination>(obj, opt =>
{
//set the umb ctx
opt.Items[UmbracoContextKey] = umbracoContext;
//set other supplied context vals
if (contextVals != null)
{
foreach (var contextVal in contextVals.ToDictionary())
{
opt.Items[contextVal.Key] = contextVal.Value;
}
}
});
public static TDestination Map<TSource, TDestination>(TSource obj, IDictionary<string, object> contextVals)
=> Mapper.Map<TSource, TDestination>(obj, opt =>
{
//set other supplied context vals
if (contextVals != null)
{
foreach (var contextVal in contextVals)
{
opt.Items[contextVal.Key] = contextVal.Value;
}
}
});
public static TDestination Map<TSource, TDestination>(TSource obj, object contextVals)
=> Mapper.Map<TSource, TDestination>(obj, opt =>
{
//set other supplied context vals
if (contextVals != null)
{
foreach (var contextVal in contextVals.ToDictionary())
{
opt.Items[contextVal.Key] = contextVal.Value;
}
}
});
/// <summary>
/// Returns the language Id in the mapping context if one is found
/// </summary>
/// <param name="resolutionContext"></param>
/// <returns></returns>
public static int? GetLanguageId(this ResolutionContext resolutionContext)
{
if (!resolutionContext.Options.Items.TryGetValue(LanguageKey, out var obj)) return null;
if (obj is int i)
return i;
return null;
}
/// <summary>
/// Returns the <see cref="UmbracoContext"/> in the mapping context if one is found
/// </summary>
/// <param name="resolutionContext"></param>
/// <param name="throwIfMissing"></param>
/// <returns></returns>
public static UmbracoContext GetUmbracoContext(this ResolutionContext resolutionContext, bool throwIfMissing = true)
{
if (resolutionContext.Options.Items.TryGetValue(UmbracoContextKey, out var obj) && obj is UmbracoContext umbracoContext)
return umbracoContext;
// better fail fast
if (throwIfMissing)
throw new InvalidOperationException("AutoMapper ResolutionContext does not contain an UmbracoContext.");
// fixme - not a good idea at all
// because this falls back to magic singletons
// so really we should remove this line, but then some tests+app breaks ;(
return Umbraco.Web.Composing.Current.UmbracoContext;
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Globalization;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using AutoMapper;
using Umbraco.Core.Models;
using Umbraco.Web.Models.ContentEditing;
@@ -12,6 +14,38 @@ namespace Umbraco.Web.Models.Mapping
{
CreateMap<ILanguage, Language>()
.ForMember(l => l.Name, expression => expression.MapFrom(x => x.CultureInfo.DisplayName));
CreateMap<IEnumerable<ILanguage>, IEnumerable<Language>>()
.ConvertUsing<LanguageCollectionTypeConverter>();
}
/// <summary>
/// Converts a list of <see cref="ILanguage"/> to a list of <see cref="Language"/> and ensures the correct order and defaults are set
/// </summary>
// ReSharper disable once ClassNeverInstantiated.Local
private class LanguageCollectionTypeConverter : ITypeConverter<IEnumerable<ILanguage>, IEnumerable<Language>>
{
public IEnumerable<Language> Convert(IEnumerable<ILanguage> source, IEnumerable<Language> destination, ResolutionContext context)
{
var allLanguages = source.OrderBy(x => x.Id).ToList();
var langs = new List<Language>(allLanguages.Select(x => context.Mapper.Map<ILanguage, Language>(x, null, context)));
//if there's only one language, by default it is the default
if (langs.Count == 1)
{
langs[0].IsDefaultVariantLanguage = true;
langs[0].Mandatory = true;
}
else if (allLanguages.All(x => !x.IsDefaultVariantLanguage))
{
//if no language has the default flag, then the defaul language is the one with the lowest id
langs[0].IsDefaultVariantLanguage = true;
langs[0].Mandatory = true;
}
return langs.OrderBy(x => x.Name);
}
}
}
}

View File

@@ -169,10 +169,11 @@ namespace Umbraco.Web.Models.Mapping
/// <param name="umbracoContext"></param>
/// <param name="content"></param>
/// <param name="properties"></param>
/// <param name="context"></param>
/// <returns></returns>
protected override List<ContentPropertyDisplay> MapProperties(UmbracoContext umbracoContext, IContentBase content, List<Property> properties)
protected override List<ContentPropertyDisplay> MapProperties(UmbracoContext umbracoContext, IContentBase content, List<Property> properties, ResolutionContext context)
{
var result = base.MapProperties(umbracoContext, content, properties);
var result = base.MapProperties(umbracoContext, content, properties, context);
var member = (IMember)content;
var memberType = member.ContentType;

View File

@@ -152,18 +152,19 @@ namespace Umbraco.Web.Models.Mapping
/// <param name="umbracoContext"></param>
/// <param name="content"></param>
/// <param name="tabs"></param>
/// <param name="context"></param>
/// <remarks>
/// The generic properties tab is responsible for
/// setting up the properties such as Created date, updated date, template selected, etc...
/// </remarks>
protected virtual void MapGenericProperties(UmbracoContext umbracoContext, IContentBase content, List<Tab<ContentPropertyDisplay>> tabs)
protected virtual void MapGenericProperties(UmbracoContext umbracoContext, IContentBase content, List<Tab<ContentPropertyDisplay>> tabs, ResolutionContext context)
{
// add the generic properties tab, for properties that don't belong to a tab
// get the properties, map and translate them, then add the tab
var noGroupProperties = content.GetNonGroupedProperties()
.Where(x => IgnoreProperties.Contains(x.Alias) == false) // skip ignored
.ToList();
var genericproperties = MapProperties(umbracoContext, content, noGroupProperties);
var genericproperties = MapProperties(umbracoContext, content, noGroupProperties, context);
tabs.Add(new Tab<ContentPropertyDisplay>
{
@@ -212,12 +213,16 @@ namespace Umbraco.Web.Models.Mapping
/// <param name="umbracoContext"></param>
/// <param name="content"></param>
/// <param name="properties"></param>
/// <param name="context"></param>
/// <returns></returns>
protected virtual List<ContentPropertyDisplay> MapProperties(UmbracoContext umbracoContext, IContentBase content, List<Property> properties)
{
var result = Mapper.Map<IEnumerable<Property>, IEnumerable<ContentPropertyDisplay>>(
protected virtual List<ContentPropertyDisplay> MapProperties(UmbracoContext umbracoContext, IContentBase content, List<Property> properties, ResolutionContext context)
{
//we need to map this way to pass the context through, I don't like it but we'll see what AutoMapper says: https://github.com/AutoMapper/AutoMapper/issues/2588
var result = context.Mapper.Map<IEnumerable<Property>, IEnumerable<ContentPropertyDisplay>>(
// Sort properties so items from different compositions appear in correct order (see U4-9298). Map sorted properties.
properties.OrderBy(prop => prop.PropertyType.SortOrder))
properties.OrderBy(prop => prop.PropertyType.SortOrder),
null,
context)
.ToList();
return result;
@@ -265,7 +270,7 @@ namespace Umbraco.Web.Models.Mapping
continue;
//map the properties
var mappedProperties = MapProperties(umbracoContext, source, properties);
var mappedProperties = MapProperties(umbracoContext, source, properties, context);
// add the tab
// we need to pick an identifier... there is no "right" way...
@@ -283,7 +288,7 @@ namespace Umbraco.Web.Models.Mapping
});
}
MapGenericProperties(umbracoContext, source, tabs);
MapGenericProperties(umbracoContext, source, tabs, context);
// activate the first tab, if any
if (tabs.Count > 0)

View File

@@ -24,7 +24,7 @@ namespace Umbraco.Web.Models.Mapping
var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList();
if (allLanguages.Count == 0) return Enumerable.Empty<ContentVariation>(); //there's only 1 language defined so we don't have language variants enabled
var langs = Mapper.Map<IEnumerable<Language>>(allLanguages).ToList();
var langs = context.Mapper.Map<IEnumerable<ILanguage>, IEnumerable<Language>>(allLanguages, null, context);
var variants = langs.Select(x => new ContentVariation
{
Language = x,
@@ -33,26 +33,14 @@ namespace Umbraco.Web.Models.Mapping
ExpireDate = source.ExpireDate,
PublishDate = source.PublishDate,
ReleaseDate = source.ReleaseDate,
Exists = source.HasVariation(x.Id),
Exists = source.HasVariation(x.Id), //TODO: This needs to be wired up with new APIs when they are ready
PublishedState = source.PublishedState.ToString()
}).ToList();
//if there's only one language, by default it is the default
if (langs.Count == 1)
{
langs[0].IsDefaultVariantLanguage = true;
langs[0].Mandatory = true;
}
else if (allLanguages.All(x => !x.IsDefaultVariantLanguage))
{
//if no language has the default flag, then the defaul language is the one with the lowest id
langs[0].IsDefaultVariantLanguage = true;
langs[0].Mandatory = true;
}
var langId = context.GetLanguageId();
//TODO: Not sure if this is required right now, IsCurrent could purely be a UI thing, we'll see
//set the 'current'
variants.First(x => x.Language.IsDefaultVariantLanguage).IsCurrent = true;
//set the current variant being edited to the one found in the context or the default, whichever matches
variants.First(x => (langId.HasValue && langId.Value == x.Language.Id) || x.Language.IsDefaultVariantLanguage).IsCurrent = true;
return variants;
}