From ba913db60eb42cd7f7aaa464def903e63ba53b0d Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 22 Oct 2018 17:19:14 +0200 Subject: [PATCH 01/31] Fix to/from (in)variant changes --- .../Implement/ContentTypeRepositoryBase.cs | 294 ++++++++++-------- .../NuCache/PublishedSnapshotService.cs | 16 +- 2 files changed, 171 insertions(+), 139 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3f1ea3116e..adf02a52f3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -270,8 +270,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? @@ -406,40 +406,33 @@ 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 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: @@ -448,15 +441,36 @@ 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; + + // 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) @@ -467,31 +481,12 @@ AND umbracoNode.id <> @id", typeId = propertyType.Id; // not an orphan anymore - if (orphanPropertyTypeIds != null) - orphanPropertyTypeIds.Remove(typeId); - } - - //check if any property types were changing variation - if (propertyTypeVariationChanges.Count > 0) - { - var changes = new Dictionary(); - - //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); + orphanPropertyTypeIds?.Remove(typeId); } + // if some property types have actually changed, move their variant data + if (propertyTypeVariationChanges != null) + MovePropertyTypeVariantData(propertyTypeVariationChanges); // deal with orphan properties: those that were in a deleted tab, // and have not been re-mapped to another tab or to 'generic properties' @@ -500,6 +495,45 @@ AND umbracoNode.id <> @id", DeletePropertyType(entity.Id, id); } + // 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 /// @@ -526,28 +560,38 @@ 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) + { + var defaultLanguageId = GetDefaultLanguageId(); //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); break; case ContentVariation.Nothing: - MovePropertyDataToVariantNothing(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(defaultLanguageId, null, propertyTypeIds); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -558,24 +602,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 @@ -585,10 +622,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 @@ -596,10 +634,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 @@ -607,32 +646,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 @@ -646,73 +684,59 @@ 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. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds) { - //first clear out any existing property data that might already exists under the default lang + //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(); + + // 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); 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(); + + // 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); 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() + .Where(x => x.LanguageId == null) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - Database.Execute(sqlDelete); - - //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); - - Database.Execute(sqlInsert); + Database.Execute(sqlDelete); + } } private void DeletePropertyType(int contentTypeId, int propertyTypeId) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a9903669b9..55f4f07bef 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 names = content is IContent document ? (published ? document.PublishNames : document.CultureNames) : content.CultureNames; - foreach (var (culture, name) in names) - { - cultureData[culture] = new CultureVariation { Name = name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + foreach (var (culture, name) in names) + { + cultureData[culture] = new CultureVariation { Name = name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + } } //the dictionary that will be serialized From d23f5f8124a291b894b8ccba14c1a724299bac9d Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Wed, 24 Oct 2018 15:31:45 +0200 Subject: [PATCH 02/31] #3419 remove check on number of trees so dictoinary tree shows in the translation section and doesn't return a error when it's moved to the settings section --- src/Umbraco.Web/Trees/ApplicationTreeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 273a7afb37..305116a443 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) { var apptree = !tree.IsNullOrWhiteSpace() ? allTrees.FirstOrDefault(x => x.Alias == tree) From 2af2a9b77fd9d74d50dba598cd076edccf59920b Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Wed, 24 Oct 2018 15:39:45 +0200 Subject: [PATCH 03/31] #3419 don't show the back button in the edit screen of dictionary...doesn't make sense to go to settings section when the tree is in the translation section --- .../src/views/dictionary/dictionary.edit.controller.js | 10 ++-------- .../src/views/dictionary/edit.html | 3 +-- 2 files changed, 3 insertions(+), 10 deletions(-) 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..1a68afe47c 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 @@ -19,10 +19,8 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes vm.page.menu.currentSection = appState.getSectionState("currentSection"); vm.page.menu.currentNode = null; vm.description = ""; - vm.showBackButton = true; - + vm.save = saveDictionary; - vm.back = back; function loadDictionary() { @@ -102,11 +100,7 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes }); } } - - function back() { - $location.path("settings/dictionary/list"); - } - + $scope.$watch("vm.content.name", function (newVal, oldVal) { //when the value changes, we need to set the name dirty if (newVal && (newVal !== oldVal) && typeof(oldVal) !== "undefined") { diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html index 41320108e7..b61175f8c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html @@ -12,8 +12,7 @@ hide-icon="true" hide-description="true" hide-alias="true" - on-back="vm.back()" - show-back-button="vm.showBackButton"> + > From 96c0b701a010aef2760171740414617e81c5edc2 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Wed, 24 Oct 2018 18:27:00 +0200 Subject: [PATCH 04/31] #3419 added always show root item to tree attribute --- src/Umbraco.Web/Trees/DictionaryTreeController.cs | 2 +- src/Umbraco.Web/Trees/TreeAttribute.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Trees/DictionaryTreeController.cs b/src/Umbraco.Web/Trees/DictionaryTreeController.cs index ca9a54f873..4362dbbd3c 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, alwaysShowRootItem: true)] 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..cac1a87bc5 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. + /// Always show the root item 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 alwaysShowRootItem = false) { ApplicationAlias = appAlias; Alias = alias; @@ -43,6 +45,7 @@ namespace Umbraco.Web.Trees IconOpen = iconOpen; Initialize = initialize; SortOrder = sortOrder; + AlwaysShowRootItem = alwaysShowRootItem; } @@ -54,5 +57,7 @@ namespace Umbraco.Web.Trees public string IconOpen { get; private set; } public bool Initialize { get; private set; } public int SortOrder { get; private set; } + + public bool AlwaysShowRootItem { get; private set; } } } From d5cc658811132c6f89a82722a22721efd4c674d6 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Wed, 24 Oct 2018 18:28:52 +0200 Subject: [PATCH 05/31] #3419 added check on tree items count back --- src/Umbraco.Web/Trees/ApplicationTreeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 305116a443..273a7afb37 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) + if (string.IsNullOrEmpty(tree) == false || allTrees.Count <= 1) { var apptree = !tree.IsNullOrWhiteSpace() ? allTrees.FirstOrDefault(x => x.Alias == tree) From 56b9d36f5325415a89def23135259bff94cdf5e2 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 24 Oct 2018 18:44:37 +0200 Subject: [PATCH 06/31] Add content type variant changes tests --- .../ContentTypeServiceVariantsTests.cs | 773 ++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 2 files changed, 774 insertions(+) create mode 100644 src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs 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/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 @@ + From 638f8b57b34bdbca2acb39559a36bacd1bd3989c Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 24 Oct 2018 18:46:51 +0200 Subject: [PATCH 07/31] Fix content type variant changes --- .../ContentVariationExtensions.cs | 2 +- .../Models/ContentTypeBaseExtensions.cs | 15 -- .../Models/ContentTypeCompositionBase.cs | 21 +++ .../IContentTypeRepositoryBase.cs | 7 - .../Implement/ContentTypeRepository.cs | 1 - .../Implement/ContentTypeRepositoryBase.cs | 150 ++++++++++++++---- .../Services/ContentTypeServiceExtensions.cs | 11 +- ...peServiceBaseOfTRepositoryTItemTService.cs | 39 ++--- .../Services/ContentTypeServiceTests.cs | 7 +- .../Editors/ContentTypeControllerBase.cs | 78 +++++---- 10 files changed, 205 insertions(+), 126 deletions(-) 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/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 838a75b98b..2f455083c0 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 adf02a52f3..8da92991a4 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 @@ -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) { @@ -420,6 +418,7 @@ AND umbracoNode.id <> @id", // collect property types that have a dirty variation List propertyTypeVariationDirty = null; + // note: this only deals with *local* property types, we're dealing w/compositions later below foreach (var propertyType in entity.PropertyTypes) { if (contentTypeVariationChanging) @@ -458,6 +457,35 @@ AND umbracoNode.id <> @id", ? 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) @@ -484,9 +512,19 @@ AND umbracoNode.id <> @id", orphanPropertyTypeIds?.Remove(typeId); } + // 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(); + + var impacted = GetImpactedContentTypes(entity, all); + // if some property types have actually changed, move their variant data if (propertyTypeVariationChanges != null) - MovePropertyTypeVariantData(propertyTypeVariationChanges); + 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' @@ -495,6 +533,38 @@ 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) @@ -575,9 +645,10 @@ AND umbracoNode.id <> @id", /// /// Moves variant data for property type variation changes. /// - private void MovePropertyTypeVariantData(IDictionary propertyTypeChanges) + 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 grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) @@ -588,10 +659,10 @@ AND umbracoNode.id <> @id", switch (toVariation) { case ContentVariation.Culture: - CopyPropertyData(null, defaultLanguageId, propertyTypeIds); + CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); break; case ContentVariation.Nothing: - CopyPropertyData(defaultLanguageId, null, propertyTypeIds); + CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -689,12 +760,36 @@ AND umbracoNode.id <> @id", /// The source language (can be null ie invariant). /// The target language (can be null ie invariant) /// The property type identifiers. - private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds) + /// The content type identifiers. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) { + // 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(); + // 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); @@ -704,6 +799,11 @@ AND umbracoNode.id <> @id", 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 target language that exists under the source language @@ -713,6 +813,11 @@ AND umbracoNode.id <> @id", .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); @@ -722,6 +827,10 @@ AND umbracoNode.id <> @id", 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); @@ -731,7 +840,12 @@ AND umbracoNode.id <> @id", if (sourceLanguageId == null) { sqlDelete = Sql() - .Delete() + .Delete(); + + if (contentTypeIds != null) + sqlDelete.WhereIn(x => x.VersionId, inSql); + + sqlDelete .Where(x => x.LanguageId == null) .WhereIn(x => x.PropertyTypeId, propertyTypeIds); @@ -872,24 +986,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/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/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index a114f415cc..9a29176860 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/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.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; From 25a90ffbf5bf0fb064459c941ae136c2c74ba041 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 25 Oct 2018 08:54:17 +0200 Subject: [PATCH 08/31] #3419 added extra parameter to createSingleRoot node method so it will always show a single tree. If a single tree doesn't have children it does not show --- src/Umbraco.Web/Models/Trees/SectionRootNode.cs | 9 ++++++--- src/Umbraco.Web/Trees/ApplicationTreeController.cs | 7 ++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Models/Trees/SectionRootNode.cs b/src/Umbraco.Web/Models/Trees/SectionRootNode.cs index cd4fc3e483..229c363fd2 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 _alwaysShow; /// /// 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 alwaysShowRootNode = false) { return new TreeRootNode(nodeId, getChildNodesUrl, menuUrl) { Children = children, - Name = title + Name = title, + _alwaysShow = alwaysShowRootNode }; } @@ -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 || _alwaysShow; } } diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 273a7afb37..98db16f0e9 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -171,12 +171,17 @@ 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.AlwaysShowRootItem); + + //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 From fe0176f97cfe0f8ab97a535e6eac5ca757bafd11 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 25 Oct 2018 08:59:13 +0200 Subject: [PATCH 09/31] Fixed unit test --- src/Umbraco.Tests/UI/LegacyDialogTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From 3a0981e02bf0e8d26646ece0d7b423703c4c7f52 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 25 Oct 2018 09:06:53 +0200 Subject: [PATCH 10/31] #3419 fix error in backend for sections with no tree (forms when not installed) --- src/Umbraco.Web/Trees/ApplicationTreeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 98db16f0e9..37659d7d66 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) From 4649fb2505582fb52bb2c97896dc735d9eb72b4e Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 25 Oct 2018 10:51:46 +0200 Subject: [PATCH 11/31] Fix merge --- src/Umbraco.Core/Services/Implement/ContentService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 2cd9051e72..200c5af096 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1994,7 +1994,7 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents && published.Any()) scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); + Audit(AuditType.Sort, userId, 0, "Sorting content performed by user"); return true; } From 96a102decfe3fe83a95161db0208f68b57a3387e Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 25 Oct 2018 12:13:54 +0200 Subject: [PATCH 12/31] #3433 fix build error --- src/Umbraco.Core/Services/Implement/ContentService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 2cd9051e72..200c5af096 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1994,7 +1994,7 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents && published.Any()) scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); + Audit(AuditType.Sort, userId, 0, "Sorting content performed by user"); return true; } From 5199e8e901619bb4045f1a157c3af8af3772cd0e Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 25 Oct 2018 12:23:34 +0200 Subject: [PATCH 13/31] #3433 replace checkbox with for setting culture variants with toggle box --- .../views/permissions/permissions.controller.js | 14 ++++++++++++-- .../views/permissions/permissions.html | 10 +++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) 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..956b0d32ed 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 @@ -13,7 +13,7 @@ @@ -48,8 +48,12 @@ Define the rules for how this content type's properties can be varied
-
From 3b2e473a9ebc98285494538bd7fa8b85363c9245 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 25 Oct 2018 12:38:43 +0200 Subject: [PATCH 14/31] #3433 changed layout so it looks better --- .../views/permissions/permissions.html | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) 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 956b0d32ed..79e6c6c3c6 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,24 +27,29 @@
- +
+
+ +
Content Type Variation
+ Define the rules for how this content type's properties can be varied + +
+
-
Content Type Variation
- Define the rules for how this content type's properties can be varied +
- +
- + From 3c217adc2e60fafd2103a55d62420db9eba70931 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 25 Oct 2018 12:49:32 +0200 Subject: [PATCH 15/31] #3433 added language keys for text of culture variant permissions --- .../views/documenttypes/views/permissions/permissions.html | 6 +++--- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 3 +++ src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) 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 79e6c6c3c6..cf7f960cea 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 @@ -41,15 +41,15 @@
-
Content Type Variation
- Define the rules for how this content type's properties can be varied +
+
- +
tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: + Content Type Variation + Define the rules for how this content type's properties can be varied + 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 54d86eee5c..b38263d06a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1505,6 +1505,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: + Content Type Variation + Define the rules for how this content type's properties can be varied + Allow varying by Culture Add language From 2fbe2bb3dad23ac6943889cd4ebb139c53722284 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 25 Oct 2018 12:54:17 +0200 Subject: [PATCH 16/31] #3433 added short cut for toglle allow culture variants to short cuts overview --- .../src/views/documenttypes/edit.controller.js | 8 +++++++- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) 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/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 9eab99a75a..b9b3d0cfed 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -680,6 +680,7 @@ Move Lines Down General Editor + Toggle allow culture variants Background colour 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 b38263d06a..0265996bbf 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -700,6 +700,7 @@ Move Lines Down General Editor + Toggle allow culture variants Background color From 93aa9dfe93ccbced50aa208b0e1b557e5d4aceb6 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Thu, 25 Oct 2018 14:28:12 +0200 Subject: [PATCH 17/31] Fix sort audit parameters call --- src/Umbraco.Core/Services/Implement/ContentService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 2cd9051e72..200c5af096 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1994,7 +1994,7 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents && published.Any()) scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); + Audit(AuditType.Sort, userId, 0, "Sorting content performed by user"); return true; } From 4cfac8da16ce5e68334f300090563e2bc0739dbd Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 25 Oct 2018 14:54:36 +0200 Subject: [PATCH 18/31] Make it possible to add ellipsis on actions that invokes dialogs by means of OpensDialog on IAction --- src/Umbraco.Tests/Composing/ActionCollectionTests.cs | 4 ++++ .../src/less/components/tree/umb-actions.less | 9 +++++++++ .../views/components/application/umb-contextmenu.html | 4 ++-- src/Umbraco.Web/Models/Trees/MenuItem.cs | 5 +++++ src/Umbraco.Web/_Legacy/Actions/Action.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionAssignDomain.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionBrowse.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionChangeDocType.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionCopy.cs | 3 +++ .../_Legacy/Actions/ActionCreateBlueprintFromContent.cs | 1 + src/Umbraco.Web/_Legacy/Actions/ActionDelete.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionEmptyTranscan.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionExport.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionImport.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionMove.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionNew.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionNotify.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionNull.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionPackage.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionPackageCreate.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionProtect.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionPublish.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionRePublish.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionRefresh.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionRestore.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionRights.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionRollback.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionSort.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionToPublish.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionTranslate.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ActionUpdate.cs | 3 +++ src/Umbraco.Web/_Legacy/Actions/ContextMenuSeperator.cs | 2 ++ src/Umbraco.Web/_Legacy/Actions/IAction.cs | 4 ++++ .../RelationTypes/TreeMenu/ActionDeleteRelationType.cs | 2 ++ .../RelationTypes/TreeMenu/ActionNewRelationType.cs | 2 ++ 36 files changed, 101 insertions(+), 2 deletions(-) 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.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/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/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/_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..cb197342f1 100644 --- a/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs +++ b/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs @@ -76,6 +76,9 @@ namespace Umbraco.Web._Legacy.Actions return false; } } + + public bool OpensDialog => 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 } } From 20b9bb1032bf0bbe29e8f45a89180c8a13713a5c Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 25 Oct 2018 21:55:44 +0100 Subject: [PATCH 19/31] Gets rid of a couple of VS SLN Warnings - due to dupe'd using's decared (done a tidy up) --- src/Umbraco.Core/Security/AuthenticationExtensions.cs | 5 ----- 1 file changed, 5 deletions(-) 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 { From 972dc56d9aec81d0bf10a77d6f3146d505508e2c Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 25 Oct 2018 21:58:39 +0100 Subject: [PATCH 20/31] More dupe using's declared & tidied up - total number of warnings now at 48 --- src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 +} From 555a275e135c06eeb2c92e4bfd2bee216874c86b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Oct 2018 13:21:30 +1100 Subject: [PATCH 21/31] fixes build --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index de52021220..3dc5cd053a 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -348,8 +348,6 @@ - - Designer From 3d29c0381e5a42094fada6026480f24e6c30773b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Oct 2018 14:18:42 +1100 Subject: [PATCH 22/31] small refactor of trees for single page apps sections and some cleanup, puts the back button for dictionary back in with the correct route --- src/Umbraco.Core/Models/ApplicationTree.cs | 29 +++++++++++++++++++ src/Umbraco.Tests/TestHelpers/TestObjects.cs | 3 +- .../dictionary/dictionary.edit.controller.js | 6 ++++ .../src/views/dictionary/edit.html | 3 +- src/Umbraco.Web/Editors/EntityController.cs | 7 ++--- .../Models/Trees/SectionRootNode.cs | 10 +++---- .../Services/ApplicationTreeService.cs | 15 +++++----- .../Trees/ApplicationTreeController.cs | 4 +-- .../Trees/ApplicationTreeExtensions.cs | 22 -------------- .../Trees/DictionaryTreeController.cs | 2 +- src/Umbraco.Web/Trees/TreeAttribute.cs | 11 ++++--- src/Umbraco.Web/Trees/TreeController.cs | 7 +++-- src/Umbraco.Web/Trees/UserTreeController.cs | 2 +- 13 files changed, 68 insertions(+), 53 deletions(-) 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.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 2707c73607..6d58a49f04 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -108,6 +108,7 @@ namespace Umbraco.Tests.TestHelpers IGlobalSettings globalSettings, IEventMessagesFactory eventMessagesFactory, IEnumerable urlSegmentProviders, + TypeLoader typeLoader, IServiceFactory container = null) { if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); @@ -181,7 +182,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.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js index 1a68afe47c..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 @@ -19,8 +19,10 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes vm.page.menu.currentSection = appState.getSectionState("currentSection"); vm.page.menu.currentNode = null; vm.description = ""; + vm.showBackButton = true; vm.save = saveDictionary; + vm.back = back; function loadDictionary() { @@ -101,6 +103,10 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes } } + function back() { + $location.path(vm.page.menu.currentSection + "/dictionary/list"); + } + $scope.$watch("vm.content.name", function (newVal, oldVal) { //when the value changes, we need to set the name dirty if (newVal && (newVal !== oldVal) && typeof(oldVal) !== "undefined") { diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html index b61175f8c9..41320108e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html @@ -12,7 +12,8 @@ hide-icon="true" hide-description="true" hide-alias="true" - > + on-back="vm.back()" + show-back-button="vm.showBackButton"> 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/SectionRootNode.cs b/src/Umbraco.Web/Models/Trees/SectionRootNode.cs index 229c363fd2..730f6e2962 100644 --- a/src/Umbraco.Web/Models/Trees/SectionRootNode.cs +++ b/src/Umbraco.Web/Models/Trees/SectionRootNode.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Models.Trees { private static readonly string RootId = Core.Constants.System.Root.ToString(CultureInfo.InvariantCulture); private bool _isGroup; - private bool _alwaysShow; + private bool _isSingleNodeTree; /// /// Creates a group node for grouped multiple trees @@ -88,15 +88,15 @@ namespace Umbraco.Web.Models.Trees /// /// /// - /// + /// /// - public static TreeRootNode CreateSingleTreeRoot(string nodeId, string getChildNodesUrl, string menuUrl, string title, TreeNodeCollection children, bool alwaysShowRootNode = false) + 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, - _alwaysShow = alwaysShowRootNode + _isSingleNodeTree = isSingleNodeTree }; } @@ -153,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 || _alwaysShow; + public bool ContainsTrees => Children.Count > 0 || !_isSingleNodeTree; } } 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 37659d7d66..c1192b6909 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -179,9 +179,7 @@ namespace Umbraco.Web.Trees rootNode.Result.MenuUrl, rootNode.Result.Name, byControllerAttempt.Result, - treeAttribute.AlwaysShowRootItem); - - + 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 4362dbbd3c..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, alwaysShowRootItem: true)] + [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 cac1a87bc5..b214698721 100644 --- a/src/Umbraco.Web/Trees/TreeAttribute.cs +++ b/src/Umbraco.Web/Trees/TreeAttribute.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Trees /// The icon open. /// if set to true [initialize]. /// The sort order. - /// Always show the root item + /// 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, @@ -36,7 +36,7 @@ namespace Umbraco.Web.Trees string iconOpen = "icon-folder-open", bool initialize = true, int sortOrder = 0, - bool alwaysShowRootItem = false) + bool isSingleNodeTree = false) { ApplicationAlias = appAlias; Alias = alias; @@ -45,7 +45,7 @@ namespace Umbraco.Web.Trees IconOpen = iconOpen; Initialize = initialize; SortOrder = sortOrder; - AlwaysShowRootItem = alwaysShowRootItem; + IsSingleNodeTree = isSingleNodeTree; } @@ -58,6 +58,9 @@ namespace Umbraco.Web.Trees public bool Initialize { get; private set; } public int SortOrder { get; private set; } - public bool AlwaysShowRootItem { 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 From e280226abccda84b9cebb8cae15802faa7795f29 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Oct 2018 14:41:49 +1100 Subject: [PATCH 23/31] fix issue with content picker always setting a dirty state --- .../contentpicker/contentpicker.controller.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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(); From 02221f6dd0c36505b66a8fc3bbfc2fdea2f51bb4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Oct 2018 15:07:21 +1100 Subject: [PATCH 24/31] Fixes how the content app view model works so dirty tracking continues to work properly, previously we tried to omit the apps so we didn' thave a circualar model reference but that doesn't work because that clones the object, so now the view model is just the variant index which works much better.Fixes other js probs --- .../umbvariantcontenteditors.directive.js | 4 +- .../treepicker/treepicker.controller.js | 2 +- .../treepicker/treepicker.controller.js | 2 +- .../apps/content/content.controller.js | 42 +++++++++++-------- .../views/content/overlays/save.controller.js | 6 +-- .../src/views/media/edit.html | 2 +- 6 files changed, 31 insertions(+), 27 deletions(-) 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/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/content/apps/content/content.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js index 7d3f10fab4..2bde680bf4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js @@ -3,33 +3,39 @@ function ContentAppContentController($scope, $timeout, serverValidationManager) { + //the contentApp's viewModel is actually the index of the variant being edited, not the variant itself. + //if we make the viewModel the variant itself, we end up with a circular reference in the models which isn't ideal + // (i.e. variant.apps[contentApp].viewModel = variant) + //so instead since we already have access to the content, we can just get the variant directly by the index. + var vm = this; vm.loading = true; function onInit() { - vm.content = $scope.model.viewModel; + //get the variant by index (see notes above) + vm.content = $scope.content.variants[$scope.model.viewModel]; serverValidationManager.notify(); vm.loading = false; + + //if this variant has a culture/language assigned, then we need to watch it since it will change + //if the language drop down changes and we need to re-init + if (vm.content.language) { + $scope.$watch(function () { + return vm.content.language.culture; + }, function (newVal, oldVal) { + if (newVal !== oldVal) { + vm.loading = true; + + //TODO: Can we minimize the flicker? + $timeout(function () { + onInit(); + }, 100); + } + }); + } } onInit(); - - //if this variant has a culture/language assigned, then we need to watch it since it will change - //if the language drop down changes and we need to re-init - if ($scope.model.viewModel.language) { - $scope.$watch(function () { - return $scope.model.viewModel.language.culture; - }, function (newVal, oldVal) { - if (newVal !== oldVal) { - vm.loading = true; - - //TODO: Can we minimize the flicker? - $timeout(function () { - onInit(); - }, 100); - } - }); - } } angular.module("umbraco").controller("Umbraco.Editors.Content.Apps.ContentController", ContentAppContentController); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js index 4a9e7d2dca..a99da13811 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js @@ -22,12 +22,8 @@ //determine a variant is 'dirty' (meaning it will show up as save-able) if it's // * the active one // * it's editor is in a $dirty state - // * it's umbContent app viewModel (if any) is in a $dirty state // * it is in NotCreated state - var contentApp = _.find(variant.apps, function(app) { - return app.alias === "umbContent"; - }); - return (variant.active || variant.isDirty || (contentApp && contentApp.viewModel && contentApp.viewModel.isDirty)); + return (variant.active || variant.isDirty); } function pristineVariantFilter(variant) { 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 @@
- +
From 83f73c085f434b857d482be2e6c20272370042a6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Oct 2018 16:40:28 +1100 Subject: [PATCH 25/31] Fixes unpublish, allows to be permission assigned and checks this permission when trying to perform that action --- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 1 + src/Umbraco.Web/Editors/ContentController.cs | 2 +- .../_Legacy/Actions/ActionUnPublish.cs | 74 +++---------------- 4 files changed, 14 insertions(+), 64 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index e852be1a4e..dc4cea3006 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 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 54d86eee5c..f2bf1c2c60 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 diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 6e095e8393..9681a79ed1 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1127,7 +1127,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/_Legacy/Actions/ActionUnPublish.cs b/src/Umbraco.Web/_Legacy/Actions/ActionUnPublish.cs index 93d1da2046..a61b805859 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,15 @@ 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 - { - 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 + 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; } } From bce2c43fdd26f4f901f365d278f6a424b811e78d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 26 Oct 2018 11:21:56 +0200 Subject: [PATCH 26/31] small fixes to alignment and copy --- .../views/permissions/permissions.html | 14 ++++---------- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 6 +++--- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 6 +++--- 3 files changed, 10 insertions(+), 16 deletions(-) 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 cf7f960cea..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 @@ -39,27 +39,21 @@ -
- -
- - -
-
- +
+
+
-
- +
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 1acf30a7bd..8bc9d190fa 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1485,9 +1485,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: - Content Type Variation - Define the rules for how this content type's properties can be varied - Allow varying by Culture + 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 fc6ffa5ca3..a2487b2dc8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1507,9 +1507,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: - Content Type Variation - Define the rules for how this content type's properties can be varied - Allow varying by Culture + Allow varying by culture + Allow editors to create content of this type in different languages + Allow varying by culture Add language From 0cb19e0dbd04786c805bd2aac010b9328f476ec9 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 26 Oct 2018 11:46:10 +0200 Subject: [PATCH 27/31] Document --- src/Umbraco.Core/Services/IContentService.cs | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 64877e393e..7371686c7c 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); /// From 45cb56b96c1b0cbbe0adb0a4c0fccc38010761dc Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 26 Oct 2018 12:36:59 +0200 Subject: [PATCH 28/31] Fix ContentCultureInfos and collection --- src/Umbraco.Core/Models/ContentCultureInfos.cs | 10 +++++----- .../Models/ContentCultureInfosCollection.cs | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) 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)); } /// From e0365d4214618bdbbc0f8f18477cc903104ba764 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 26 Oct 2018 13:17:12 +0200 Subject: [PATCH 29/31] add ellipsis to action menu items that opens a dialog + fix action menu not opening anything --- .../src/views/components/editor/umb-editor-content-header.html | 2 +- .../src/views/components/editor/umb-editor-menu.html | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 @@ From ed4439e3d53a9a84a7582e1141e9383d5aabd7ed Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 26 Oct 2018 13:36:05 +0200 Subject: [PATCH 30/31] add ellipsis to more items --- .../insertcodesnippet/insertcodesnippet.html | 9 ++++----- .../src/views/components/umb-groups-builder.html | 3 ++- .../src/views/datatypes/create.html | 2 +- .../src/views/documenttypes/create.html | 4 ++-- .../src/views/mediatypes/create.html | 2 +- .../src/views/partialviewmacros/create.html | 4 ++-- .../src/views/partialviews/create.html | 4 ++-- src/Umbraco.Web.UI.Client/src/views/scripts/create.html | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) 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/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/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/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/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 @@
  • - + ...
  • From 628895cc89c59d66de1cbea95bd59952d03f2408 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 26 Oct 2018 14:38:30 +0200 Subject: [PATCH 31/31] In-Progress in ContentService --- .../Services/Implement/ContentService.cs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 200c5af096..f14747cda3 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)