diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index d18fb4b091..516192b905 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -115,7 +115,7 @@ namespace Umbraco.Core /// /// Determines whether a variation varies by culture and segment. /// - public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) > 0; + public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; /// /// Validates that a combination of culture and segment is valid for the variation. diff --git a/src/Umbraco.Core/Models/ApplicationTree.cs b/src/Umbraco.Core/Models/ApplicationTree.cs index 8b0bbc29c4..ccdebea724 100644 --- a/src/Umbraco.Core/Models/ApplicationTree.cs +++ b/src/Umbraco.Core/Models/ApplicationTree.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using Umbraco.Core.Services; namespace Umbraco.Core.Models { @@ -35,6 +36,7 @@ namespace Umbraco.Core.Models IconClosed = iconClosed; IconOpened = iconOpened; Type = type; + } /// @@ -85,6 +87,33 @@ namespace Umbraco.Core.Models /// The type. public string Type { get; set; } + /// + /// Returns the localized root node display name + /// + /// + /// + public string GetRootNodeDisplayName(ILocalizedTextService textService) + { + var label = $"[{Alias}]"; + + // try to look up a the localized tree header matching the tree alias + var localizedLabel = textService.Localize("treeHeaders/" + Alias); + + // if the localizedLabel returns [alias] then return the title attribute from the trees.config file, if it's defined + if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) + { + if (string.IsNullOrEmpty(Title) == false) + label = Title; + } + else + { + // the localizedLabel translated into something that's not just [alias], so use the translation + label = localizedLabel; + } + + return label; + } + private Type _runtimeType; /// diff --git a/src/Umbraco.Core/Models/ContentCultureInfos.cs b/src/Umbraco.Core/Models/ContentCultureInfos.cs index bcf1dbb1b1..f51e3a275a 100644 --- a/src/Umbraco.Core/Models/ContentCultureInfos.cs +++ b/src/Umbraco.Core/Models/ContentCultureInfos.cs @@ -28,11 +28,11 @@ namespace Umbraco.Core.Models /// Initializes a new instance of the class. /// /// Used for cloning, without change tracking. - private ContentCultureInfos(string culture, string name, DateTime date) - : this(culture) + internal ContentCultureInfos(ContentCultureInfos other) + : this(other.Culture) { - _name = name; - _date = date; + _name = other.Name; + _date = other.Date; } /// @@ -61,7 +61,7 @@ namespace Umbraco.Core.Models /// public object DeepClone() { - return new ContentCultureInfos(Culture, Name, Date); + return new ContentCultureInfos(this); } /// diff --git a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs index 5238e65631..82b0ba6475 100644 --- a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs +++ b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs @@ -24,8 +24,12 @@ namespace Umbraco.Core.Models public ContentCultureInfosCollection(IEnumerable items) : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) { + // make sure to add *copies* and not the original items, + // as items can be modified by AddOrUpdate, and therefore + // the new collection would be impacted by changes made + // to the old collection foreach (var item in items) - Add(item); + Add(new ContentCultureInfos(item)); } /// diff --git a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs index 0d2f817660..8af48bb881 100644 --- a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs @@ -63,20 +63,5 @@ namespace Umbraco.Core.Models aliases = a; return hasAnyPropertyVariationChanged; } - - /// - /// Returns the list of content types the composition is used in - /// - /// - /// - /// - internal static IEnumerable GetWhereCompositionIsUsedInContentTypes(this IContentTypeComposition source, - IContentTypeComposition[] allContentTypes) - { - var sourceId = source != null ? source.Id : 0; - - // find which content types are using this composition - return allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); - } } } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index cf43b661c7..08b9f74802 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -114,6 +114,27 @@ namespace Umbraco.Core.Models } } + /// + /// Gets the property types obtained via composition. + /// + /// + /// Gets them raw, ie with their original variation. + /// + [IgnoreDataMember] + internal IEnumerable RawComposedPropertyTypes => GetRawComposedPropertyTypes(); + + private IEnumerable GetRawComposedPropertyTypes(bool start = true) + { + var propertyTypes = ContentTypeComposition + .Cast() + .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes); + + if (!start) + propertyTypes = propertyTypes.Union(PropertyTypes); + + return propertyTypes; + } + /// /// Adds a content type to the composition. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 3bb1ac38ca..cc9b86c56b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -11,13 +11,6 @@ namespace Umbraco.Core.Persistence.Repositories TItem Get(string alias); IEnumerable> Move(TItem moving, EntityContainer container); - /// - /// Returns the content types that are direct compositions of the content type - /// - /// The content type id - /// - IEnumerable GetTypesDirectlyComposedOf(int id); - /// /// Derives a unique alias from an existing alias. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index aa61383f85..4bec3160a7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -67,7 +67,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, IsPublishing, this, _templateRepository); } - protected override IEnumerable PerformGetAll(params Guid[] ids) { // use the underlying GetAll which will force cache all content types diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index bea6eb9bce..3184c69dfe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -119,7 +119,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistNewBaseContentType(IContentTypeComposition entity) { - var dto = ContentTypeFactory.BuildContentTypeDto(entity); //Cannot add a duplicate content type type @@ -234,7 +233,6 @@ AND umbracoNode.nodeObjectType = @objectType", protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { - var dto = ContentTypeFactory.BuildContentTypeDto(entity); // ensure the alias is not used already @@ -270,8 +268,8 @@ AND umbracoNode.id <> @id", // 1. Find content based on the current ContentType: entity.Id // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one - var compositionBase = entity as ContentTypeCompositionBase; - if (compositionBase != null && compositionBase.RemovedContentTypeKeyTracker != null && + if (entity is ContentTypeCompositionBase compositionBase && + compositionBase.RemovedContentTypeKeyTracker != null && compositionBase.RemovedContentTypeKeyTracker.Any()) { //TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? @@ -314,7 +312,7 @@ AND umbracoNode.id <> @id", } } - // delete the allowed content type entries before re-inserting the collectino of allowed content types + // delete the allowed content type entries before re-inserting the collection of allowed content types Database.Delete("WHERE Id = @Id", new { entity.Id }); foreach (var allowedContentType in entity.AllowedContentTypes) { @@ -409,40 +407,34 @@ AND umbracoNode.id <> @id", } //check if the content type variation has been changed - var ctVariationChanging = entity.IsPropertyDirty("Variations"); - if (ctVariationChanging) + var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); + var oldContentTypeVariation = (ContentVariation) dtoPk.Variations; + var newContentTypeVariation = entity.Variations; + var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; + if (contentTypeVariationChanging) { - //we've already looked up the previous version of the content type so we know it's previous variation state - MoveVariantData(entity, (ContentVariation)dtoPk.Variations, entity.Variations); + MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); Clear301Redirects(entity); ClearScheduledPublishing(entity); - } + } - //track any content type/property types that are changing variation which will require content updates - var propertyTypeVariationChanges = new Dictionary(); + // collect property types that have a dirty variation + List propertyTypeVariationDirty = null; - // insert or update properties - // all of them, no-group and in-groups + // note: this only deals with *local* property types, we're dealing w/compositions later below foreach (var propertyType in entity.PropertyTypes) { - //if the content type variation isn't changing track if any property type is changing - if (!ctVariationChanging) + if (contentTypeVariationChanging) { - if (propertyType.IsPropertyDirty("Variations")) + // content type is changing + switch (newContentTypeVariation) { - propertyTypeVariationChanges[propertyType.Id] = propertyType.Variations; - } - } - else - { - switch(entity.Variations) - { - case ContentVariation.Nothing: - //if the content type is changing to Nothing, then all property type's must change to nothing + case ContentVariation.Nothing: // changing to Nothing + // all property types must change to Nothing propertyType.Variations = ContentVariation.Nothing; break; - case ContentVariation.Culture: - //we don't need to modify the property type in this case + case ContentVariation.Culture: // changing to Culture + // all property types can remain Nothing break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -451,15 +443,65 @@ AND umbracoNode.id <> @id", } } - var groupId = propertyType.PropertyGroupId?.Value ?? default(int); + // then, track each property individually + if (propertyType.IsPropertyDirty("Variations")) + { + // allocate the list only when needed + if (propertyTypeVariationDirty == null) + propertyTypeVariationDirty = new List(); + + propertyTypeVariationDirty.Add(propertyType); + } + } + + // figure out dirty property types that have actually changed + // before we insert or update properties, so we can read the old variations + var propertyTypeVariationChanges = propertyTypeVariationDirty != null + ? GetPropertyVariationChanges(propertyTypeVariationDirty) + : null; + + // deal with composition property types + // add changes for property types obtained via composition, which change due + // to this content type variations change + if (contentTypeVariationChanging) + { + // must use RawComposedPropertyTypes here: only those types that are obtained + // via composition, with their original variations (ie not filtered by this + // content type variations - we need this true value to make decisions. + + foreach (var propertyType in ((ContentTypeCompositionBase) entity).RawComposedPropertyTypes) + { + if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment()) + throw new NotSupportedException(); // TODO: support this + + if (propertyType.Variations == ContentVariation.Culture) + { + if (propertyTypeVariationChanges == null) + propertyTypeVariationChanges = new Dictionary(); + + // if content type moves to Culture, property type becomes Culture here again + // if content type moves to Nothing, property type becomes Nothing here + if (newContentTypeVariation == ContentVariation.Culture) + propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Nothing, ContentVariation.Culture); + else if (newContentTypeVariation == ContentVariation.Nothing) + propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Culture, ContentVariation.Nothing); + } + } + } + + // insert or update properties + // all of them, no-group and in-groups + foreach (var propertyType in entity.PropertyTypes) + { // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int)) + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) AssignDataTypeFromPropertyEditor(propertyType); // validate the alias ValidateAlias(propertyType); // insert or update property + var groupId = propertyType.PropertyGroupId?.Value ?? default; var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); var typeId = propertyType.HasIdentity ? Database.Update(propertyTypeDto) @@ -470,31 +512,22 @@ AND umbracoNode.id <> @id", typeId = propertyType.Id; // not an orphan anymore - if (orphanPropertyTypeIds != null) - orphanPropertyTypeIds.Remove(typeId); + orphanPropertyTypeIds?.Remove(typeId); } - //check if any property types were changing variation - if (propertyTypeVariationChanges.Count > 0) - { - var changes = new Dictionary(); + // must restrict property data changes to impacted content types - if changing a composing + // type, some composed types (those that do not vary) are not impacted and should be left + // unchanged + // + // getting 'all' from the cache policy is prone to race conditions - fast but dangerous + //var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); + var all = PerformGetAll(); - //now get the current property type variations for the changed ones so that we know which variation they - //are going from and to - var from = Database.Dictionary(Sql() - .Select(x => x.Id, x => x.Variations) - .From() - .WhereIn(x => x.Id, propertyTypeVariationChanges.Keys)); - - foreach (var f in from) - { - changes[f.Key] = (propertyTypeVariationChanges[f.Key], (ContentVariation)f.Value); - } - - //perform the move - MoveVariantData(changes); - } + var impacted = GetImpactedContentTypes(entity, all); + // if some property types have actually changed, move their variant data + if (propertyTypeVariationChanges != null) + MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); // deal with orphan properties: those that were in a deleted tab, // and have not been re-mapped to another tab or to 'generic properties' @@ -503,6 +536,77 @@ AND umbracoNode.id <> @id", DeletePropertyType(entity.Id, id); } + private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all) + { + var impact = new List(); + var set = new List { contentType }; + + var tree = new Dictionary>(); + foreach (var x in all) + foreach (var y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out var list)) + list = tree[y.Id] = new List(); + list.Add(x); + } + + var nset = new List(); + do + { + impact.AddRange(set); + + foreach (var x in set) + { + if (!tree.TryGetValue(x.Id, out var list)) continue; + nset.AddRange(list.Where(y => y.VariesByCulture())); + } + + set = nset; + nset = new List(); + } while (set.Count > 0); + + return impact; + } + + // gets property types that have actually changed, and the corresponding changes + // returns null if no property type has actually changed + private Dictionary GetPropertyVariationChanges(IEnumerable propertyTypes) + { + var propertyTypesL = propertyTypes.ToList(); + + // select the current variations (before the change) from database + var selectCurrentVariations = Sql() + .Select(x => x.Id, x => x.Variations) + .From() + .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); + + var oldVariations = Database.Dictionary(selectCurrentVariations); + + // build a dictionary of actual changes + Dictionary changes = null; + + foreach (var propertyType in propertyTypesL) + { + // new property type, ignore + if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) + continue; + var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly + + // only those property types that *actually* changed + var newVariation = propertyType.Variations; + if (oldVariation == newVariation) + continue; + + // allocate the dictionary only when needed + if (changes == null) + changes = new Dictionary(); + + changes[propertyType.Id] = (oldVariation, newVariation); + } + + return changes; + } + /// /// Clear any redirects associated with content for a content type /// @@ -529,28 +633,39 @@ AND umbracoNode.id <> @id", } /// - /// Moves variant data for property type changes + /// Gets the default language identifier. /// - /// - private void MoveVariantData(IDictionary propertyTypeChanges) + private int GetDefaultLanguageId() { - var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + var selectDefaultLanguageId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); + + return Database.First(selectDefaultLanguageId); + } + + /// + /// Moves variant data for property type variation changes. + /// + private void MovePropertyTypeVariantData(IDictionary propertyTypeChanges, IEnumerable impacted) + { + var defaultLanguageId = GetDefaultLanguageId(); + var impactedL = impacted.Select(x => x.Id).ToList(); //Group by the "To" variation so we can bulk update in the correct batches - foreach(var g in propertyTypeChanges.GroupBy(x => x.Value.Item2)) + foreach(var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) { - var propertyTypeIds = g.Select(s => s.Key).ToList(); + var propertyTypeIds = grouping.Select(x => x.Key).ToList(); + var toVariation = grouping.Key; - //the ContentVariation that the data is moving "To" - var toVariantType = g.Key; - - switch(toVariantType) + switch (toVariation) { case ContentVariation.Culture: - MovePropertyDataToVariantCulture(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); break; case ContentVariation.Nothing: - MovePropertyDataToVariantNothing(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -561,24 +676,17 @@ AND umbracoNode.id <> @id", } /// - /// Moves variant data for a content type variation change + /// Moves variant data for a content type variation change. /// - /// - /// - /// - private void MoveVariantData(IContentTypeComposition contentType, ContentVariation from, ContentVariation to) + private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, ContentVariation toVariation) { - var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + var defaultLanguageId = GetDefaultLanguageId(); - var sqlPropertyTypeIds = Sql().Select(x => x.Id).From().Where(x => x.ContentTypeId == contentType.Id); - switch (to) + switch (toVariation) { case ContentVariation.Culture: - //move the property data - MovePropertyDataToVariantCulture(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); - - //now we need to move the names + //move the names //first clear out any existing names that might already exists under the default lang //there's 2x tables to update @@ -588,10 +696,11 @@ AND umbracoNode.id <> @id", .InnerJoin().On(x => x.Id, x => x.VersionId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLangId); + .Where(x => x.LanguageId == defaultLanguageId); var sqlDelete = Sql() .Delete() .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); //clear out the documentCultureVariation table @@ -599,10 +708,11 @@ AND umbracoNode.id <> @id", .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLangId); + .Where(x => x.LanguageId == defaultLanguageId); sqlDelete = Sql() .Delete() .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); //now we need to insert names into these 2 tables based on the invariant data @@ -610,32 +720,31 @@ AND umbracoNode.id <> @id", //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) - .Append($", {defaultLangId}") //default language ID + .Append($", {defaultLanguageId}") //default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); //insert rows into the documentCultureVariation table cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) .AndSelect(x => x.Text) - .Append($", 1, {defaultLangId}") //make Available + default language ID + .Append($", 1, {defaultLanguageId}") //make Available + default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); break; case ContentVariation.Nothing: - //move the property data - MovePropertyDataToVariantNothing(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); - - //we dont need to move the names! this is because we always keep the invariant names with the name of the default language. + //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( // if we want these SQL statements back, look into GIT history @@ -649,73 +758,102 @@ AND umbracoNode.id <> @id", } /// - /// This will move all property data from variant to invariant + /// Copies property data from one language to another. /// - /// - /// Optional list of property type ids of the properties to be updated - /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated - private void MovePropertyDataToVariantNothing(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) + /// The source language (can be null ie invariant). + /// The target language (can be null ie invariant) + /// The property type identifiers. + /// The content type identifiers. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) { - //first clear out any existing property data that might already exists under the default lang + // fixme - should we batch then? + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + //first clear out any existing property data that might already exists under the target language var sqlDelete = Sql() - .Delete() - .Where(x => x.LanguageId == null); - if (sqlPropertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .Delete(); + + // not ok for SqlCe (no JOIN in DELETE) + //if (contentTypeIds != null) + // sqlDelete + // .From() + // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); + + Sql inSql = null; + if (contentTypeIds != null) + { + inSql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlDelete.WhereIn(x => x.VersionId, inSql); + } + + // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it + if (targetLanguageId == null) + sqlDelete.Where(x => x.LanguageId == null); + else + sqlDelete.Where(x => x.LanguageId == targetLanguageId); + + sqlDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + // see note above, not ok for SqlCe + //if (contentTypeIds != null) + // sqlDelete + // .WhereIn(x => x.ContentTypeId, contentTypeIds); Database.Execute(sqlDelete); - //now insert all property data into the default language that exists under the invariant lang + //now insert all property data into the target language that exists under the source language + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append(", NULL") //null language ID - .From() - .Where(x => x.LanguageId == defaultLangId); - if (sqlPropertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .Append(", " + targetLanguageIdS) //default language ID + .From(); + + if (contentTypeIds != null) + sqlSelectData + .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); + + // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it + if (sourceLanguageId == null) + sqlSelectData.Where(x => x.LanguageId == null); + else + sqlSelectData.Where(x => x.LanguageId == sourceLanguageId); + + sqlSelectData + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + sqlSelectData + .WhereIn(x => x.ContentTypeId, contentTypeIds); var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); Database.Execute(sqlInsert); - } - /// - /// This will move all property data from invariant to variant - /// - /// - /// Optional list of property type ids of the properties to be updated - /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated - private void MovePropertyDataToVariantCulture(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) - { - //first clear out any existing property data that might already exists under the default lang - var sqlDelete = Sql() - .Delete() - .Where(x => x.LanguageId == defaultLangId); - if (sqlPropertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + // when copying from Culture, keep the original values around in case we want to go back + // when copying from Nothing, kill the original values, we don't want them around + if (sourceLanguageId == null) + { + sqlDelete = Sql() + .Delete(); - Database.Execute(sqlDelete); + if (contentTypeIds != null) + sqlDelete.WhereIn(x => x.VersionId, inSql); - //now insert all property data into the default language that exists under the invariant lang - var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); - var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append($", {defaultLangId}") //default language ID - .From() - .Where(x => x.LanguageId == null); - if (sqlPropertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + sqlDelete + .Where(x => x.LanguageId == null) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - Database.Execute(sqlInsert); + Database.Execute(sqlDelete); + } } private void DeletePropertyType(int contentTypeId, int propertyTypeId) @@ -851,24 +989,6 @@ AND umbracoNode.id <> @id", } } - /// - public IEnumerable GetTypesDirectlyComposedOf(int id) - { - //fixme - this will probably be more efficient to simply load all content types and do the calculation, see GetWhereCompositionIsUsedInContentTypes - - var sql = Sql() - .SelectAll() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.ChildId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == id); - var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetMany(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) - : Enumerable.Empty(); - } - internal static class ContentTypeQueryMapper { public class AssociatedTemplate diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index b65ab83439..7c3e835a77 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -1,15 +1,10 @@ using System; using System.Collections.Concurrent; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; using System.Globalization; using System.Linq; using System.Security.Claims; using System.Security.Principal; -using System.Text; using System.Threading; -using System.Threading.Tasks; namespace Umbraco.Core.Security { diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 66b3982b49..06ba1ada79 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -30,12 +30,12 @@ namespace Umbraco.Core.Services string[] filterPropertyTypes = null) { filterContentTypes = filterContentTypes == null - ? new string[] { } - : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + ? Array.Empty() + : filterContentTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); filterPropertyTypes = filterPropertyTypes == null - ? new string[] {} - : filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + ? Array.Empty() + : filterPropertyTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); //create the full list of property types to use as the filter //this is the combination of all property type aliases found in the content types passed in for the filter @@ -47,7 +47,7 @@ namespace Umbraco.Core.Services .Union(filterPropertyTypes) .ToArray(); - var sourceId = source != null ? source.Id : 0; + var sourceId = source?.Id ?? 0; // find out if any content type uses this content type var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); @@ -161,6 +161,5 @@ namespace Umbraco.Core.Services return all; } - } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index ed682096c0..49f3d1a123 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -368,6 +368,11 @@ namespace Umbraco.Core.Services /// A publishing document is a document with values that are being published, i.e. /// that have been published or cleared via and /// . + /// When one needs to publish or unpublish a single culture, or all cultures, using + /// and is the way to go. But if one needs to, say, publish two cultures and unpublish a third + /// one, in one go, then one needs to invoke and + /// on the content itself - this prepares the content, but does not commit anything - and then, invoke + /// to actually commit the changes to the database. /// The document is *always* saved, even when publishing fails. /// PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true); @@ -375,11 +380,30 @@ namespace Umbraco.Core.Services /// /// Saves and publishes a document branch. /// + /// + /// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more + /// that one culture, see the other overload of this method. + /// The parameter determines which documents are published. When false, + /// only those documents that are already published, are republished. When true, all documents are + /// published. + /// IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0); /// /// Saves and publishes a document branch. /// + /// + /// The parameter determines which documents are published. When false, + /// only those documents that are already published, are republished. When true, all documents are + /// published. + /// The parameter is a function which determines whether a document has + /// values to publish (else there is no need to publish it). If one wants to publish only a selection of + /// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other + /// cultures may trigger an unwanted republish. + /// The parameter is a function to execute to publish cultures, on + /// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating + /// whether the cultures could be published. + /// IEnumerable SaveAndPublishBranch(IContent content, bool force, Func editing, Func publishCultures, int userId = 0); /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index ac1171cfdd..4025effb21 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1272,12 +1272,49 @@ namespace Umbraco.Core.Services.Implement bool IsEditing(IContent c, string l) => c.PublishName != c.Name || - c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || - c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture == l).Any(y => !y.EditedValue.Equals(y.PublishedValue))); + c.PublishedCultures.Where(x => x.InvariantEquals(l)).Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || + c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture.InvariantEquals(l)).Any(y => !y.EditedValue.Equals(y.PublishedValue))); return SaveAndPublishBranch(content, force, document => IsEditing(document, culture), document => document.PublishCulture(culture), userId); } + // fixme - make this public once we know it works + document + private IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = 0) + { + // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() + // and not to == them, else we would be comparing references, and that is a bad thing + + cultures = cultures ?? Array.Empty(); + + // determines whether the document is edited, and thus needs to be published, + // for the specified cultures (it may be edited for other cultures and that + // should not trigger a publish). + bool IsEdited(IContent c) + { + if (cultures.Length == 0) + { + // nothing = everything + return c.PublishName != c.Name || + c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || + c.Properties.Any(x => x.Values.Any(y => !y.EditedValue.Equals(y.PublishedValue))); + } + + return c.PublishName != c.Name || + c.PublishedCultures.Where(x => cultures.Contains(x, StringComparer.InvariantCultureIgnoreCase)).Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || + c.Properties.Any(x => x.Values.Where(y => cultures.Contains(y.Culture, StringComparer.InvariantCultureIgnoreCase)).Any(y => !y.EditedValue.Equals(y.PublishedValue))); + } + + // publish the specified cultures + bool PublishCultures(IContent c) + { + return cultures.Length == 0 + ? c.PublishCulture() // nothing = everything + : cultures.All(c.PublishCulture); + } + + return SaveAndPublishBranch(content, force, IsEdited, PublishCultures, userId); + } + /// public IEnumerable SaveAndPublishBranch(IContent document, bool force, Func editing, Func publishCultures, int userId = 0) diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 60677cfd81..b74abc03f7 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -344,37 +344,18 @@ namespace Umbraco.Core.Services.Implement } } + public IEnumerable GetComposedOf(int id, IEnumerable all) + { + return all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id)); + + } + public IEnumerable GetComposedOf(int id) { - //fixme: this is essentially the same as ContentTypeServiceExtensions.GetWhereCompositionIsUsedInContentTypes which loads - // all content types to figure this out, this instead makes quite a few queries so should be replaced - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(ReadLockIds); - - // hash set handles duplicates - var composed = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id.GetHashCode())); - - var ids = new Stack(); - ids.Push(id); - - while (ids.Count > 0) - { - var i = ids.Pop(); - var result = Repository.GetTypesDirectlyComposedOf(i).ToArray(); - - foreach (var c in result) - { - composed.Add(c); - ids.Push(c.Id); - } - } - - return composed.ToArray(); - } + // GetAll is cheap, repository has a full dataset cache policy + // fixme - still, because it uses the cache, race conditions! + var allContentTypes = GetAll(Array.Empty()); + return GetComposedOf(id, allContentTypes); } public int Count() diff --git a/src/Umbraco.Tests/Composing/ActionCollectionTests.cs b/src/Umbraco.Tests/Composing/ActionCollectionTests.cs index 04bd0a2e1e..46e4eee765 100644 --- a/src/Umbraco.Tests/Composing/ActionCollectionTests.cs +++ b/src/Umbraco.Tests/Composing/ActionCollectionTests.cs @@ -49,6 +49,8 @@ namespace Umbraco.Tests.Composing public bool ShowInNotifier => false; public bool CanBePermissionAssigned => true; + + public bool OpensDialog => true; } public class NonSingletonAction : IAction @@ -66,6 +68,8 @@ namespace Umbraco.Tests.Composing public bool ShowInNotifier => false; public bool CanBePermissionAssigned => true; + + public bool OpensDialog => true; } #endregion diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index f25382d557..f186ae8e83 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -1,10 +1,8 @@ -using System.Runtime.Remoting; -using NUnit.Framework; +using NUnit.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using NPoco; using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; @@ -12,10 +10,9 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; -using Umbraco.Core.Components; +using Umbraco.Tests.Scoping; namespace Umbraco.Tests.Services { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs new file mode 100644 index 0000000000..c28d4f7955 --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using LightInject; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Sync; +using Umbraco.Tests.Testing; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.DataSource; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true, WithApplication = true)] + public class ContentTypeServiceVariantsTests : TestWithSomeContentBase + { + protected override void Compose() + { + base.Compose(); + + // pfew - see note in ScopedNuCacheTests? + Container.RegisterSingleton(); + Container.RegisterSingleton(f => Mock.Of()); + Container.RegisterCollectionBuilder() + .Add(f => f.TryGetInstance().GetCacheRefreshers()); + } + + protected override IPublishedSnapshotService CreatePublishedSnapshotService() + { + var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); + var runtimeStateMock = new Mock(); + runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run); + + var contentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), Mock.Of()); + //var documentRepository = Mock.Of(); + var documentRepository = Container.GetInstance(); + var mediaRepository = Mock.Of(); + var memberRepository = Mock.Of(); + + return new PublishedSnapshotService( + options, + null, + runtimeStateMock.Object, + ServiceContext, + contentTypeFactory, + null, + publishedSnapshotAccessor, + Mock.Of(), + Logger, + ScopeProvider, + documentRepository, mediaRepository, memberRepository, + DefaultCultureAccessor, + new DatabaseDataSource(), + Container.GetInstance(), new SiteDomainHelper()); + } + + public class LocalServerMessenger : ServerMessengerBase + { + public LocalServerMessenger() + : base(false) + { } + + protected override void DeliverRemote(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + throw new NotImplementedException(); + } + } + + private void AssertJsonStartsWith(int id, string expected) + { + var json = GetJson(id).Replace('"', '\''); + var pos = json.IndexOf("'cultureData':", StringComparison.InvariantCultureIgnoreCase); + json = json.Substring(0, pos + "'cultureData':".Length); + Assert.AreEqual(expected, json); + } + + private string GetJson(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var selectJson = SqlContext.Sql().Select().From().Where(x => x.NodeId == id && !x.Published); + var dto = scope.Database.Fetch(selectJson).FirstOrDefault(); + Assert.IsNotNull(dto); + var json = dto.Data; + return json; + } + } + + [Test] + public void Change_Variations_SimpleContentType_VariantToInvariantAndBack() + { + // one simple content type, variant, with both variant and invariant properties + // can change it to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Culture + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent) new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en", "en"); + document.SetValue("value1", "v1fr", "fr"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content type to Nothing + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content back to Culture + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property back to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_SimpleContentType_InvariantToVariantAndBack() + { + // one simple content type, invariant + // can change it to variant and back + // can then switch one property to variant + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Nothing + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Nothing + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent) new Content("document", -1, contentType); + document.Name = "doc1"; + document.SetValue("value1", "v1"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content type to Culture + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.AreEqual("v1", document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch content back to Nothing + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1", document.Name); + Assert.IsNull(document.GetCultureName("en")); + Assert.IsNull(document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_SimpleContentType_VariantPropertyToInvariantAndBack() + { + // one simple content type, variant, with both variant and invariant properties + // can change an invariant property to variant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Culture + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value2", ValueStorageType.Ntext) + { + Alias = "value2", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent)new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en", "en"); + document.SetValue("value1", "v1fr", "fr"); + document.SetValue("value2", "v2"); + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property type to Nothing + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); + Assert.IsNull(document.GetValue("value1", "fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsFalse(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch property back to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value1").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + + // switch other property to Culture + contentType.PropertyTypes.First(x => x.Alias == "value2").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v2", document.GetValue("value2", "en")); + Assert.IsNull(document.GetValue("value2", "fr")); + Assert.IsNull(document.GetValue("value2")); + + Assert.IsTrue(document.ContentType.PropertyTypes.First(x => x.Alias == "value2").VariesByCulture()); + + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_ComposedContentType_1() + { + // one composing content type, variant, with both variant and invariant properties + // one composed content type, variant, with both variant and invariant properties + // can change the composing content type to invariant and back + // can change the composed content type to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var composing = new ContentType(-1) + { + Alias = "composing", + Name = "composing", + Variations = ContentVariation.Culture + }; + + var properties1 = new PropertyTypeCollection(true) + { + new PropertyType("value11", ValueStorageType.Ntext) + { + Alias = "value11", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value12", ValueStorageType.Ntext) + { + Alias = "value12", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(composing); + + var composed = new ContentType(-1) + { + Alias = "composed", + Name = "composed", + Variations = ContentVariation.Culture + }; + + var properties2 = new PropertyTypeCollection(true) + { + new PropertyType("value21", ValueStorageType.Ntext) + { + Alias = "value21", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value22", ValueStorageType.Ntext) + { + Alias = "value22", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); + composed.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed); + + var document = (IContent) new Content("document", -1, composed); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value11", "v11en", "en"); + document.SetValue("value11", "v11fr", "fr"); + document.SetValue("value12", "v12"); + document.SetValue("value21", "v21en", "en"); + document.SetValue("value21", "v21fr", "fr"); + document.SetValue("value22", "v22"); + ServiceContext.ContentService.Save(document); + + // both value11 and value21 are variant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composed); + + // both value11 and value21 are invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed); + + // value11 is variant again, but value21 is still invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composed.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed); + + // we can make it variant again + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is still invariant + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // we can make it variant again + Console.WriteLine(GetJson(document.Id)); + AssertJsonStartsWith(document.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + } + + [Test] + public void Change_Variations_ComposedContentType_2() + { + // one composing content type, variant, with both variant and invariant properties + // one composed content type, variant, with both variant and invariant properties + // one composed content type, invariant + // can change the composing content type to invariant and back + // can change the variant composed content type to invariant and back + + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + + var composing = new ContentType(-1) + { + Alias = "composing", + Name = "composing", + Variations = ContentVariation.Culture + }; + + var properties1 = new PropertyTypeCollection(true) + { + new PropertyType("value11", ValueStorageType.Ntext) + { + Alias = "value11", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value12", ValueStorageType.Ntext) + { + Alias = "value12", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(composing); + + var composed1 = new ContentType(-1) + { + Alias = "composed1", + Name = "composed1", + Variations = ContentVariation.Culture + }; + + var properties2 = new PropertyTypeCollection(true) + { + new PropertyType("value21", ValueStorageType.Ntext) + { + Alias = "value21", + DataTypeId = -88, + Variations = ContentVariation.Culture + }, + new PropertyType("value22", ValueStorageType.Ntext) + { + Alias = "value22", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed1.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); + composed1.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed1); + + var composed2 = new ContentType(-1) + { + Alias = "composed2", + Name = "composed2", + Variations = ContentVariation.Nothing + }; + + var properties3 = new PropertyTypeCollection(true) + { + new PropertyType("value31", ValueStorageType.Ntext) + { + Alias = "value31", + DataTypeId = -88, + Variations = ContentVariation.Nothing + }, + new PropertyType("value32", ValueStorageType.Ntext) + { + Alias = "value32", + DataTypeId = -88, + Variations = ContentVariation.Nothing + } + }; + + composed2.PropertyGroups.Add(new PropertyGroup(properties3) { Name = "Content" }); + composed2.AddContentType(composing); + ServiceContext.ContentTypeService.Save(composed2); + + var document1 = (IContent) new Content ("document1", -1, composed1); + document1.SetCultureName("doc1en", "en"); + document1.SetCultureName("doc1fr", "fr"); + document1.SetValue("value11", "v11en", "en"); + document1.SetValue("value11", "v11fr", "fr"); + document1.SetValue("value12", "v12"); + document1.SetValue("value21", "v21en", "en"); + document1.SetValue("value21", "v21fr", "fr"); + document1.SetValue("value22", "v22"); + ServiceContext.ContentService.Save(document1); + + var document2 = (IContent)new Content("document2", -1, composed2); + document2.Name = "doc2"; + document2.SetValue("value11", "v11"); + document2.SetValue("value12", "v12"); + document2.SetValue("value31", "v31"); + document2.SetValue("value32", "v32"); + ServiceContext.ContentService.Save(document2); + + // both value11 and value21 are variant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composed1); + + // both value11 and value21 are invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed1); + + // value11 is variant again, but value21 is still invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composed1.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composed1); + + // we can make it variant again + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // value11 is still invariant + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + + composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(composing); + + // we can make it variant again + Console.WriteLine(GetJson(document1.Id)); + AssertJsonStartsWith(document1.Id, + "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + + Console.WriteLine(GetJson(document2.Id)); + AssertJsonStartsWith(document2.Id, + "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 088a696986..4529c4f1ef 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -110,6 +110,7 @@ namespace Umbraco.Tests.TestHelpers IUmbracoSettingsSection umbracoSettings, IEventMessagesFactory eventMessagesFactory, IEnumerable urlSegmentProviders, + TypeLoader typeLoader, IServiceFactory container = null) { if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); @@ -183,7 +184,7 @@ namespace Umbraco.Tests.TestHelpers var macroService = GetLazyService(container, c => new MacroService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c))); var packagingService = GetLazyService(container, c => new PackagingService(logger, contentService.Value, contentTypeService.Value, mediaService.Value, macroService.Value, dataTypeService.Value, fileService.Value, localizationService.Value, entityService.Value, userService.Value, scopeProvider, urlSegmentProviders, GetRepo(c), GetRepo(c), new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty())))); var relationService = GetLazyService(container, c => new RelationService(scopeProvider, logger, eventMessagesFactory, entityService.Value, GetRepo(c), GetRepo(c))); - var treeService = GetLazyService(container, c => new ApplicationTreeService(logger, cache)); + var treeService = GetLazyService(container, c => new ApplicationTreeService(logger, cache, typeLoader)); var tagService = GetLazyService(container, c => new TagService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var sectionService = GetLazyService(container, c => new SectionService(userService.Value, treeService.Value, scopeProvider, cache)); var redirectUrlService = GetLazyService(container, c => new RedirectUrlService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); diff --git a/src/Umbraco.Tests/UI/LegacyDialogTests.cs b/src/Umbraco.Tests/UI/LegacyDialogTests.cs index 99391104ab..be9b0d4d7e 100644 --- a/src/Umbraco.Tests/UI/LegacyDialogTests.cs +++ b/src/Umbraco.Tests/UI/LegacyDialogTests.cs @@ -23,7 +23,7 @@ namespace Umbraco.Tests.UI } } - [TestCase(typeof(macroTasks), Constants.Applications.Packages)] + [TestCase(typeof(macroTasks), Constants.Applications.Settings)] [TestCase(typeof(CreatedPackageTasks), Constants.Applications.Packages)] public void Check_Assigned_Apps_For_Tasks(Type taskType, string app) { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 04bccc8bcb..c62e79b4ef 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -129,6 +129,7 @@ + diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index defdec5660..1e6229fa4c 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Linq; using System.Web; using LightInject; @@ -14,15 +13,12 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Security; using Umbraco.Web.Templates; -using System.Linq; -using Umbraco.Core.Services; using Umbraco.Core.Configuration; namespace Umbraco.Tests.Web @@ -118,4 +114,4 @@ namespace Umbraco.Tests.Web } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index a3a212a603..1987c897f0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -194,7 +194,9 @@ return a.alias === "umbContent"; }); - contentApp.viewModel = _.omit(variant, 'apps'); + //The view model for the content app is simply the index of the variant being edited + var variantIndex = vm.content.variants.indexOf(variant); + contentApp.viewModel = variantIndex; // make sure the same app it set to active in the new variant if(activeAppAlias) { 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 f52258333d..15296a6aaa 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 @@ -51,6 +51,15 @@ text-decoration: none; } +.umb-action { + &.-opens-dialog { + .menu-label:after { + // adds an ellipsis (...) after the menu label for actions that open a dialog + content: '\2026'; + } + } +} + .umb-actions-child { .umb-action { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html index 58b422ceb2..2ccbf11cc1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html @@ -16,24 +16,23 @@
-
+
...
-
+
...
-
+
...
-
-
+
...
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 5b5de1b393..002b617f84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -376,7 +376,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", var foundIndex = 0; if ($scope.model.selection.length > 0) { - for (i = 0; $scope.model.selection.length > i; i++) { + for (var i = 0; $scope.model.selection.length > i; i++) { var selectedItem = $scope.model.selection[i]; if (selectedItem.id === item.id) { found = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index fa7a797125..827b2ad4e0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -285,7 +285,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", var foundIndex = 0; if ($scope.model.selection.length > 0) { - for (i = 0; $scope.model.selection.length > i; i++) { + for (var i = 0; $scope.model.selection.length > i; i++) { var selectedItem = $scope.model.selection[i]; if (selectedItem.id === item.id) { found = true; 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 32dd57ade3..9d3fa3765d 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,7 +5,7 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 149fccd00a..e0f40a1b3b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -72,7 +72,7 @@ + current-section="{{menu.currentNode.section}}"> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html index f724f39be7..bf9c8fab8c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html @@ -8,7 +8,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 2a5bc4a572..1e8c1f74e5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -13,7 +13,8 @@ label-key="contentTypeEditor_compositions" icon="icon-merge" action="openCompositionsDialog()" - size="xs"> + size="xs" + add-ellipsis="true"> - New folder + New folder... diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js index 8c1e7edb73..f91e0ac2c3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js @@ -20,7 +20,7 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes vm.page.menu.currentNode = null; vm.description = ""; vm.showBackButton = true; - + vm.save = saveDictionary; vm.back = back; @@ -102,9 +102,9 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes }); } } - + function back() { - $location.path("settings/dictionary/list"); + $location.path(vm.page.menu.currentSection + "/dictionary/list"); } $scope.$watch("vm.content.name", function (newVal, oldVal) { diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html index d19b1329d2..c9f62cd870 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html @@ -25,7 +25,7 @@ - Document Type Collection + Document Type Collection... @@ -33,7 +33,7 @@
  • - + ...
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 10c563a289..ec7a30f9ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -48,7 +48,8 @@ "shortcuts_toggleListView", "shortcuts_toggleAllowAsRoot", "shortcuts_addChildNode", - "shortcuts_addTemplate" + "shortcuts_addTemplate", + "shortcuts_toggleAllowCultureVariants" ]; onInit(); @@ -81,6 +82,7 @@ vm.labels.allowAsRoot = values[11]; vm.labels.addChildNode = values[12]; vm.labels.addTemplate = values[13]; + vm.labels.allowCultureVariants = values[14]; var buttons = [ { @@ -161,6 +163,10 @@ { "description": vm.labels.addChildNode, "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] + }, + { + "description": vm.labels.allowCultureVariants, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "v" }] } ] }, diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js index 028380ff81..4a7a870618 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js @@ -23,7 +23,8 @@ vm.addChild = addChild; vm.removeChild = removeChild; - vm.toggle = toggle; + vm.toggleAllowAsRoot = toggleAllowAsRoot; + vm.toggleAllowCultureVariants = toggleAllowCultureVariants; /* ---------- INIT ---------- */ @@ -86,7 +87,7 @@ /** * Toggle the $scope.model.allowAsRoot value to either true or false */ - function toggle(){ + function toggleAllowAsRoot(){ if($scope.model.allowAsRoot){ $scope.model.allowAsRoot = false; return; @@ -95,6 +96,15 @@ $scope.model.allowAsRoot = true; } + function toggleAllowCultureVariants() { + if ($scope.model.allowCultureVariant) { + $scope.model.allowCultureVariant = false; + return; + } + + $scope.model.allowCultureVariant = true; + } + } angular.module("umbraco").controller("Umbraco.Editors.DocumentType.PermissionsController", PermissionsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html index 2ecd1c518c..ec1e528f8c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html @@ -10,11 +10,10 @@
    - +
    @@ -28,14 +27,13 @@
    - +
    @@ -44,18 +42,20 @@
    -
    Content Type Variation
    - Define the rules for how this content type's properties can be varied -
    -
    - +
    +
    +
    + + +
    +
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/media/edit.html b/src/Umbraco.Web.UI.Client/src/views/media/edit.html index a9afde36ac..2dfc8c967e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html @@ -19,7 +19,7 @@
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.html index ca85bcbf9e..795fd0ba7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.html @@ -16,7 +16,7 @@
  • - + ...
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/create.html b/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/create.html index 36ab0e71c1..74a611b3d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/create.html @@ -25,13 +25,13 @@
  • - >New partial view macro from snippet + >New partial view macro from snippet...
  • - + ...
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html b/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html index 59c0b0b344..cfeb2396a7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html @@ -18,13 +18,13 @@
  • - New partial view from snippet + New partial view from snippet...
  • - + ...
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 70572a5bcf..8bbf440fae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -282,7 +282,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper }); /** Syncs the renderModel based on the actual model.value and returns a promise */ - function syncRenderModel() { + function syncRenderModel(validate) { var valueIds = $scope.model.value ? $scope.model.value.split(',') : []; @@ -324,7 +324,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper }); - validate(); + if (validate) { + validate(); + } + setSortingState($scope.renderModel); return $q.when(true); }); @@ -344,7 +347,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper } } - validate(); + if (validate) { + validate(); + } + setSortingState($scope.renderModel); return $q.when(true); } @@ -425,7 +431,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper } function init() { - syncRenderModel().then(function () { + syncRenderModel(false).then(function () { //everything is loaded, start the watch on the model startWatch(); subscribe(); diff --git a/src/Umbraco.Web.UI.Client/src/views/scripts/create.html b/src/Umbraco.Web.UI.Client/src/views/scripts/create.html index d4c21b4b8a..8b5e0732d2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/scripts/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/scripts/create.html @@ -13,7 +13,7 @@
  • - + ...
  • diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index cd1591df8d..ed86e0988b 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -66,6 +66,7 @@ Allow access to move a node Allow access to set and change public access for a node Allow access to publish a node + Allow access to unpublish a node Allow access to change permissions for a node Allow access to roll back a node to a previous state Allow access to send a node for approval before publishing @@ -680,6 +681,7 @@ Move Lines Down General Editor + Toggle allow culture variants Background colour @@ -1487,6 +1489,9 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: + Allow varying by culture + Allow editors to create content of this type in different languages + Allow varying by culture Building models 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 c6574198e8..7196de1f93 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -65,6 +65,7 @@ Allow access to move a node Allow access to set and change public access for a node Allow access to publish a node + Allow access to unpublish a node Allow access to change permissions for a node Allow access to roll back a node to a previous state Allow access to send a node for approval before publishing @@ -700,6 +701,7 @@ Move Lines Down General Editor + Toggle allow culture variants Background color @@ -1509,6 +1511,9 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: + Allow varying by culture + Allow editors to create content of this type in different languages + Allow varying by culture Add language diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index ea5546b6ea..5210f1b756 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1124,7 +1124,7 @@ namespace Umbraco.Web.Editors /// /// The content and variants to unpublish /// - [EnsureUserPermissionForContent("model.Id", 'U')] + [EnsureUserPermissionForContent("model.Id", 'Z')] [OutgoingEditorModelEvent] public ContentItemDisplay PostUnpublish(UnpublishContent model) { diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 22d86631ca..563e8dda1a 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -119,66 +119,74 @@ namespace Umbraco.Web.Editors /// Type of content Type, eg documentType or mediaType /// Id of composition content type /// - protected IEnumerable PerformGetWhereCompositionIsUsedInContentTypes(int contentTypeId, - UmbracoObjectTypes type) + protected IEnumerable PerformGetWhereCompositionIsUsedInContentTypes(int contentTypeId, UmbracoObjectTypes type) { - IContentTypeComposition source = null; + var id = 0; - //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic + if (contentTypeId > 0) + { + IContentTypeComposition source; - IContentTypeComposition[] allContentTypes; + switch (type) + { + case UmbracoObjectTypes.DocumentType: + source = Services.ContentTypeService.Get(contentTypeId); + break; + + case UmbracoObjectTypes.MediaType: + source = Services.ContentTypeService.Get(contentTypeId); + break; + + case UmbracoObjectTypes.MemberType: + source = Services.MemberTypeService.Get(contentTypeId); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + + if (source == null) + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + id = source.Id; + } + + IEnumerable composedOf; switch (type) { case UmbracoObjectTypes.DocumentType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.ContentTypeService.GetAll().Cast().ToArray(); + composedOf = Services.ContentTypeService.GetComposedOf(id); break; case UmbracoObjectTypes.MediaType: - if (contentTypeId > 0) - { - source = Services.ContentTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.ContentTypeService.GetAll().Cast().ToArray(); + composedOf = Services.MediaTypeService.GetComposedOf(id); break; case UmbracoObjectTypes.MemberType: - if (contentTypeId > 0) - { - source = Services.MemberTypeService.Get(contentTypeId); - if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - } - allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray(); + composedOf = Services.MemberTypeService.GetComposedOf(id); break; default: - throw new ArgumentOutOfRangeException("The entity type was not a content type"); + throw new ArgumentOutOfRangeException(nameof(type)); } - var contentTypesWhereCompositionIsUsed = source.GetWhereCompositionIsUsedInContentTypes(allContentTypes); - return contentTypesWhereCompositionIsUsed - .Select(x => Mapper.Map(x)) - .Select(x => - { - //translate the name - x.Name = TranslateItem(x.Name); + EntityBasic TranslateName(EntityBasic e) + { + e.Name = TranslateItem(e.Name); + return e; + } - return x; - }) + return composedOf + .Select(Mapper.Map) + .Select(TranslateName) .ToList(); } + protected string TranslateItem(string text) { if (text == null) - { return null; - } if (text.StartsWith("#") == false) return text; diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index fcbe4bdd4c..5444aadcec 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -131,13 +131,10 @@ namespace Umbraco.Web.Editors if (tree == null) continue; //shouldn't occur var searchableTreeAttribute = searchableTree.Value.SearchableTree.GetType().GetCustomAttribute(false); - var treeAttribute = tree.GetTreeAttribute(); - long total; - - result[treeAttribute.GetRootNodeDisplayName(Services.TextService)] = new TreeSearchResult + result[tree.GetRootNodeDisplayName(Services.TextService)] = new TreeSearchResult { - Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out total), + Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out var total), TreeAlias = searchableTree.Key, AppAlias = searchableTree.Value.AppAlias, JsFormatterService = searchableTreeAttribute == null ? "" : searchableTreeAttribute.ServiceName, diff --git a/src/Umbraco.Web/Models/Trees/MenuItem.cs b/src/Umbraco.Web/Models/Trees/MenuItem.cs index 88d772b939..412cd9106d 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItem.cs @@ -38,6 +38,7 @@ namespace Umbraco.Web.Models.Trees SeperatorBefore = false; Icon = legacyMenu.Icon; Action = legacyMenu; + OpensDialog = legacyMenu.OpensDialog; } #endregion @@ -71,6 +72,10 @@ namespace Umbraco.Web.Models.Trees [DataMember(Name = "cssclass")] public string Icon { get; set; } + + [DataMember(Name = "opensDialog")] + public bool OpensDialog { get; set; } + #endregion #region Constants diff --git a/src/Umbraco.Web/Models/Trees/SectionRootNode.cs b/src/Umbraco.Web/Models/Trees/SectionRootNode.cs index cd4fc3e483..730f6e2962 100644 --- a/src/Umbraco.Web/Models/Trees/SectionRootNode.cs +++ b/src/Umbraco.Web/Models/Trees/SectionRootNode.cs @@ -28,6 +28,7 @@ namespace Umbraco.Web.Models.Trees { private static readonly string RootId = Core.Constants.System.Root.ToString(CultureInfo.InvariantCulture); private bool _isGroup; + private bool _isSingleNodeTree; /// /// Creates a group node for grouped multiple trees @@ -87,13 +88,15 @@ namespace Umbraco.Web.Models.Trees /// /// /// + /// /// - public static TreeRootNode CreateSingleTreeRoot(string nodeId, string getChildNodesUrl, string menuUrl, string title, TreeNodeCollection children) + public static TreeRootNode CreateSingleTreeRoot(string nodeId, string getChildNodesUrl, string menuUrl, string title, TreeNodeCollection children, bool isSingleNodeTree = false) { return new TreeRootNode(nodeId, getChildNodesUrl, menuUrl) { Children = children, - Name = title + Name = title, + _isSingleNodeTree = isSingleNodeTree }; } @@ -150,6 +153,6 @@ namespace Umbraco.Web.Models.Trees /// This is used in the UI to configure a full screen section/app /// [DataMember(Name = "containsTrees")] - public bool ContainsTrees => Children.Count > 0; + public bool ContainsTrees => Children.Count > 0 || !_isSingleNodeTree; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index f414702824..671a949a77 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1160,6 +1160,10 @@ namespace Umbraco.Web.PublishedCache.NuCache var pdatas = new List(); foreach (var pvalue in prop.Values) { + // sanitize - properties should be ok but ... never knows + if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) + continue; + // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' var value = published ? pvalue.PublishedValue : pvalue.EditedValue; if (value != null) @@ -1191,15 +1195,19 @@ namespace Umbraco.Web.PublishedCache.NuCache var cultureData = new Dictionary(); - var names = content is IContent document + // sanitize - names should be ok but ... never knows + if (content.GetContentType().VariesByCulture()) + { + var infos = content is IContent document ? (published ? document.PublishCultureInfos : document.CultureInfos) : content.CultureInfos; - foreach (var (culture, name) in names) - { - cultureData[culture] = new CultureVariation { Name = name.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + foreach (var (culture, info) in infos) + { + cultureData[culture] = new CultureVariation { Name = info.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + } } //the dictionary that will be serialized diff --git a/src/Umbraco.Web/Services/ApplicationTreeService.cs b/src/Umbraco.Web/Services/ApplicationTreeService.cs index f13c0547b6..86bfc5d0bb 100644 --- a/src/Umbraco.Web/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Web/Services/ApplicationTreeService.cs @@ -20,16 +20,18 @@ namespace Umbraco.Web.Services { private readonly ILogger _logger; private readonly CacheHelper _cache; + private readonly TypeLoader _typeLoader; private Lazy> _allAvailableTrees; internal const string TreeConfigFileName = "trees.config"; private static string _treeConfig; private static readonly object Locker = new object(); private readonly Lazy>> _groupedTrees; - public ApplicationTreeService(ILogger logger, CacheHelper cache) + public ApplicationTreeService(ILogger logger, CacheHelper cache, TypeLoader typeLoader) { _logger = logger; _cache = cache; + _typeLoader = typeLoader; _groupedTrees = new Lazy>>(InitGroupedTrees); } @@ -443,19 +445,18 @@ namespace Umbraco.Web.Services /// private class LazyEnumerableTrees : IEnumerable { - public LazyEnumerableTrees() + public LazyEnumerableTrees(TypeLoader typeLoader) { _lazyTrees = new Lazy>(() => { var added = new List(); // Load all Controller Trees by attribute - var types = Current.TypeLoader.GetTypesWithAttribute(); // fixme inject + var types = typeLoader.GetTypesWithAttribute(); // fixme inject //convert them to ApplicationTree instances var items = types - .Select(x => - new Tuple(x, x.GetCustomAttributes(false).Single())) - .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())) + .Select(x => (tree: x, treeAttribute: x.GetCustomAttributes(false).Single())) + .Select(x => new ApplicationTree(x.treeAttribute.Initialize, x.treeAttribute.SortOrder, x.treeAttribute.ApplicationAlias, x.treeAttribute.Alias, x.treeAttribute.Title, x.treeAttribute.IconClosed, x.treeAttribute.IconOpen, x.tree.GetFullNameWithAssembly())) .ToArray(); added.AddRange(items.Select(x => x.Alias)); @@ -465,7 +466,7 @@ namespace Umbraco.Web.Services } private readonly Lazy> _lazyTrees; - + /// /// Returns an enumerator that iterates through the collection. /// diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 273a7afb37..c1192b6909 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -41,7 +41,7 @@ namespace Umbraco.Web.Trees var groupedTrees = Services.ApplicationTreeService.GetGroupedApplicationTrees(application, onlyInitialized); var allTrees = groupedTrees.Values.SelectMany(x => x).ToList(); - if (string.IsNullOrEmpty(tree) == false || allTrees.Count <= 1) + if (string.IsNullOrEmpty(tree) == false || allTrees.Count == 1) { var apptree = !tree.IsNullOrWhiteSpace() ? allTrees.FirstOrDefault(x => x.Alias == tree) @@ -171,12 +171,15 @@ namespace Umbraco.Web.Trees throw new InvalidOperationException("Could not create root node for tree " + configTree.Alias); } + var treeAttribute = configTree.GetTreeAttribute(); + var sectionRoot = TreeRootNode.CreateSingleTreeRoot( rootId, rootNode.Result.ChildNodesUrl, rootNode.Result.MenuUrl, rootNode.Result.Name, - byControllerAttempt.Result); + byControllerAttempt.Result, + treeAttribute.IsSingleNodeTree); //assign the route path based on the root node, this means it will route there when the section is navigated to //and no dashboards will be available for this section diff --git a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs index 171601a338..c688491ebb 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs @@ -48,28 +48,6 @@ namespace Umbraco.Web.Trees return tree.GetRuntimeType().GetTreeAttribute(); } - internal static string GetRootNodeDisplayName(this TreeAttribute attribute, ILocalizedTextService textService) - { - var label = $"[{attribute.Alias}]"; - - // try to look up a the localized tree header matching the tree alias - var localizedLabel = textService.Localize("treeHeaders/" + attribute.Alias); - - // if the localizedLabel returns [alias] then return the title attribute from the trees.config file, if it's defined - if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) - { - if (string.IsNullOrEmpty(attribute.Title) == false) - label = attribute.Title; - } - else - { - // the localizedLabel translated into something that's not just [alias], so use the translation - label = localizedLabel; - } - - return label; - } - internal static Attempt TryGetControllerTree(this ApplicationTree appTree) { //get reference to all TreeApiControllers diff --git a/src/Umbraco.Web/Trees/DictionaryTreeController.cs b/src/Umbraco.Web/Trees/DictionaryTreeController.cs index ca9a54f873..3043377d65 100644 --- a/src/Umbraco.Web/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web/Trees/DictionaryTreeController.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.Trees [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] [Mvc.PluginController("UmbracoTrees")] [CoreTree(TreeGroup = Constants.Trees.Groups.Settings)] - [Tree(Constants.Applications.Translation, Constants.Trees.Dictionary, null, sortOrder: 0)] + [Tree(Constants.Applications.Translation, Constants.Trees.Dictionary, null)] public class DictionaryTreeController : TreeController { protected override TreeNode CreateRootNode(FormDataCollection queryStrings) diff --git a/src/Umbraco.Web/Trees/TreeAttribute.cs b/src/Umbraco.Web/Trees/TreeAttribute.cs index 5df0275298..b214698721 100644 --- a/src/Umbraco.Web/Trees/TreeAttribute.cs +++ b/src/Umbraco.Web/Trees/TreeAttribute.cs @@ -28,13 +28,15 @@ namespace Umbraco.Web.Trees /// The icon open. /// if set to true [initialize]. /// The sort order. + /// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app) public TreeAttribute(string appAlias, string alias, string title, string iconClosed = "icon-folder", string iconOpen = "icon-folder-open", bool initialize = true, - int sortOrder = 0) + int sortOrder = 0, + bool isSingleNodeTree = false) { ApplicationAlias = appAlias; Alias = alias; @@ -43,6 +45,7 @@ namespace Umbraco.Web.Trees IconOpen = iconOpen; Initialize = initialize; SortOrder = sortOrder; + IsSingleNodeTree = isSingleNodeTree; } @@ -54,5 +57,10 @@ namespace Umbraco.Web.Trees public string IconOpen { get; private set; } public bool Initialize { get; private set; } public int SortOrder { get; private set; } + + /// + /// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app) + /// + public bool IsSingleNodeTree { get; private set; } } } diff --git a/src/Umbraco.Web/Trees/TreeController.cs b/src/Umbraco.Web/Trees/TreeController.cs index b53ad8a057..b5708ff57d 100644 --- a/src/Umbraco.Web/Trees/TreeController.cs +++ b/src/Umbraco.Web/Trees/TreeController.cs @@ -10,6 +10,7 @@ namespace Umbraco.Web.Trees public abstract class TreeController : TreeControllerBase { private TreeAttribute _attribute; + private string _rootNodeDisplayName; protected TreeController() { @@ -20,9 +21,9 @@ namespace Umbraco.Web.Trees /// The name to display on the root node /// public override string RootNodeDisplayName - { - get { return _attribute.GetRootNodeDisplayName(Services.TextService); } - } + => _rootNodeDisplayName + ?? (_rootNodeDisplayName = Services.ApplicationTreeService.GetByAlias(_attribute.Alias) + ?.GetRootNodeDisplayName(Services.TextService)); /// /// Gets the current tree alias from the attribute assigned to it. diff --git a/src/Umbraco.Web/Trees/UserTreeController.cs b/src/Umbraco.Web/Trees/UserTreeController.cs index e6bd53ddf8..8ae5b002c6 100644 --- a/src/Umbraco.Web/Trees/UserTreeController.cs +++ b/src/Umbraco.Web/Trees/UserTreeController.cs @@ -10,7 +10,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Users)] - [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 0)] + [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 0, isSingleNodeTree: true)] [PluginController("UmbracoTrees")] [CoreTree] public class UserTreeController : TreeController diff --git a/src/Umbraco.Web/_Legacy/Actions/Action.cs b/src/Umbraco.Web/_Legacy/Actions/Action.cs index 388a5735fd..241218ddb7 100644 --- a/src/Umbraco.Web/_Legacy/Actions/Action.cs +++ b/src/Umbraco.Web/_Legacy/Actions/Action.cs @@ -174,6 +174,7 @@ namespace Umbraco.Web._Legacy.Actions public string Alias { get; set; } public string JsFunctionName { get; set; } public string JsSource { get; set; } + public bool OpensDialog { get; set; } public PlaceboAction() { } public PlaceboAction(IAction legacyAction) @@ -185,6 +186,7 @@ namespace Umbraco.Web._Legacy.Actions Alias = legacyAction.Alias; JsFunctionName = legacyAction.JsFunctionName; JsSource = legacyAction.JsSource; + OpensDialog = legacyAction.OpensDialog; } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs b/src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs index 37de1f8e0f..c313f282ad 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs @@ -69,6 +69,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs b/src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs index 1425b27917..20dc331516 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs @@ -60,6 +60,8 @@ namespace Umbraco.Web._Legacy.Actions get { return ""; } } + public bool OpensDialog => false; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs b/src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs index 9c31c172ab..b68627c38c 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs @@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs b/src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs index a489f1d280..5addcec99f 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs @@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionCreateBlueprintFromContent.cs b/src/Umbraco.Web/_Legacy/Actions/ActionCreateBlueprintFromContent.cs index e00de39aea..0d028c35b4 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionCreateBlueprintFromContent.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionCreateBlueprintFromContent.cs @@ -20,6 +20,7 @@ namespace Umbraco.Web._Legacy.Actions public string Alias { get; private set; } public string JsFunctionName { get; private set; } public string JsSource { get; private set; } + public bool OpensDialog => true; public ActionCreateBlueprintFromContent() { diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs b/src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs index 09ce4d8602..53f7822d47 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs @@ -77,6 +77,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs b/src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs index 7f8dd6b03c..f0da5323b9 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs @@ -73,6 +73,8 @@ namespace Umbraco.Web._Legacy.Actions } } + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionExport.cs b/src/Umbraco.Web/_Legacy/Actions/ActionExport.cs index df78026ea0..56b98c02f2 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionExport.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionExport.cs @@ -71,6 +71,8 @@ namespace Umbraco.Web._Legacy.Actions } } + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionImport.cs b/src/Umbraco.Web/_Legacy/Actions/ActionImport.cs index 42947cf36e..52f163ee6b 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionImport.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionImport.cs @@ -72,6 +72,8 @@ } } + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionMove.cs b/src/Umbraco.Web/_Legacy/Actions/ActionMove.cs index 80aff5736a..81d1803679 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionMove.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionMove.cs @@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionNew.cs b/src/Umbraco.Web/_Legacy/Actions/ActionNew.cs index 72e863e38b..ef1b61efc5 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionNew.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionNew.cs @@ -70,6 +70,8 @@ namespace Umbraco.Web._Legacy.Actions } } + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs b/src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs index ef281eecbe..fd6bc3d61a 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs @@ -76,6 +76,9 @@ namespace Umbraco.Web._Legacy.Actions return false; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionNull.cs b/src/Umbraco.Web/_Legacy/Actions/ActionNull.cs index 78c5175fb6..3344560c3f 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionNull.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionNull.cs @@ -51,6 +51,8 @@ get { return string.Empty; } } + public bool OpensDialog => false; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs b/src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs index 832e691b48..fa17b87073 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs @@ -75,6 +75,8 @@ namespace Umbraco.Web._Legacy.Actions } } + public bool OpensDialog => false; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs b/src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs index f0ccb03d8e..fdec43e810 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs @@ -75,6 +75,8 @@ namespace Umbraco.Web._Legacy.Actions } } + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs b/src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs index 357dfe89a4..65e9d7128e 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs @@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs index 6b54873c43..70c7735572 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs @@ -77,6 +77,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs index b78af779e4..312ae80825 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs @@ -74,6 +74,9 @@ namespace Umbraco.Web._Legacy.Actions return false; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs index 07133b4030..0abf4fcac5 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs @@ -81,6 +81,9 @@ namespace Umbraco.Web._Legacy.Actions return false; } } + + public bool OpensDialog => false; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs index da70eb1409..2a2baac070 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs @@ -27,6 +27,8 @@ public bool CanBePermissionAssigned => false; + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRights.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRights.cs index e1ee74e61c..beb3b06ddf 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionRights.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionRights.cs @@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs b/src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs index 59044666f7..3179dc9fb5 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs @@ -82,6 +82,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionSort.cs b/src/Umbraco.Web/_Legacy/Actions/ActionSort.cs index b813dcbc8c..48f6b8d1e9 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionSort.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionSort.cs @@ -83,6 +83,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs index ff471bc198..a04a24f4a3 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs @@ -78,6 +78,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => false; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs b/src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs index 0cc5120fd0..157fd827a6 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs @@ -77,6 +77,8 @@ namespace Umbraco.Web._Legacy.Actions } } + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs index 93d1da2046..11225add1d 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs @@ -1,13 +1,14 @@ using System; +using Umbraco.Core; +using Umbraco.Core.CodeAnnotations; namespace Umbraco.Web._Legacy.Actions { - - /// /// This action is invoked when a document is being unpublished /// + [ActionMetadata(Constants.Conventions.PermissionCategories.ContentCategory)] public class ActionUnpublish : IAction { //create singleton @@ -15,68 +16,17 @@ namespace Umbraco.Web._Legacy.Actions private static readonly ActionUnpublish m_instance = new ActionUnpublish(); #pragma warning restore 612,618 - public static ActionUnpublish Instance - { - get { return m_instance; } - } + public static ActionUnpublish Instance => m_instance; - #region IAction Members + public char Letter => 'Z'; + public string JsFunctionName => ""; + public string JsSource => null; + public string Alias => "unpublish"; + public string Icon => "circle-dotted"; + public bool ShowInNotifier => false; + public bool CanBePermissionAssigned => true; + public bool OpensDialog => false; - public char Letter - { - get - { - return 'Z'; - } - } - - public string JsFunctionName - { - get - { - return ""; - } - } - - public string JsSource - { - get - { - return null; - } - } - - public string Alias - { - get - { - return "unpublish"; - } - } - - public string Icon - { - get - { - return "circle-dotted"; - } - } - - public bool ShowInNotifier - { - get - { - return false; - } - } - public bool CanBePermissionAssigned - { - get - { - return false; - } - } - #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs b/src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs index 15458e83ad..5621d505a9 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs @@ -77,6 +77,9 @@ namespace Umbraco.Web._Legacy.Actions return true; } } + + public bool OpensDialog => false; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs b/src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs index 2c66932a04..45a1d0e1c5 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs @@ -46,6 +46,8 @@ get { return false; } } + public bool OpensDialog => false; + #endregion } } diff --git a/src/Umbraco.Web/_Legacy/Actions/IAction.cs b/src/Umbraco.Web/_Legacy/Actions/IAction.cs index 410a407517..48a752e7da 100644 --- a/src/Umbraco.Web/_Legacy/Actions/IAction.cs +++ b/src/Umbraco.Web/_Legacy/Actions/IAction.cs @@ -14,5 +14,9 @@ namespace Umbraco.Web._Legacy.Actions /// A path to a supporting JavaScript file for the IAction. A script tag will be rendered out with the reference to the JavaScript file. /// string JsSource { get; } + /// + /// Whether or not the action opens a dialog when invoked + /// + bool OpensDialog { get; } } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs index cf39b17e55..5526a3d9a3 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs @@ -78,6 +78,8 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu get { return "javascript:actionDeleteRelationType(UmbClientMgr.mainTree().getActionNode().nodeId,UmbClientMgr.mainTree().getActionNode().nodeName);"; } } + public bool OpensDialog => true; + #endregion } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs index 6018539983..cb776d5246 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs @@ -78,6 +78,8 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu get { return "javascript:actionNewRelationType();"; } } + public bool OpensDialog => true; + #endregion } }