Merge remote-tracking branch 'origin/temp8' into temp8-logviewer

This commit is contained in:
Warren Buckley
2018-09-30 21:56:17 +01:00
39 changed files with 835 additions and 634 deletions
-1
View File
@@ -40,7 +40,6 @@
<!-- these files are copied by install.ps1 -->
<file src="$BuildTmp$\WebApp\Web.config" target="UmbracoFiles\Web.config" />
<file src="$BuildTmp$\WebApp\App_Browsers\**" target="UmbracoFiles\App_Browsers" />
<file src="$BuildTmp$\WebApp\umbraco\**" target="UmbracoFiles\umbraco" />
<file src="$BuildTmp$\WebApp\config\splashes\**" target="UmbracoFiles\Config\splashes" />
@@ -118,6 +118,7 @@ namespace Umbraco.Core.Migrations.Upgrade
Chain<RenameTrueFalseField>("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
Chain<SetDefaultTagsStorageType>("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
Chain<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}");
Chain<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
//FINAL
@@ -0,0 +1,79 @@
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
public class RefactorVariantsModel : MigrationBase
{
public RefactorVariantsModel(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
Delete.Column("edited").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do();
// add available column
AddColumn<DocumentCultureVariationDto>("available", out var sqls);
// so far, only those cultures that were available had records in the table
Update.Table(DocumentCultureVariationDto.TableName).Set(new { available = true }).AllRows().Do();
foreach (var sql in sqls) Execute.Sql(sql).Do();
// add published column
AddColumn<DocumentCultureVariationDto>("published", out sqls);
// make it false by default
Update.Table(DocumentCultureVariationDto.TableName).Set(new { published = false }).AllRows().Do();
// now figure out whether these available cultures are published, too
var getPublished = Sql()
.Select<NodeDto>(x => x.NodeId)
.AndSelect<ContentVersionCultureVariationDto>(x => x.LanguageId)
.From<NodeDto>()
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((node, cv) => node.NodeId == cv.NodeId)
.InnerJoin<DocumentVersionDto>().On<ContentVersionDto, DocumentVersionDto>((cv, dv) => cv.Id == dv.Id && dv.Published)
.InnerJoin<ContentVersionCultureVariationDto>().On<ContentVersionDto, ContentVersionCultureVariationDto>((cv, ccv) => cv.Id == ccv.VersionId);
foreach (var dto in Database.Fetch<TempDto>(getPublished))
Database.Execute(Sql()
.Update<DocumentCultureVariationDto>(u => u.Set(x => x.Published, true))
.Where<DocumentCultureVariationDto>(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId));
foreach (var sql in sqls) Execute.Sql(sql).Do();
// so far, it was kinda impossible to make a culture unavailable again,
// so we *should* not have anything published but not available - ignore
// add name column
AddColumn<DocumentCultureVariationDto>("name");
// so far, every record in the table mapped to an available culture
var getNames = Sql()
.Select<NodeDto>(x => x.NodeId)
.AndSelect<ContentVersionCultureVariationDto>(x => x.LanguageId, x => x.Name)
.From<NodeDto>()
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((node, cv) => node.NodeId == cv.NodeId && cv.Current)
.InnerJoin<ContentVersionCultureVariationDto>().On<ContentVersionDto, ContentVersionCultureVariationDto>((cv, ccv) => cv.Id == ccv.VersionId);
foreach (var dto in Database.Fetch<TempDto>(getNames))
Database.Execute(Sql()
.Update<DocumentCultureVariationDto>(u => u.Set(x => x.Name, dto.Name))
.Where<DocumentCultureVariationDto>(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId));
}
// ReSharper disable once ClassNeverInstantiated.Local
// ReSharper disable UnusedAutoPropertyAccessor.Local
private class TempDto
{
public int NodeId { get; set; }
public int LanguageId { get; set; }
public string Name { get; set; }
}
// ReSharper restore UnusedAutoPropertyAccessor.Local
}
}
+2 -2
View File
@@ -269,7 +269,7 @@ namespace Umbraco.Core.Models
if (_publishInfos == null)
_publishInfos = new Dictionary<string, (string Name, DateTime Date)>(StringComparer.OrdinalIgnoreCase);
_publishInfos[culture] = (name, date);
_publishInfos[culture.ToLowerInvariant()] = (name, date);
}
private void ClearPublishInfos()
@@ -294,7 +294,7 @@ namespace Umbraco.Core.Models
throw new ArgumentNullOrEmptyException(nameof(culture));
if (_editedCultures == null)
_editedCultures = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_editedCultures.Add(culture);
_editedCultures.Add(culture.ToLowerInvariant());
}
// sets all publish edited
+7 -1
View File
@@ -167,7 +167,7 @@ namespace Umbraco.Core.Models
}
/// <inheritdoc />
public DateTime? GetCultureDate(string culture)
public DateTime? GetUpdateDate(string culture)
{
if (culture.IsNullOrWhiteSpace()) return null;
if (!ContentTypeBase.VariesByCulture()) return null;
@@ -202,6 +202,12 @@ namespace Umbraco.Core.Models
}
}
internal void TouchCulture(string culture)
{
if (ContentTypeBase.VariesByCulture() && _cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos))
_cultureInfos[culture] = (infos.Name, DateTime.Now);
}
protected void ClearCultureInfos()
{
_cultureInfos = null;
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Core.Models.Entities
{
@@ -8,13 +9,32 @@ namespace Umbraco.Core.Models.Entities
public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim
{
private static readonly IReadOnlyDictionary<string, string> Empty = new Dictionary<string, string>();
private IReadOnlyDictionary<string, string> _cultureNames;
private IEnumerable<string> _publishedCultures;
private IEnumerable<string> _editedCultures;
/// <inheritdoc />
public IReadOnlyDictionary<string, string> CultureNames
{
get => _cultureNames ?? Empty;
set => _cultureNames = value;
}
/// <inheritdoc />
public IEnumerable<string> PublishedCultures
{
get => _publishedCultures ?? Enumerable.Empty<string>();
set => _publishedCultures = value;
}
/// <inheritdoc />
public IEnumerable<string> EditedCultures
{
get => _editedCultures ?? Enumerable.Empty<string>();
set => _editedCultures = value;
}
public ContentVariation Variations { get; set; }
/// <inheritdoc />
@@ -22,5 +42,6 @@ namespace Umbraco.Core.Models.Entities
/// <inheritdoc />
public bool Edited { get; set; }
}
}
@@ -20,4 +20,4 @@
/// </summary>
string ContentTypeThumbnail { get; }
}
}
}
@@ -7,26 +7,35 @@ namespace Umbraco.Core.Models.Entities
/// </summary>
public interface IDocumentEntitySlim : IContentEntitySlim
{
//fixme we need to supply more information than this and change this property name. This will need to include Published/Editor per variation since we need this information for the tree
/// <summary>
/// Gets the variant name for each culture
/// </summary>
IReadOnlyDictionary<string, string> CultureNames { get; }
/// <summary>
/// Gets the published cultures.
/// </summary>
IEnumerable<string> PublishedCultures { get; }
/// <summary>
/// Gets the edited cultures.
/// </summary>
IEnumerable<string> EditedCultures { get; }
/// <summary>
/// Gets the content variation of the content type.
/// </summary>
ContentVariation Variations { get; }
/// <summary>
/// At least one variation is published
/// Gets a value indicating whether the content is published.
/// </summary>
/// <remarks>
/// If the document is invariant, this simply means there is a published version
/// </remarks>
bool Published { get; set; }
bool Published { get; }
/// <summary>
/// At least one variation has pending changes
/// Gets a value indicating whether the content has been edited.
/// </summary>
/// <remarks>
/// If the document is invariant, this simply means there is pending changes
/// </remarks>
bool Edited { get; set; }
bool Edited { get; }
}
}
+2 -2
View File
@@ -80,13 +80,13 @@ namespace Umbraco.Core.Models
bool IsCultureAvailable(string culture);
/// <summary>
/// Gets the date a culture was created.
/// Gets the date a culture was updated.
/// </summary>
/// <remarks>
/// <para>When <paramref name="culture" /> is <c>null</c>, returns <c>null</c>.</para>
/// <para>If the specified culture is not available, returns <c>null</c>.</para>
/// </remarks>
DateTime? GetCultureDate(string culture);
DateTime? GetUpdateDate(string culture);
/// <summary>
/// List of properties, which make up all the data available for this Content object
@@ -6,7 +6,7 @@
/// </summary>
/// <remarks>Every strongly-typed property set class should inherit from <c>PublishedElementModel</c>
/// (or inherit from a class that inherits from... etc.) so they are picked by the factory.</remarks>
public class PublishedElementModel : PublishedElementWrapped
public abstract class PublishedElementModel : PublishedElementWrapped
{
/// <inheritdoc />
/// <summary>
@@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.Dtos
internal class ContentVersionCultureVariationDto
{
public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCultureVariation;
private int? _publishedUserId;
private int? _updateUserId;
[Column("id")]
[PrimaryKeyColumn]
@@ -33,16 +33,12 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("name")]
public string Name { get; set; }
[Column("date")]
public DateTime Date { get; set; }
[Column("date")] // fixme: db rename to 'updateDate'
public DateTime UpdateDate { get; set; }
// fixme want?
[Column("availableUserId")]
[Column("availableUserId")] // fixme: db rename to 'updateDate'
[ForeignKey(typeof(UserDto))]
[NullSetting(NullSetting = NullSettings.Null)]
public int? PublishedUserId { get => _publishedUserId == 0 ? null : _publishedUserId; set => _publishedUserId = value; } //return null if zero
[Column("edited")]
public bool Edited { get; set; }
public int? UpdateUserId { get => _updateUserId == 0 ? null : _updateUserId; set => _updateUserId = value; } //return null if zero
}
}
@@ -21,11 +21,11 @@ namespace Umbraco.Core.Persistence.Dtos
[ForeignKey(typeof(ContentDto))]
public int NodeId { get; set; }
[Column("versionDate")]
[Column("versionDate")] // fixme: db rename to 'updateDate'
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime VersionDate { get; set; }
[Column("userId")]
[Column("userId")] // fixme: db rename to 'updateUserId'
[ForeignKey(typeof(UserDto))]
[NullSetting(NullSetting = NullSettings.Null)]
public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero
@@ -28,7 +28,25 @@ namespace Umbraco.Core.Persistence.Dtos
[Ignore]
public string Culture { get; set; }
// authority on whether a culture has been edited
[Column("edited")]
public bool Edited { get; set; }
// de-normalized for perfs
// (means there is a current content version culture variation for the language)
[Column("available")]
public bool Available { get; set; }
// de-normalized for perfs
// (means there is a published content version culture variation for the language)
[Column("published")]
public bool Published { get; set; }
// de-normalized for perfs
// (when available, copies name from current content version culture variation for the language)
// (otherwise, it's the published one, 'cos we need to have one)
[Column("name")]
[NullSetting(NullSetting = NullSettings.Null)]
public string Name { get; set; }
}
}
+3 -3
View File
@@ -45,7 +45,7 @@ namespace Umbraco.Core.Persistence.Dtos
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")]
public bool Trashed { get; set; }
[Column("nodeUser")] // fixme dbfix rename userId
[Column("nodeUser")] // fixme: db rename to 'createUserId'
[ForeignKey(typeof(UserDto))]
[NullSetting(NullSetting = NullSettings.Null)]
public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero
@@ -54,10 +54,10 @@ namespace Umbraco.Core.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.Null)]
public string Text { get; set; }
[Column("nodeObjectType")] // fixme dbfix rename objectType
[Column("nodeObjectType")] // fixme: db rename to 'objectType'
[NullSetting(NullSetting = NullSettings.Null)]
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType")]
public Guid? NodeObjectType { get; set; } // fixme dbfix rename ObjectType
public Guid? NodeObjectType { get; set; }
[Column("createDate")]
[Constraint(Default = SystemMethods.CurrentDateTime)]
@@ -76,26 +76,7 @@ namespace Umbraco.Core.Persistence
var (s, a) = sql.SqlContext.Visit(predicate, alias);
return sql.Where(s, a);
}
/// <summary>
/// Appends an AND clause to a WHERE Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the Dto.</typeparam>
/// <param name="sql">The Sql statement.</param>
/// <param name="predicate">A predicate to transform and append to the Sql statement.</param>
/// <param name="alias">An optional alias for the table.</param>
/// <returns>The Sql statement.</returns>
/// <remarks>
/// <para>Chaining <c>.Where(...).Where(...)</c> in NPoco works because it merges the two WHERE statements,
/// however if the first statement is not an explicit WHERE statement, chaining fails and two WHERE
/// statements appear in the resulting Sql. This allows for adding an AND clause without problems.</para>
/// </remarks>
public static Sql<ISqlContext> AndWhere<TDto>(this Sql<ISqlContext> sql, Expression<Func<TDto, bool>> predicate, string alias = null)
{
var (s, a) = sql.SqlContext.Visit(predicate, alias);
return sql.Append("AND (" + s + ")", a);
}
/// <summary>
/// Appends a WHERE clause to the Sql statement.
/// </summary>
@@ -411,6 +392,23 @@ namespace Umbraco.Core.Persistence
#region Joins
/// <summary>
/// Appends a CROSS JOIN clause to the Sql statement.
/// </summary>
/// <typeparam name="TDto">The type of the Dto.</typeparam>
/// <param name="sql">The Sql statement.</param>
/// <param name="alias">An optional alias for the joined table.</param>
/// <returns>The Sql statement.</returns>
public static Sql<ISqlContext> CrossJoin<TDto>(this Sql<ISqlContext> sql, string alias = null)
{
var type = typeof(TDto);
var tableName = type.GetTableName();
var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName);
if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias);
return sql.Append("CROSS JOIN " + join);
}
/// <summary>
/// Appends an INNER JOIN clause to the Sql statement.
/// </summary>
@@ -552,6 +550,25 @@ namespace Umbraco.Core.Persistence
return sqlJoin.On(onExpression, expresionist.GetSqlParameters());
}
/// <summary>
/// Appends an ON clause to a SqlJoin statement.
/// </summary>
/// <typeparam name="TDto1">The type of Dto 1.</typeparam>
/// <typeparam name="TDto2">The type of Dto 2.</typeparam>
/// <typeparam name="TDto3">The type of Dto 3.</typeparam>
/// <param name="sqlJoin">The SqlJoin statement.</param>
/// <param name="predicate">A predicate to transform and use as the ON clause body.</param>
/// <param name="aliasLeft">An optional alias for Dto 1 table.</param>
/// <param name="aliasRight">An optional alias for Dto 2 table.</param>
/// <param name="aliasOther">An optional alias for Dto 3 table.</param>
/// <returns>The Sql statement.</returns>
public static Sql<ISqlContext> On<TDto1, TDto2, TDto3>(this Sql<ISqlContext>.SqlJoinClause<ISqlContext> sqlJoin, Expression<Func<TDto1, TDto2, TDto3, bool>> predicate, string aliasLeft = null, string aliasRight = null, string aliasOther = null)
{
var expresionist = new PocoToSqlExpressionVisitor<TDto1, TDto2, TDto3>(sqlJoin.SqlContext, aliasLeft, aliasRight, aliasOther);
var onExpression = expresionist.Visit(predicate);
return sqlJoin.On(onExpression, expresionist.GetSqlParameters());
}
#endregion
#region Select
@@ -167,4 +167,93 @@ namespace Umbraco.Core.Persistence.Querying
}
}
/// <summary>
/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements.
/// </summary>
/// <typeparam name="TDto1">The type of DTO 1.</typeparam>
/// <typeparam name="TDto2">The type of DTO 2.</typeparam>
/// <typeparam name="TDto3">The type of DTO 3.</typeparam>
/// <remarks>This visitor is stateful and cannot be reused.</remarks>
internal class PocoToSqlExpressionVisitor<TDto1, TDto2, TDto3> : ExpressionVisitorBase
{
private readonly PocoData _pocoData1, _pocoData2, _pocoData3;
private readonly string _alias1, _alias2, _alias3;
private string _parameterName1, _parameterName2, _parameterName3;
public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string alias1, string alias2, string alias3)
: base(sqlContext.SqlSyntax)
{
_pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1));
_pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2));
_pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3));
_alias1 = alias1;
_alias2 = alias2;
_alias3 = alias3;
}
protected override string VisitLambda(LambdaExpression lambda)
{
if (lambda.Parameters.Count == 3)
{
_parameterName1 = lambda.Parameters[0].Name;
_parameterName2 = lambda.Parameters[1].Name;
_parameterName3 = lambda.Parameters[2].Name;
}
else if (lambda.Parameters.Count == 2)
{
_parameterName1 = lambda.Parameters[0].Name;
_parameterName2 = lambda.Parameters[1].Name;
}
else
{
_parameterName1 = _parameterName2 = null;
}
return base.VisitLambda(lambda);
}
protected override string VisitMemberAccess(MemberExpression m)
{
if (m.Expression != null)
{
if (m.Expression.NodeType == ExpressionType.Parameter)
{
var pex = (ParameterExpression)m.Expression;
if (pex.Name == _parameterName1)
return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1);
if (pex.Name == _parameterName2)
return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2);
if (pex.Name == _parameterName3)
return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3);
}
else if (m.Expression.NodeType == ExpressionType.Convert)
{
// here: which _pd should we use?!
throw new NotSupportedException();
//return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name);
}
}
var member = Expression.Convert(m, typeof(object));
var lambda = Expression.Lambda<Func<object>>(member);
var getter = lambda.Compile();
var o = getter();
SqlParameters.Add(o);
// execute if not already compiled
return Visited ? string.Empty : "@" + (SqlParameters.Count - 1);
}
protected virtual string GetFieldName(PocoData pocoData, string name, string alias)
{
var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name);
var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName);
var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName);
return tableName + "." + columnName;
}
}
}
@@ -8,14 +8,12 @@ using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -505,8 +503,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// names also impact 'edited'
foreach (var (culture, name) in content.CultureNames)
if (name != content.GetPublishName(culture))
{
edited = true;
(editedCultures ?? (editedCultures = new HashSet<string>(StringComparer.OrdinalIgnoreCase))).Add(culture);
// fixme - change tracking
// at the moment, we don't do any dirty tracking on property values, so we don't know whether the
// culture has just been edited or not, so we don't update its update date - that date only changes
// when the name is set, and it all works because the controller does it - but, if someone uses a
// service to change a property value and save (without setting name), the update date does not change.
}
// replace the content version variations (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var deleteContentVariations = Sql().Delete<ContentVersionCultureVariationDto>().Where<ContentVersionCultureVariationDto>(x => x.VersionId == versionToDelete);
@@ -1033,7 +1040,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId),
Name = dto.Name,
Date = dto.Date
Date = dto.UpdateDate
});
}
@@ -1078,7 +1085,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."),
Culture = culture,
Name = name,
Date = content.GetCultureDate(culture) ?? DateTime.MinValue // we *know* there is a value
UpdateDate = content.GetUpdateDate(culture) ?? DateTime.MinValue // we *know* there is a value
};
// if not publishing, we're just updating the 'current' (non-published) version,
@@ -1093,22 +1100,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."),
Culture = culture,
Name = name,
Date = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value
UpdateDate = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value
};
}
private IEnumerable<DocumentCultureVariationDto> GetDocumentVariationDtos(IContent content, bool publishing, HashSet<string> editedCultures)
{
foreach (var (culture, name) in content.CultureNames)
var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct
foreach (var culture in allCultures)
yield return new DocumentCultureVariationDto
{
NodeId = content.Id,
LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."),
Culture = culture,
// if not published, always edited
// no need to check for availability: it *is* available since it is in content.CultureNames
Edited = !content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))
Name = content.GetCultureName(culture) ?? content.GetPublishName(culture),
// note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem
Available = content.IsCultureAvailable(culture),
Published = content.IsCulturePublished(culture),
Edited = content.IsCultureAvailable(culture) &&
(!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture)))
};
}
@@ -9,6 +9,7 @@ using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -34,6 +35,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected IUmbracoDatabase Database => _scopeAccessor.AmbientScope.Database;
protected Sql<ISqlContext> Sql() => _scopeAccessor.AmbientScope.SqlContext.Sql();
protected ISqlSyntaxProvider SqlSyntax => _scopeAccessor.AmbientScope.SqlContext.SqlSyntax;
#region Repository
@@ -57,83 +59,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
//fixme - we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently
sql = sql.OrderBy("NodeId");
//IEnumerable<IUmbracoEntity> result;
//
//if (isMedia)
//{
// //Treat media differently for now, as an Entity it will be returned with ALL of it's properties in the AdditionalData bag!
// var pagedResult = UnitOfWork.Database.Page<dynamic>(pageIndex + 1, pageSize, pagedSql);
// var ids = pagedResult.Items.Select(x => (int)x.id).InGroupsOf(2000);
// var entities = pagedResult.Items.Select(BuildEntityFromDynamic).Cast<IUmbracoEntity>().ToList();
// //Now we need to merge in the property data since we need paging and we can't do this the way that the big media query was working before
// foreach (var idGroup in ids)
// {
// var propSql = GetPropertySql(Constants.ObjectTypes.Media)
// .WhereIn<NodeDto>(x => x.NodeId, idGroup)
// .OrderBy<NodeDto>(x => x.NodeId);
// //This does NOT fetch all data into memory in a list, this will read
// // over the records as a data reader, this is much better for performance and memory,
// // but it means that during the reading of this data set, nothing else can be read
// // from SQL server otherwise we'll get an exception.
// var allPropertyData = UnitOfWork.Database.Query<dynamic>(propSql);
// //keep track of the current property data item being enumerated
// var propertyDataSetEnumerator = allPropertyData.GetEnumerator();
// var hasCurrent = false; // initially there is no enumerator.Current
// try
// {
// //This must be sorted by node id (which is done by SQL) because this is how we are sorting the query to lookup property types above,
// // which allows us to more efficiently iterate over the large data set of property values.
// foreach (var entity in entities)
// {
// // assemble the dtos for this def
// // use the available enumerator.Current if any else move to next
// while (hasCurrent || propertyDataSetEnumerator.MoveNext())
// {
// if (propertyDataSetEnumerator.Current.nodeId == entity.Id)
// {
// hasCurrent = false; // enumerator.Current is not available
// //the property data goes into the additional data
// entity.AdditionalData[propertyDataSetEnumerator.Current.propertyTypeAlias] = new UmbracoEntity.EntityProperty
// {
// PropertyEditorAlias = propertyDataSetEnumerator.Current.propertyEditorAlias,
// Value = StringExtensions.IsNullOrWhiteSpace(propertyDataSetEnumerator.Current.textValue)
// ? propertyDataSetEnumerator.Current.varcharValue
// : StringExtensions.ConvertToJsonIfPossible(propertyDataSetEnumerator.Current.textValue)
// };
// }
// else
// {
// hasCurrent = true; // enumerator.Current is available for another def
// break; // no more propertyDataDto for this def
// }
// }
// }
// }
// finally
// {
// propertyDataSetEnumerator.Dispose();
// }
// }
// result = entities;
//}
//else
//{
// var pagedResult = UnitOfWork.Database.Page<dynamic>(pageIndex + 1, pageSize, pagedSql);
// result = pagedResult.Items.Select(BuildEntityFromDynamic).Cast<IUmbracoEntity>().ToList();
//}
var page = Database.Page<BaseDto>(pageIndex + 1, pageSize, sql);
var dtos = page.Items;
var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray();
//TODO: For isContent will we need to build up the variation info?
if (isContent)
BuildVariants(entities.Cast<DocumentEntitySlim>());
if (isMedia)
BuildProperties(entities, dtos);
@@ -154,11 +85,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
{
var dtos = Database.FetchOneToMany<ContentEntityDto>(
ddto => ddto.VariationInfo,
ddto => ddto.VersionId,
sql);
return dtos.Count == 0 ? null : BuildDocumentEntity(dtos[0]);
var cdtos = Database.Fetch<ContentEntityDto>(sql);
return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0]));
}
var dto = Database.FirstOrDefault<BaseDto>(sql);
@@ -216,13 +145,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
{
var cdtos = Database.FetchOneToMany<ContentEntityDto>(
dto => dto.VariationInfo,
dto => dto.VersionId,
sql);
var cdtos = Database.Fetch<ContentEntityDto>(sql);
return cdtos.Count == 0
? Enumerable.Empty<IEntitySlim>()
: cdtos.Select(BuildDocumentEntity).ToArray();
: BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList();
}
var dtos = Database.Fetch<BaseDto>(sql);
@@ -323,7 +250,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
private void BuildProperties(EntitySlim[] entities, List<BaseDto> dtos)
{
var versionIds = dtos.Select(x => x.VersionId).Distinct().ToArray();
var versionIds = dtos.Select(x => x.VersionId).Distinct().ToList();
var pdtos = Database.FetchByGroups<PropertyDataDto, int>(versionIds, 2000, GetPropertyData);
var xentity = entities.ToDictionary(x => x.Id, x => x); // nodeId -> entity
@@ -346,10 +273,70 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
entity.AdditionalData[pdto.PropertyTypeDto.Alias] = new EntitySlim.PropertySlim(pdto.PropertyTypeDto.DataTypeDto.EditorAlias, value);
}
private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity)
=> BuildVariants(new[] { entity }).First();
private IEnumerable<DocumentEntitySlim> BuildVariants(IEnumerable<DocumentEntitySlim> entities)
{
List<DocumentEntitySlim> v = null;
var entitiesList = entities.ToList();
foreach (var e in entitiesList)
{
if (e.Variations.VariesByCulture())
(v ?? (v = new List<DocumentEntitySlim>())).Add(e);
}
if (v == null) return entitiesList;
// fetch all variant info dtos
var dtos = Database.FetchByGroups<VariantInfoDto, int>(v.Select(x => x.Id), 2000, GetVariantInfos);
// group by node id (each group contains all languages)
var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x);
foreach (var e in v)
{
// since we're only iterating on entities that vary, we must have something
var edtos = xdtos[e.Id];
e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name);
e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode);
e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode);
}
return entitiesList;
}
#endregion
#region Sql
protected Sql<ISqlContext> GetVariantInfos(IEnumerable<int> ids)
{
return Sql()
.Select<NodeDto>(x => x.NodeId)
.AndSelect<LanguageDto>(x => x.IsoCode)
.AndSelect<DocumentDto>("doc", x => Alias(x.Published, "DocumentPublished"), x => Alias(x.Edited, "DocumentEdited"))
.AndSelect<DocumentCultureVariationDto>("dcv",
x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), x => Alias(x.Edited, "CultureEdited"),
x => Alias(x.Name, "Name"))
// from node x language
.From<NodeDto>()
.CrossJoin<LanguageDto>()
// join to document - always exists - indicates global document published/edited status
.InnerJoin<DocumentDto>("doc")
.On<NodeDto, DocumentDto>((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc")
// left-join do document variation - matches cultures that are *available* + indicates when *edited*
.LeftJoin<DocumentCultureVariationDto>("dcv")
.On<NodeDto, DocumentCultureVariationDto, LanguageDto>((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv")
// for selected nodes
.WhereIn<NodeDto>(x => x.NodeId, ids);
}
// gets the full sql for a given object type and a given unique id
protected Sql<ISqlContext> GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId)
{
@@ -371,24 +358,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return AddGroupBy(isContent, isMedia, sql);
}
// fixme kill this nonsense
//// gets the SELECT + FROM + WHERE sql
//// to get all property data for all items of the specified object type
//private Sql<ISqlContext> GetPropertySql(Guid objectType)
//{
// return Sql()
// .Select<PropertyDataDto>(x => x.VersionId, x => x.TextValue, x => x.VarcharValue)
// .AndSelect<NodeDto>(x => x.NodeId)
// .AndSelect<DataTypeDto>(x => x.PropertyEditorAlias)
// .AndSelect<PropertyTypeDto>(x => Alias(x.Alias, "propertyTypeAlias"))
// .From<PropertyDataDto>()
// .InnerJoin<ContentVersionDto>().On<PropertyDataDto, ContentVersionDto>((left, right) => left.VersionId == right.Id)
// .InnerJoin<NodeDto>().On<ContentVersionDto, NodeDto>((left, right) => left.NodeId == right.NodeId)
// .InnerJoin<PropertyTypeDto>().On<PropertyDataDto, PropertyTypeDto>(dto => dto.PropertyTypeId, dto => dto.Id)
// .InnerJoin<DataTypeDto>().On<PropertyTypeDto, DataTypeDto>(dto => dto.DataTypeId, dto => dto.DataTypeId)
// .Where<NodeDto>(x => x.NodeObjectType == objectType);
//}
private Sql<ISqlContext> GetPropertyData(int versionId)
{
return Sql()
@@ -408,89 +377,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.InnerJoin<DataTypeDto>().On<PropertyTypeDto, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId)
.WhereIn<PropertyDataDto>(x => x.VersionId, versionIds)
.OrderBy<PropertyDataDto>(x => x.VersionId);
}
// fixme - wtf is this?
//private Sql<ISqlContext> GetFullSqlForMedia(Sql<ISqlContext> entitySql, Action<Sql<ISqlContext>> filter = null)
//{
// //this will add any varcharValue property to the output which can be added to the additional properties
// var sql = GetPropertySql(Constants.ObjectTypes.Media);
// filter?.Invoke(sql);
// // We're going to create a query to query against the entity SQL
// // because we cannot group by nText columns and we have a COUNT in the entitySql we cannot simply left join
// // the entitySql query, we have to join the wrapped query to get the ntext in the result
// var wrappedSql = Sql()
// .Append("SELECT * FROM (")
// .Append(entitySql)
// .Append(") tmpTbl LEFT JOIN (")
// .Append(sql)
// .Append(") as property ON id = property.nodeId")
// .OrderBy("sortOrder, id");
// return wrappedSql;
//}
/// <summary>
/// The DTO used to fetch results for a content item with its variation info
/// </summary>
private class ContentEntityDto : BaseDto
{
public ContentVariation Variations { get; set; }
[ResultColumn, Reference(ReferenceType.Many)]
public List<ContentEntityVariationInfoDto> VariationInfo { get; set; }
public bool Published { get; set; }
public bool Edited { get; set; }
}
/// <summary>
/// The DTO used in the 1:M result for content variation info
/// </summary>
private class ContentEntityVariationInfoDto
{
[Column("versionCultureId")]
public int VersionCultureId { get; set; }
[Column("versionCultureLangId")]
public int LanguageId { get; set; }
[Column("versionCultureName")]
public string Name { get; set; }
}
// ReSharper disable once ClassNeverInstantiated.Local
/// <summary>
/// the DTO corresponding to fields selected by GetBase
/// </summary>
private class BaseDto
{
// ReSharper disable UnusedAutoPropertyAccessor.Local
// ReSharper disable UnusedMember.Local
public int NodeId { get; set; }
public bool Trashed { get; set; }
public int ParentId { get; set; }
public int? UserId { get; set; }
public int Level { get; set; }
public string Path { get; set; }
public int SortOrder { get; set; }
public Guid UniqueId { get; set; }
public string Text { get; set; }
public Guid NodeObjectType { get; set; }
public DateTime CreateDate { get; set; }
public int Children { get; set; }
public int VersionId { get; set; }
public string Alias { get; set; }
public string Icon { get; set; }
public string Thumbnail { get; set; }
public bool IsContainer { get; set; }
// ReSharper restore UnusedAutoPropertyAccessor.Local
// ReSharper restore UnusedMember.Local
}
}
// gets the base SELECT + FROM [+ filter] sql
// always from the 'current' content version
@@ -517,12 +404,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (isContent)
{
sql
.AndSelect<DocumentDto>(x => x.Published, x => x.Edited)
//This MUST come last in the select statements since we will end up with a 1:M query
.AndSelect<ContentVersionCultureVariationDto>(
x => Alias(x.Id, "versionCultureId"),
x => Alias(x.LanguageId, "versionCultureLangId"),
x => Alias(x.Name, "versionCultureName"));
.AndSelect<DocumentDto>(x => x.Published, x => x.Edited);
}
}
@@ -534,7 +416,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
sql
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId && right.Current)
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((left, right) => left.NodeId == right.NodeId)
.LeftJoin<ContentTypeDto>().On<ContentDto, ContentTypeDto>((left, right) => left.ContentTypeId == right.NodeId);
.InnerJoin<ContentTypeDto>().On<ContentDto, ContentTypeDto>((left, right) => left.ContentTypeId == right.NodeId);
}
if (isContent)
@@ -548,10 +430,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
sql
.LeftJoin<NodeDto>("child").On<NodeDto, NodeDto>((left, right) => left.NodeId == right.ParentId, aliasRight: "child");
if (isContent)
sql
.LeftJoin<ContentVersionCultureVariationDto>().On<ContentVersionDto, ContentVersionCultureVariationDto>((left, right) => left.Id == right.VersionId);
}
@@ -613,8 +491,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (isContent)
{
sql
.AndBy<DocumentDto>(x => x.Published, x => x.Edited)
.AndBy<ContentVersionCultureVariationDto>(x => x.Id, x => x.LanguageId, x => x.Name);
.AndBy<DocumentDto>(x => x.Published, x => x.Edited);
}
@@ -649,184 +526,60 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
public string TextValue { get; set; }
}
// fixme kill
/// <summary>
/// This is a special relator in that it is not returning a DTO but a real resolved entity and that it accepts
/// a dynamic instance.
/// The DTO used to fetch results for a content item with its variation info
/// </summary>
/// <remarks>
/// We're doing this because when we query the db, we want to use dynamic so that it returns all available fields not just the ones
/// defined on the entity so we can them to additional data
/// </remarks>
//internal class UmbracoEntityRelator
//{
// internal UmbracoEntity Current;
private class ContentEntityDto : BaseDto
{
public ContentVariation Variations { get; set; }
// public IEnumerable<IUmbracoEntity> MapAll(IEnumerable<dynamic> input)
// {
// UmbracoEntity entity;
public bool Published { get; set; }
public bool Edited { get; set; }
}
// foreach (var x in input)
// {
// entity = Map(x);
// if (entity != null) yield return entity;
// }
public class VariantInfoDto
{
public int NodeId { get; set; }
public string IsoCode { get; set; }
public string Name { get; set; }
public bool DocumentPublished { get; set; }
public bool DocumentEdited { get; set; }
// entity = Map((dynamic) null);
// if (entity != null) yield return entity;
// }
public bool CultureAvailable { get; set; }
public bool CulturePublished { get; set; }
public bool CultureEdited { get; set; }
}
// // must be called one last time with null in order to return the last one!
// public UmbracoEntity Map(dynamic a)
// {
// // Terminating call. Since we can return null from this function
// // we need to be ready for NPoco to callback later with null
// // parameters
// if (a == null)
// return Current;
// string pPropertyEditorAlias = a.propertyEditorAlias;
// var pExists = pPropertyEditorAlias != null;
// string pPropertyAlias = a.propertyTypeAlias;
// string pTextValue = a.textValue;
// string pNVarcharValue = a.varcharValue;
// // Is this the same UmbracoEntity as the current one we're processing
// if (Current != null && Current.Key == a.uniqueID)
// {
// if (pExists && pPropertyAlias.IsNullOrWhiteSpace() == false)
// {
// // Add this UmbracoProperty to the current additional data
// Current.AdditionalData[pPropertyAlias] = new UmbracoEntity.EntityProperty
// {
// PropertyEditorAlias = pPropertyEditorAlias,
// Value = pTextValue.IsNullOrWhiteSpace()
// ? pNVarcharValue
// : pTextValue.ConvertToJsonIfPossible()
// };
// }
// // Return null to indicate we're not done with this UmbracoEntity yet
// return null;
// }
// // This is a different UmbracoEntity to the current one, or this is the
// // first time through and we don't have a Tab yet
// // Save the current UmbracoEntityDto
// var prev = Current;
// // Setup the new current UmbracoEntity
// Current = BuildEntityFromDynamic(a);
// if (pExists && pPropertyAlias.IsNullOrWhiteSpace() == false)
// {
// //add the property/create the prop list if null
// Current.AdditionalData[pPropertyAlias] = new UmbracoEntity.EntityProperty
// {
// PropertyEditorAlias = pPropertyEditorAlias,
// Value = pTextValue.IsNullOrWhiteSpace()
// ? pNVarcharValue
// : pTextValue.ConvertToJsonIfPossible()
// };
// }
// // Return the now populated previous UmbracoEntity (or null if first time through)
// return prev;
// }
//}
// fixme need to review what's below
// comes from 7.6, similar to what's in VersionableRepositoryBase
// not sure it really makes sense...
//private class EntityDefinitionCollection : KeyedCollection<int, EntityDefinition>
//{
// protected override int GetKeyForItem(EntityDefinition item)
// {
// return item.Id;
// }
// /// <summary>
// /// if this key already exists if it does then we need to check
// /// if the existing item is 'older' than the new item and if that is the case we'll replace the older one
// /// </summary>
// /// <param name="item"></param>
// /// <returns></returns>
// public bool AddOrUpdate(EntityDefinition item)
// {
// if (Dictionary == null)
// {
// Add(item);
// return true;
// }
// var key = GetKeyForItem(item);
// if (TryGetValue(key, out EntityDefinition found))
// {
// //it already exists and it's older so we need to replace it
// if (item.VersionId > found.VersionId)
// {
// var currIndex = Items.IndexOf(found);
// if (currIndex == -1)
// throw new IndexOutOfRangeException("Could not find the item in the list: " + found.Id);
// //replace the current one with the newer one
// SetItem(currIndex, item);
// return true;
// }
// //could not add or update
// return false;
// }
// Add(item);
// return true;
// }
// private bool TryGetValue(int key, out EntityDefinition val)
// {
// if (Dictionary != null) return Dictionary.TryGetValue(key, out val);
// val = null;
// return false;
// }
//}
// fixme wtf is this, why dynamics here, this is horrible !!
//private class EntityDefinition
//{
// private readonly dynamic _entity;
// private readonly bool _isContent;
// private readonly bool _isMedia;
// public EntityDefinition(dynamic entity, bool isContent, bool isMedia)
// {
// _entity = entity;
// _isContent = isContent;
// _isMedia = isMedia;
// }
// public IUmbracoEntity BuildFromDynamic()
// {
// return BuildEntityFromDynamic(_entity);
// }
// public int Id => _entity.id;
// public int VersionId
// {
// get
// {
// if (_isContent || _isMedia)
// {
// return _entity.versionId;
// }
// return _entity.id;
// }
// }
//}
// ReSharper disable once ClassNeverInstantiated.Local
/// <summary>
/// the DTO corresponding to fields selected by GetBase
/// </summary>
private class BaseDto
{
// ReSharper disable UnusedAutoPropertyAccessor.Local
// ReSharper disable UnusedMember.Local
public int NodeId { get; set; }
public bool Trashed { get; set; }
public int ParentId { get; set; }
public int? UserId { get; set; }
public int Level { get; set; }
public string Path { get; set; }
public int SortOrder { get; set; }
public Guid UniqueId { get; set; }
public string Text { get; set; }
public Guid NodeObjectType { get; set; }
public DateTime CreateDate { get; set; }
public int Children { get; set; }
public int VersionId { get; set; }
public string Alias { get; set; }
public string Icon { get; set; }
public string Thumbnail { get; set; }
public bool IsContainer { get; set; }
// ReSharper restore UnusedAutoPropertyAccessor.Local
// ReSharper restore UnusedMember.Local
}
#endregion
#region Factory
@@ -879,44 +632,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
private DocumentEntitySlim BuildDocumentEntity(BaseDto dto)
{
// EntitySlim does not track changes
var entity = new DocumentEntitySlim();
BuildContentEntity(entity, dto);
if (dto is ContentEntityDto contentDto)
{
return BuildDocumentEntity(contentDto);
// fill in the invariant info
entity.Edited = contentDto.Edited;
entity.Published = contentDto.Published;
entity.Variations = contentDto.Variations;
}
// EntitySlim does not track changes
var entity = new DocumentEntitySlim();
BuildContentEntity(entity, dto);
return entity;
}
/// <summary>
/// Builds the <see cref="EntitySlim"/> from a <see cref="ContentEntityDto"/> and ensures the AdditionalData is populated with variant info
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
private DocumentEntitySlim BuildDocumentEntity(ContentEntityDto dto)
{
// EntitySlim does not track changes
var entity = new DocumentEntitySlim();
BuildContentEntity(entity, dto);
//fixme we need to set these statuses for each variant, see notes in IDocumentEntitySlim
entity.Edited = dto.Edited;
entity.Published = dto.Published;
if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0)
{
var variantInfo = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
foreach (var info in dto.VariationInfo)
{
var isoCode = _langRepository.GetIsoCodeById(info.LanguageId);
if (isoCode != null)
variantInfo[isoCode] = info.Name;
}
entity.CultureNames = variantInfo;
entity.Variations = dto.Variations;
}
return entity;
}
+1
View File
@@ -370,6 +370,7 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\LanguageColumns.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\PropertyEditorsMigration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorMacroColumns.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\RefactorVariantsModel.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\SuperZero.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\TagsMigration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\UpdateDefaultMandatoryLanguage.cs" />
@@ -41,10 +41,17 @@
//We fetch all ancestors of the node to generate the footer breadcrumb navigation
if (!$scope.page.isNew) {
if (content.parentId && content.parentId !== -1) {
entityResource.getAncestors(content.id, "document")
entityResource.getAncestors(content.id, "document", $scope.culture)
.then(function (anc) {
$scope.ancestors = anc;
});
$scope.$watch('culture',
function(value, oldValue) {
entityResource.getAncestors(content.id, "document", value)
.then(function (anc) {
$scope.ancestors = anc;
});
});
}
}
@@ -0,0 +1,27 @@
(function () {
/**
* @ngdoc directive
* @name umbraco.directives.directive:multi
* @restrict A
* @description Used on input fields when you want to validate multiple fields at once.
**/
function multi($parse, $rootScope) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ngModelCtrl) {
var validate = $parse(attrs.multi)(scope);
ngModelCtrl.$viewChangeListeners.push(function () {
// ngModelCtrl.$setValidity('multi', validate());
$rootScope.$broadcast('multi:valueChanged');
});
var deregisterListener = scope.$on('multi:valueChanged', function (event) {
ngModelCtrl.$setValidity('multi', validate());
});
scope.$on('$destroy', deregisterListener); // optional, only required for $rootScope.$on
}
};
}
angular.module('umbraco.directives.validation').directive('multi', ['$parse', '$rootScope', multi]);
})();
@@ -242,6 +242,44 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
{ id: id, culture: culture })),
'Failed to publish content with id ' + id);
},
/**
* @ngdoc method
* @name umbraco.resources.contentResource#getCultureAndDomains
* @methodOf umbraco.resources.contentResource
*
* @description
* Gets the culture and hostnames for a content item with the given Id
*
* ##usage
* <pre>
* contentResource.getCultureAndDomains(1234)
* .then(function(data) {
* alert(data.Domains, data.Language);
* });
* </pre>
* @param {Int} id the ID of the node to get the culture and domains for.
* @returns {Promise} resourcePromise object.
*
*/
getCultureAndDomains: function (id) {
if (!id) {
throw "id cannot be null";
}
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetCultureAndDomains", { id: id })),
'Failed to retreive culture and hostnames for ' + id);
},
saveLanguageAndDomains: function (model) {
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"PostSaveLanguageAndDomains"),
model));
},
/**
* @ngdoc method
* @name umbraco.resources.contentResource#emptyRecycleBin
@@ -334,26 +372,26 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
*/
getById: function (id) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetById",
{ id: id })),
'Failed to retrieve data for content id ' + id)
.then(function(result) {
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetById",
{ id: id })),
'Failed to retrieve data for content id ' + id)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));
});
},
getBlueprintById: function (id) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetBlueprintById",
[{ id: id }])),
'Failed to retrieve data for content id ' + id)
.then(function(result) {
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetBlueprintById",
[{ id: id }])),
'Failed to retrieve data for content id ' + id)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));
});
},
@@ -410,15 +448,15 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
});
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetByIds",
idQuery)),
'Failed to retrieve data for content with multiple ids')
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetByIds",
idQuery)),
'Failed to retrieve data for content with multiple ids')
.then(function (result) {
//each item needs to be re-formatted
_.each(result, function(r) {
_.each(result, function (r) {
umbDataFormatter.formatContentGetData(r)
});
return $q.when(result);
@@ -461,13 +499,13 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
getScaffold: function (parentId, alias) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetEmpty",
[{ contentTypeAlias: alias }, { parentId: parentId }])),
'Failed to retrieve data for empty content item type ' + alias)
.then(function(result) {
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetEmpty",
[{ contentTypeAlias: alias }, { parentId: parentId }])),
'Failed to retrieve data for empty content item type ' + alias)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));
});
},
@@ -475,13 +513,13 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
getBlueprintScaffold: function (parentId, blueprintId) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetEmpty",
[{ blueprintId: blueprintId }, { parentId: parentId }])),
'Failed to retrieve blueprint for id ' + blueprintId)
.then(function(result) {
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetEmpty",
[{ blueprintId: blueprintId }, { parentId: parentId }])),
'Failed to retrieve blueprint for id ' + blueprintId)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));
});
},
@@ -288,17 +288,19 @@ function entityResource($q, $http, umbRequestHelper) {
* Gets ancestor entities for a given item
*
*
* @param {string} type Object type name
* @param {string} type Object type name
* @param {string} culture Culture
* @returns {Promise} resourcePromise object containing the entity.
*
*/
getAncestors: function (id, type) {
getAncestors: function (id, type, culture) {
if (culture === undefined) culture = "";
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"entityApiBaseUrl",
"GetAncestors",
[{id: id}, {type: type}])),
[{ id: id }, { type: type }, { culture: culture }])),
'Failed to retrieve ancestor data for id ' + id);
},
@@ -1,24 +1,70 @@
<div ng-controller="Umbraco.Editors.Content.AssignDomainController as vm">
<div class="umb-dialog-body">
<umb-pane>
<p>UI magic to be done...</p>
</umb-pane>
</div>
<div ng-controller="Umbraco.Editors.Content.AssignDomainController as vm" ng-cloak>
<div class="umb-dialog-body">
<umb-pane>
<h5 class="umb-pane-title"><localize key="assignDomain_setLanguage">Culture</localize></h5>
<label for="assignDomain_language" class="control-label"><localize key="general_language"></localize></label>
<select class="umb-property-editor umb-dropdown" name="language" id="assignDomain_language" ng-model="vm.language" ng-options="lang.name for lang in vm.languages">
<option value="">{{vm.inherit}}</option>
</select>
</umb-pane>
<umb-pane>
<h5 class="umb-pane-title"><localize key="assignDomain_setDomains">Domains</localize></h5>
<form name="vm.domainForm" novalidate="" ng-submit="vm.save()" class="control-group umb-control-group">
<small class="help-inline">
<localize key="assignDomain_domainHelp"></localize>
</small>
<div class="umb-el-wrap hidelabel">
<table class="table domains" ng-if="vm.domains.length > 0">
<thead>
<tr>
<th><localize key="assignDomain_domain"></localize></th>
<th><localize key="assignDomain_language"></localize></th>
<th />
</tr>
</thead>
<tbody>
<tr ng-repeat="domain in vm.domains">
<td>
<input type="text" ng-model="domain.Name" multi="vm.validateDomain" ng-pattern="vm.domainPattern" name="domain_name_{{$index}}" />
<span ng-if="vm.domainForm.$submitted" ng-messages="vm.domainForm['domain_name_' + $index].$error">
<span class="help-inline" ng-message="multi"><localize key="assignDomain_duplicateDomain"></localize></span>
<span class="help-inline" ng-message="pattern"><localize key="assignDomain_invalidDomain"></localize></span>
</span>
</td>
<td>
<select name="domain_language_{{$index}}" class="language" ng-model="domain.Lang" ng-options="lang.name for lang in vm.languages"></select>
</td>
<td>
<umb-button icon="icon-trash"
action="vm.removeDomain($index)"
type="button"
button-style="danger">
</umb-button>
</td>
</tr>
</tbody>
</table>
</div>
<umb-button label-key="assignDomain_addNew"
action="vm.addDomain()"
type="button">
</umb-button>
<div class="umb-dialog-footer btn-toolbar umb-btn-toolbar">
<div class="umb-dialog-footer btn-toolbar umb-btn-toolbar">
<umb-button label-key="general_cancel"
action="vm.closeDialog()"
type="button"
button-style="link">
</umb-button>
<umb-button
label-key="general_cancel"
action="vm.closeDialog()"
type="button"
button-style="link">
</umb-button>
<umb-button
label-key="buttons_save"
type="button"
button-style="success">
</umb-button>
<umb-button label-key="buttons_save"
type="submit"
button-style="success">
</umb-button>
</div>
</form>
</umb-pane>
</div>
</div>
@@ -1,17 +1,126 @@
(function () {
"use strict";
function AssignDomainController($scope) {
function AssignDomainController($scope, localizationService, languageResource, contentResource) {
var vm = this;
vm.closeDialog = closeDialog;
function closeDialog() {
$scope.nav.hideDialog();
vm.addDomain = addDomain;
vm.removeDomain = removeDomain;
vm.save = save;
vm.validateDomain = validateDomain;
vm.languages = [];
vm.domains = [];
vm.language = null;
vm.domainPattern = /^(http[s]?:\/\/)?([-\w]+(\.[-\w]+)*)(:\d+)?(\/[-\w]*|-)?$/gi; //TODO: This regex is not working as it should.
function activate() {
languageResource.getAll().then(function (langs) {
vm.languages = langs;
var defLang = langs.filter(function (l) {
return l.isDefault;
});
if (defLang.length > 0) {
vm.defaultLanguage = defLang[0];
}
else {
vm.defaultLanguage = langs[0];
}
getCultureAndDomains();
});
localizationService.localize("assignDomain_inherit").then(function (value) {
vm.inherit = value;
});
}
function getCultureAndDomains () {
contentResource.getCultureAndDomains($scope.currentNode.id)
.then(function (data) {
if (data.Language !== "undefined") {
var lang = vm.languages.filter(function (l) {
return matchLanguageById(l, data.Language.Id);
});
if (lang.length > 0) {
vm.language = lang[0];
}
}
vm.domains = data.Domains.map(function (d) {
var matchedLangs = vm.languages.filter(function (l) {
return matchLanguageById(l, d.Language);
});
return {
Name: d.Name,
Lang: matchedLangs.length > 0 ? matchedLangs[0] : vm.defaultLanguage
}
});
});
}
function matchLanguageById(language, id) {
return language.Id === id;
}
function closeDialog() {
$scope.nav.hideDialog();
}
function addDomain() {
vm.domains.push({
Name: '',
Lang: vm.defaultLanguage
});
}
function removeDomain(index) {
vm.domains.splice(index, 1);
}
function validateDomain() {
var valid = true, duplicateTest = {};
if (vm.domains.length > 1) {
vm.domains.map(function (d, index) {
if (d.Name in duplicateTest) {
valid = false;
}
else {
duplicateTest[d.Name] = index;
}
});
}
return valid;
}
function save() {
if (vm.domainForm.$valid) {
var data = {
NodeId: $scope.currentNode.id,
Domains: vm.domains.map(function (d) {
return {
Name: d.Name,
Lang: d.Lang.id
};
}),
Language: vm.language != null ? vm.language.id : 0
};
console.log(data);
contentResource.saveLanguageAndDomains(data).then(function () {
closeDialog();
}, function (e) {
console.log(e); //TODO: not sure how best to handle this case
});
}
else {
console.log('not valid');
}
}
activate();
}
angular.module("umbraco").controller("Umbraco.Editors.Content.AssignDomainController", AssignDomainController);
})();
})();
@@ -279,7 +279,8 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop
_.each($scope.model.config.contentTypes, function (contentType) {
contentResource.getScaffold(-20, contentType.ncAlias).then(function (scaffold) {
// remove all tabs except the specified tab
var tab = _.find(scaffold.tabs, function (tab) {
var tabs = scaffold.variants[0].tabs;
var tab = _.find(tabs, function (tab) {
return tab.id != 0 && (tab.alias.toLowerCase() == contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias == "");
});
scaffold.tabs = [];
-1
View File
@@ -334,7 +334,6 @@
<Content Include="Umbraco\Developer\RelationTypes\RelationTypesWebService.asmx" />
<Content Include="Umbraco\Developer\RelationTypes\TreeMenu\ActionDeleteRelationType.js" />
<Content Include="Umbraco\Developer\RelationTypes\TreeMenu\ActionNewRelationType.js" />
<Content Include="Umbraco\Dialogs\AssignDomain2.aspx" />
<Content Include="Umbraco\Dialogs\ChangeDocType.aspx">
<SubType>ASPXCodeBehind</SubType>
</Content>
@@ -1,79 +0,0 @@
<%@ Page Language="c#" MasterPageFile="../masterpages/umbracoDialog.Master" Codebehind="AssignDomain2.aspx.cs" AutoEventWireup="True" Inherits="umbraco.dialogs.AssignDomain2" %>
<%@ Import Namespace="Umbraco.Web" %>
<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %>
<%@ Register TagPrefix="cc1" Namespace="Umbraco.Web._Legacy.Controls" Assembly="Umbraco.Web" %>
<asp:Content ContentPlaceHolderID="head" runat="server">
<umb:JsInclude runat="server" FilePath="Dialogs/AssignDomain2.js" PathNameAlias="UmbracoClient" />
<umb:JsInclude runat="server" FilePath="PunyCode/punycode.min.js" PathNameAlias="UmbracoClient" />
<umb:JsInclude runat="server" FilePath="/Umbraco/lib/jquery-validate/jquery.validate.min.js" PathNameAlias="UmbracoClient" />
<umb:CssInclude runat="server" FilePath="Dialogs/AssignDomain2.css" PathNameAlias="UmbracoClient" />
<script type="text/javascript">
(function ($) {
$(document).ready(function () {
var dialog = new Umbraco.Dialogs.AssignDomain2({
nodeId: <%=GetNodeId()%>,
restServiceLocation: '<%=GetRestServicePath() %>',
invalidDomain: '<%=Services.TextService.Localize("assignDomain/invalidDomain") %>',
duplicateDomain: '<%=Services.TextService.Localize("assignDomain/duplicateDomain") %>',
<asp:Literal runat="server" ID="data" />
});
dialog.init();
});
})(jQuery);
</script>
</asp:Content>
<asp:Content ContentPlaceHolderID="body" runat="server">
<div class="umb-dialog-body">
<cc1:Feedback ID="feedback" runat="server" />
<div id="komask"></div>
<cc1:Pane runat="server" ID="pane_language" cssClass="hide">
<cc1:PropertyPanel runat="server" ID="prop_language">
<select class="umb-property-editor umb-dropdown" name="language" data-bind="options: languages, optionsText: 'Code', optionsValue: 'Id', value: language, optionsCaption: '<%=Services.TextService.Localize("assignDomain/inherit") %> '"></select>
<!-- <small class="help-inline"><%=Services.TextService.Localize("assignDomain/setLanguageHelp") %></small>-->
</cc1:PropertyPanel>
</cc1:Pane>
<cc1:Pane runat="server" ID="pane_domains">
<small class="help-inline"><%=Services.TextService.Localize("assignDomain/domainHelp") %></small>
<cc1:PropertyPanel runat="server">
<table class="table domains" data-bind="visible: domains().length > 0">
<thead>
<tr>
<th><%=Services.TextService.Localize("assignDomain/domain") %></th>
<th><%=Services.TextService.Localize("assignDomain/language") %></th>
<th />
</tr>
</thead>
<tbody data-bind="foreach: domains">
<tr>
<td valign="top"><input class="domain duplicate" data-bind="value: Name, uniqueName: true" /><input type="hidden" value="" data-bind="uniqueName: true"/></td>
<td valign="top"><select class="language" data-bind="options: $parent.languages, optionsText: 'Code', optionsValue: 'Id', value: Lang, uniqueName: true"></select></td>
<td valign="top"><a href="#" class="btn btn-danger" data-bind="click: $parent.removeDomain"><i class="icon icon-trash"></i></a></td>
</tr>
</tbody>
</table>
</cc1:PropertyPanel>
<cc1:PropertyPanel runat="server">
<button class="btn" data-bind="click: addDomain"><%=Services.TextService.Localize("assignDomain/addNew") %></button>
</cc1:PropertyPanel>
</cc1:Pane>
</div>
<div runat="server" ID="p_buttons" class="umb-dialog-footer btn-toolbar umb-btn-toolbar">
<a href="#" class="btn btn-link" onclick="UmbClientMgr.closeModalWindow()"><%=Services.TextService.Localize("general/cancel")%></a>
<button class="btn btn-primary" id="btnSave"><%=Services.TextService.Localize("buttons/save") %></button>
</div>
</asp:Content>
@@ -56,6 +56,8 @@ namespace Umbraco.Web.Editors
private readonly PropertyEditorCollection _propertyEditors;
private readonly Lazy<IDictionary<string, ILanguage>> _allLangs;
public object Domains { get; private set; }
public ContentController(IPublishedSnapshotService publishedSnapshotService, PropertyEditorCollection propertyEditors)
{
if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService));
@@ -1146,6 +1148,18 @@ namespace Umbraco.Web.Editors
}
}
public ContentDomainsAndCulture GetCultureAndDomains(int id)
{
var nodeDomains = Services.DomainService.GetAssignedDomains(id, true).ToArray();
var wildcard = nodeDomains.FirstOrDefault(d => d.IsWildcard);
var domains = nodeDomains.Where(d => !d.IsWildcard).Select(d => new DomainDisplay(d.DomainName, d.LanguageId.GetValueOrDefault(0)));
return new ContentDomainsAndCulture
{
Domains = domains,
Language = wildcard == null || !wildcard.LanguageId.HasValue ? "undefined" : wildcard.LanguageId.ToString()
};
}
[HttpPost]
public DomainSave PostSaveLanguageAndDomains(DomainSave model)
{
+11 -5
View File
@@ -10,15 +10,18 @@ using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using Umbraco.Core.Models;
using Constants = Umbraco.Core.Constants;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using System.Web.Http.Controllers;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Xml;
using Umbraco.Web.Models.Mapping;
using Umbraco.Web.Search;
using Umbraco.Web.Trees;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Editors
{
@@ -211,7 +214,7 @@ namespace Umbraco.Web.Editors
}
}
var ancestors = GetAncestors(id, type);
var ancestors = GetResultForAncestors(id, type);
//if content, skip the first node for replicating NiceUrl defaults
if(type == UmbracoEntityTypes.Document) {
@@ -582,9 +585,10 @@ namespace Umbraco.Web.Editors
}
}
public IEnumerable<EntityBasic> GetAncestors(int id, UmbracoEntityTypes type)
[HttpQueryStringFilter("queryStrings")]
public IEnumerable<EntityBasic> GetAncestors(int id, UmbracoEntityTypes type, FormDataCollection queryStrings)
{
return GetResultForAncestors(id, type);
return GetResultForAncestors(id, type, queryStrings);
}
/// <summary>
@@ -626,7 +630,7 @@ namespace Umbraco.Web.Editors
}
}
private IEnumerable<EntityBasic> GetResultForAncestors(int id, UmbracoEntityTypes entityType)
private IEnumerable<EntityBasic> GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormDataCollection queryStrings = null)
{
var objectType = ConvertToObjectType(entityType);
if (objectType.HasValue)
@@ -666,12 +670,14 @@ namespace Umbraco.Web.Editors
ids = lids.ToArray();
}
var culture = queryStrings?.GetValue<string>("culture");
return ids.Length == 0
? Enumerable.Empty<EntityBasic>()
: Services.EntityService.GetAll(objectType.Value, ids)
.WhereNotNull()
.OrderBy(x => x.Level)
.Select(Mapper.Map<EntityBasic>);
.Select(x => Mapper.Map<EntityBasic>(x, opts => { opts.SetCulture(culture);}));
}
//now we need to convert the unknown ones
switch (entityType)
@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Umbraco.Web.Models.ContentEditing
{
public class ContentDomainsAndCulture
{
public IEnumerable<DomainDisplay> Domains { get; set; }
public string Language { get; internal set; }
}
}
@@ -108,7 +108,7 @@ namespace Umbraco.Web.Models.Mapping
// if we don't have a date for a culture, it means the culture is not available, and
// hey we should probably not be mapping it, but it's too late, return a fallback date
var date = source.GetCultureDate(culture);
var date = source.GetUpdateDate(culture);
return date ?? source.UpdateDate;
}
}
@@ -25,6 +25,7 @@ namespace Umbraco.Web.Models.Mapping
var contentTypeUdiResolver = new ContentTypeUdiResolver();
CreateMap<EntitySlim, EntityBasic>()
.ForMember(dest => dest.Name, opt => opt.ResolveUsing<NameResolver>())
.ForMember(dest => dest.Udi, opt => opt.MapFrom(src => Udi.Create(ObjectTypes.GetUdiType(src.NodeObjectType), src.Key)))
.ForMember(dest => dest.Icon, opt => opt.MapFrom(src => GetContentTypeIcon(src)))
.ForMember(dest => dest.Trashed, opt => opt.MapFrom(src => src.Trashed))
@@ -179,5 +180,33 @@ namespace Umbraco.Web.Models.Mapping
CreateMap<IEnumerable<SearchResult>, IEnumerable<SearchResultItem>>()
.ConvertUsing(results => results.Select(Mapper.Map<SearchResultItem>).ToList());
}
/// <summary>
/// Resolves the name for a content item/content variant
/// </summary>
private class NameResolver : IValueResolver<EntitySlim, EntityBasic, string>
{
public string Resolve(EntitySlim source, EntityBasic destination, string destMember, ResolutionContext context)
{
if (!(source is DocumentEntitySlim doc))
return source.Name;
// invariant = only 1 name
if (!doc.Variations.VariesByCulture()) return source.Name;
// variant = depends on culture
var culture = context.Options.GetCulture();
// if there's no culture here, the issue is somewhere else (UI, whatever) - throw!
if (culture == null)
//throw new InvalidOperationException("Missing culture in mapping options.");
// fixme we should throw, but this is used in various places that won't set a culture yet
return source.Name;
// if we don't have a name for a culture, it means the culture is not available, and
// hey we should probably not be mapping it, but it's too late, return a fallback name
return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() ? name : $"(({source.Name}))";
}
}
}
}
@@ -54,7 +54,7 @@
/// Sets the node style to show that it is has unpublished versions (but is currently published)
/// </summary>
/// <param name="treeNode"></param>
public static void SetHasUnpublishedVersionStyle(this TreeNode treeNode)
public static void SetHasPendingVersionStyle(this TreeNode treeNode)
{
if (treeNode.CssClasses.Contains("has-unpublished-version") == false)
{
@@ -272,6 +272,9 @@ namespace Umbraco.Web.PropertyEditors
public IEnumerable<ValidationResult> Validate(object rawValue, string valueType, object dataTypeConfiguration)
{
if (rawValue == null)
yield break;
var value = JsonConvert.DeserializeObject<List<object>>(rawValue.ToString());
if (value == null)
yield break;
@@ -55,19 +55,18 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
{
using (_proflog.DebugDuration<PublishedPropertyType>($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})"))
{
var value = (string)inter;
if (string.IsNullOrWhiteSpace(value)) return null;
var objects = JsonConvert.DeserializeObject<List<JObject>>(value);
if (objects.Count == 0)
return Enumerable.Empty<IPublishedElement>();
var configuration = propertyType.DataType.ConfigurationAs<NestedContentConfiguration>();
var contentTypes = configuration.ContentTypes;
var elements = contentTypes.Length > 1
? new List<IPublishedElement>()
: PublishedModelFactory.CreateModelList(contentTypes[0].Alias);
var value = (string)inter;
if (string.IsNullOrWhiteSpace(value)) return elements;
var objects = JsonConvert.DeserializeObject<List<JObject>>(value);
if (objects.Count == 0) return elements;
foreach (var sourceObject in objects)
{
var element = ConvertToElement(sourceObject, referenceCacheLevel, preview);
@@ -1196,7 +1196,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
foreach (var (culture, name) in names)
{
cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) ?? DateTime.MinValue };
cultureData[culture] = new CultureVariation { Name = name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue };
}
//the dictionary that will be serialized
+19 -7
View File
@@ -47,10 +47,10 @@ namespace Umbraco.Web.Trees
/// <inheritdoc />
protected override TreeNode GetSingleTreeNode(IEntitySlim entity, string parentId, FormDataCollection queryStrings)
{
var langId = queryStrings?["culture"];
var culture = queryStrings?["culture"];
var allowedUserOptions = GetAllowedUserMenuItemsForNode(entity);
if (CanUserAccessNode(entity, allowedUserOptions, langId))
if (CanUserAccessNode(entity, allowedUserOptions, culture))
{
//Special check to see if it ia a container, if so then we'll hide children.
var isContainer = entity.IsContainer; // && (queryStrings.Get("isDialog") != "true");
@@ -74,11 +74,23 @@ namespace Umbraco.Web.Trees
{
var documentEntity = (IDocumentEntitySlim) entity;
//fixme we need these statuses per variant but to do that we need to fix the issues listed in IDocumentEntitySlim
if (!documentEntity.Published)
node.SetNotPublishedStyle();
//if (documentEntity.Edited)
// node.SetHasUnpublishedVersionStyle();
if (!documentEntity.Variations.VariesByCulture())
{
if (!documentEntity.Published)
node.SetNotPublishedStyle();
else if (documentEntity.Edited)
node.SetHasPendingVersionStyle();
}
else
{
if (!culture.IsNullOrWhiteSpace())
{
if (!documentEntity.PublishedCultures.Contains(culture))
node.SetNotPublishedStyle();
else if (documentEntity.EditedCultures.Contains(culture))
node.SetHasPendingVersionStyle();
}
}
node.AdditionalData.Add("contentType", documentEntity.ContentTypeAlias);
}
+1
View File
@@ -164,6 +164,7 @@
<Compile Include="Editors\LogViewerController.cs" />
<Compile Include="ImageProcessorLogger.cs" />
<Compile Include="Macros\MacroTagParser.cs" />
<Compile Include="Models\ContentEditing\ContentDomainsAndCulture.cs" />
<Compile Include="Models\ContentEditing\ContentSavedState.cs" />
<Compile Include="Trees\LogViewerTreeController.cs" />
<Compile Include="Models\Mapping\ContentSavedStateResolver.cs" />