From 7689ee73d4a2817ad7a05a0002a6a31e5ff6665b Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 20 Jun 2017 14:33:24 +1000 Subject: [PATCH] Wires up saving the user group --- .../Persistence/Factories/UserGroupFactory.cs | 6 +- .../src/common/resources/users.resource.js | 18 + .../src/common/services/util.service.js | 63 ++- .../src/views/users/group.controller.js | 381 ++++++++++-------- .../src/views/users/group.html | 4 +- .../users/views/groups/groups.controller.js | 2 +- src/Umbraco.Web/Editors/UsersController.cs | 57 ++- .../Models/ContentEditing/UserGroupSave.cs | 39 ++ .../Models/Mapping/UserModelMapper.cs | 20 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 10 files changed, 396 insertions(+), 195 deletions(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/UserGroupSave.cs diff --git a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs index 79f03b9910..20057db20f 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs @@ -22,6 +22,8 @@ namespace Umbraco.Core.Persistence.Factories userGroup.Id = dto.Id; userGroup.CreateDate = dto.CreateDate; userGroup.UpdateDate = dto.UpdateDate; + userGroup.StartContentId = dto.StartContentId; + userGroup.StartMediaId = dto.StartMediaId; if (dto.UserGroup2AppDtos != null) { foreach (var app in dto.UserGroup2AppDtos) @@ -49,7 +51,9 @@ namespace Umbraco.Core.Persistence.Factories UserGroup2AppDtos = new List(), CreateDate = entity.CreateDate, UpdateDate = entity.UpdateDate, - Icon = entity.Icon + Icon = entity.Icon, + StartMediaId = entity.StartMediaId, + StartContentId = entity.StartContentId }; foreach (var app in entity.AllowedSections) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index c5ed1a7fcc..b77da610c3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -174,6 +174,23 @@ "Failed to save user"); } + function saveUserGroup(userGroup) { + if (!userGroup) { + throw "userGroup not specified"; + } + + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserGroupPostData(userGroup); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostSaveUserGroup"), + formattedSaveData), + "Failed to save user group"); + } + function getUserGroup(id) { return umbRequestHelper.resourcePromise( @@ -203,6 +220,7 @@ createUser: createUser, inviteUser: inviteUser, saveUser: saveUser, + saveUserGroup: saveUserGroup, getUserGroup: getUserGroup, getUserGroups: getUserGroups, clearAvatar: clearAvatar, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index ce0bcf9e52..5b61bf15d4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -652,14 +652,14 @@ function umbDataFormatter() { }, /** formats the display model used to display the user to the model used to save the user */ - formatUserPostData: function (displayModel, preValues, action) { + formatUserPostData: function (displayModel) { //create the save model from the display model - var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message'); - + var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message'); + //make sure the userGroups are just a string array var currGroups = saveModel.userGroups; - var formattedGroups = []; + var formattedGroups = []; for (var i = 0; i < currGroups.length; i++) { if (!angular.isString(currGroups[i])) { formattedGroups.push(currGroups[i].alias); @@ -671,7 +671,7 @@ function umbDataFormatter() { saveModel.userGroups = formattedGroups; //make sure the startnodes are just a string array - var props = ["startContentIds", "startMediaIds"]; + var props = ["startContentIds", "startMediaIds"]; for (var m = 0; m < props.length; m++) { var startIds = saveModel[props[m]]; if (!startIds) { @@ -682,6 +682,59 @@ function umbDataFormatter() { formattedIds.push(startIds[j].id); } saveModel[props[m]] = formattedIds; + } + + return saveModel; + }, + + /** formats the display model used to display the user group to the model used to save the user group*/ + formatUserGroupPostData: function (displayModel) { + + //create the save model from the display model + var saveModel = _.pick(displayModel, 'id', 'alias', 'name', 'sections', 'users', 'startContentId', 'startMediaId'); + + //make sure the sections are just a string array + var currSections = saveModel.sections; + var formattedSections = []; + for (var i = 0; i < currSections.length; i++) { + if (!angular.isString(currSections[i])) { + formattedSections.push(currSections[i].alias); + } + else { + formattedSections.push(currSections[i]); + } + } + saveModel.sections = formattedSections; + + //make sure the user are just an int array + var currUsers = saveModel.users; + var formattedUsers = []; + for (var j = 0; j < currUsers.length; j++) { + if (!angular.isNumber(currUsers[j])) { + formattedUsers.push(currUsers[j].id); + } + else { + formattedUsers.push(currUsers[j]); + } + } + saveModel.users = formattedUsers; + + //make sure the startnodes are just an int + var props = ["startContentId", "startMediaId"]; + for (var m = 0; m < props.length; m++) { + var startId = saveModel[props[m]]; + if (!startId) { + continue; + } + saveModel[props[m]] = startId.id; + } + + saveModel.parentId = -1; + if (!saveModel.startContentId) { + saveModel.startContentId = -1; + } + if (!saveModel.startMediaId) { + saveModel.startMediaId = -1; } return saveModel; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js index 6953d8b5a8..a53ee21883 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js @@ -1,189 +1,218 @@ (function () { - "use strict"; + "use strict"; - function UserGroupEditController($scope, $timeout, $location, $routeParams, usersResource, localizationService) { + function UserGroupEditController($scope, $timeout, $location, $routeParams, usersResource, localizationService, contentEditingHelper) { - var vm = this; + var vm = this; + var localizeSaving = localizationService.localize("general_saving"); - vm.page = {}; - vm.userGroup = {}; - vm.labels = {}; + vm.page = {}; + vm.userGroup = {}; + vm.labels = {}; - vm.goToPage = goToPage; - vm.openSectionPicker = openSectionPicker; - vm.openContentPicker = openContentPicker; - vm.openMediaPicker = openMediaPicker; - vm.openUserPicker = openUserPicker; - vm.removeSelectedItem = removeSelectedItem; - vm.clearStartNode = clearStartNode; - vm.getUserStateType = getUserStateType; + vm.goToPage = goToPage; + vm.openSectionPicker = openSectionPicker; + vm.openContentPicker = openContentPicker; + vm.openMediaPicker = openMediaPicker; + vm.openUserPicker = openUserPicker; + vm.removeSelectedItem = removeSelectedItem; + vm.clearStartNode = clearStartNode; + vm.getUserStateType = getUserStateType; + vm.save = save; + + function init() { - function init() { + vm.loading = true; - vm.loading = true; + localizationService.localize("general_cancel").then(function (name) { + vm.labels.cancel = name; + }); - localizationService.localize("general_cancel").then(function(name){ - vm.labels.cancel = name; - }); - - if ($routeParams.create) { - // get user group scaffold - usersResource.getUserGroupScaffold().then(function (userGroup) { - vm.userGroup = userGroup; - setSectionIcon(vm.userGroup.sections); - makeBreadcrumbs(); - vm.loading = false; - }); - } else { - // get user group - usersResource.getUserGroup($routeParams.id).then(function (userGroup) { - vm.userGroup = userGroup; - setSectionIcon(vm.userGroup.sections); - makeBreadcrumbs(); - vm.loading = false; - }); - } - - } - - function goToPage(ancestor) { - $location.path(ancestor.path).search("subview", ancestor.subView); - } - - function openSectionPicker() { - vm.sectionPicker = { - title: "Select sections", - view: "sectionpicker", - selection: vm.userGroup.sections, - closeButtonLabel: vm.labels.cancel, - show: true, - submit: function(model) { - vm.sectionPicker.show = false; - vm.sectionPicker = null; - }, - close: function(oldModel) { - if(oldModel.selection) { - vm.userGroup.sections = oldModel.selection; - } - vm.sectionPicker.show = false; - vm.sectionPicker = null; - } - }; - } - - function openContentPicker() { - vm.contentPicker = { - title: "Select content start node", - view: "contentpicker", - hideSubmitButton: true, - show: true, - submit: function(model) { - if(model.selection) { - vm.userGroup.startContentId = model.selection[0]; - } - vm.contentPicker.show = false; - vm.contentPicker = null; - }, - close: function(oldModel) { - vm.contentPicker.show = false; - vm.contentPicker = null; - } - }; - } - - function openMediaPicker() { - vm.contentPicker = { - title: "Select media start node", - view: "treepicker", - section: "media", - treeAlias: "media", - entityType: "media", - hideSubmitButton: true, - show: true, - submit: function(model) { - if(model.selection) { - vm.userGroup.startMediaId = model.selection[0]; - } - vm.contentPicker.show = false; - vm.contentPicker = null; - }, - close: function(oldModel) { - vm.contentPicker.show = false; - vm.contentPicker = null; - } - }; - } - - function openUserPicker() { - vm.userPicker = { - title: "Select users", - view: "userpicker", - selection: vm.userGroup.users, - show: true, - submit: function(model) { - /* - if(model.selection) { - vm.userGroup.startNodesMedia = model.selection; - } - */ - vm.userPicker.show = false; - vm.userPicker = null; - }, - close: function(oldModel) { - vm.userPicker.show = false; - vm.userPicker = null; - } - }; - } - - function removeSelectedItem(index, selection) { - if(selection && selection.length > 0) { - selection.splice(index, 1); - } - } - - function clearStartNode(type) { - if (type === "content") { - vm.userGroup.startContentId = null; - } else if (type === "media") { - vm.userGroup.startMediaId = null; - } - } - - function getUserStateType(state) { - switch (state) { - case "disabled" || "umbracoDisabled": - return "danger"; - case "pending": - return "warning"; - default: - return "success"; - } - } - - function makeBreadcrumbs() { - vm.breadcrumbs = [ - { - "name": "Groups", - "path": "/users/users/overview", - "subView": "groups" - }, - { - "name": vm.userGroup.name - } - ]; - } - - function setSectionIcon(sections) { - angular.forEach(sections, function(section) { - section.icon = "icon-section " + section.cssclass; - }); - } - - init(); + if ($routeParams.create) { + // get user group scaffold + usersResource.getUserGroupScaffold().then(function (userGroup) { + vm.userGroup = userGroup; + setSectionIcon(vm.userGroup.sections); + makeBreadcrumbs(); + vm.loading = false; + }); + } else { + // get user group + usersResource.getUserGroup($routeParams.id).then(function (userGroup) { + vm.userGroup = userGroup; + setSectionIcon(vm.userGroup.sections); + makeBreadcrumbs(); + vm.loading = false; + }); + } } - angular.module("umbraco").controller("Umbraco.Editors.Users.GroupController", UserGroupEditController); + function save() { + vm.page.saveButtonState = "busy"; + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: usersResource.saveUserGroup, + scope: $scope, + content: vm.userGroup, + // We do not redirect on failure for users - this is because it is not possible to actually save a user + // when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) { } + }).then(function (saved) { + + vm.userGroup = saved; + setSectionIcon(vm.userGroup.sections); + makeBreadcrumbs(); + vm.page.saveButtonState = "success"; + + }, function (err) { + + vm.page.saveButtonState = "error"; + + }); + } + + function goToPage(ancestor) { + $location.path(ancestor.path).search("subview", ancestor.subView); + } + + function openSectionPicker() { + vm.sectionPicker = { + title: "Select sections", + view: "sectionpicker", + selection: vm.userGroup.sections, + closeButtonLabel: vm.labels.cancel, + show: true, + submit: function (model) { + vm.sectionPicker.show = false; + vm.sectionPicker = null; + }, + close: function (oldModel) { + if (oldModel.selection) { + vm.userGroup.sections = oldModel.selection; + } + vm.sectionPicker.show = false; + vm.sectionPicker = null; + } + }; + } + + function openContentPicker() { + vm.contentPicker = { + title: "Select content start node", + view: "contentpicker", + hideSubmitButton: true, + show: true, + submit: function (model) { + if (model.selection) { + vm.userGroup.startContentId = model.selection[0]; + } + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function (oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + + function openMediaPicker() { + vm.contentPicker = { + title: "Select media start node", + view: "treepicker", + section: "media", + treeAlias: "media", + entityType: "media", + hideSubmitButton: true, + show: true, + submit: function (model) { + if (model.selection) { + vm.userGroup.startMediaId = model.selection[0]; + } + vm.contentPicker.show = false; + vm.contentPicker = null; + }, + close: function (oldModel) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } + }; + } + + function openUserPicker() { + vm.userPicker = { + title: "Select users", + view: "userpicker", + selection: vm.userGroup.users, + show: true, + submit: function (model) { + /* + if(model.selection) { + vm.userGroup.startNodesMedia = model.selection; + } + */ + vm.userPicker.show = false; + vm.userPicker = null; + }, + close: function (oldModel) { + vm.userPicker.show = false; + vm.userPicker = null; + } + }; + } + + function removeSelectedItem(index, selection) { + if (selection && selection.length > 0) { + selection.splice(index, 1); + } + } + + function clearStartNode(type) { + if (type === "content") { + vm.userGroup.startContentId = null; + } else if (type === "media") { + vm.userGroup.startMediaId = null; + } + } + + function getUserStateType(state) { + switch (state) { + case "disabled" || "umbracoDisabled": + return "danger"; + case "pending": + return "warning"; + default: + return "success"; + } + } + + function makeBreadcrumbs() { + vm.breadcrumbs = [ + { + "name": "Groups", + "path": "/users/users/overview", + "subView": "groups" + }, + { + "name": vm.userGroup.name + } + ]; + } + + function setSectionIcon(sections) { + angular.forEach(sections, function (section) { + section.icon = "icon-section " + section.cssclass; + }); + } + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Users.GroupController", UserGroupEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index e1a870972d..ad0c0e31cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -8,9 +8,9 @@ + hide-description="true"> diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index 520bc8c0a1..1c63a0828b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -51,7 +51,7 @@ } function goToUserGroup(userGroup) { - $location.path('users/users/group/' + userGroup.id); + $location.path('users/users/group/' + userGroup.id).search("create", null); } onInit(); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index a21ee9166f..193322e223 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -94,7 +94,7 @@ namespace Umbraco.Web.Editors { return Request.CreateResponse(HttpStatusCode.NotFound); } - + var user = Services.UserService.GetUserById(id); if (user == null) return Request.CreateResponse(HttpStatusCode.NotFound); @@ -124,7 +124,7 @@ namespace Umbraco.Web.Editors //track the temp file so the cleanup filter removes it tempFiles.UploadedFiles.Add(new ContentItemFile - { + { TempFilePath = file.LocalFileName }); } @@ -298,7 +298,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException( Request.CreateNotificationValidationErrorResponse("No Email server is configured")); } - + var user = Services.UserService.GetByEmail(userSave.Email); if (user != null && (user.LastLoginDate != default(DateTime) || user.EmailConfirmedDate.HasValue)) { @@ -328,12 +328,12 @@ namespace Umbraco.Web.Editors //map the save info over onto the user user = Mapper.Map(userSave, user); - + //ensure the invited date is set user.InvitedDate = DateTime.Now; //Save the updated user - Services.UserService.Save(user); + Services.UserService.Save(user); var display = Mapper.Map(user); //send the email @@ -362,7 +362,7 @@ namespace Umbraco.Web.Editors private async Task SendEmailAsync(UserDisplay userDisplay, string from, IUser to, string message) { - var token = await UserManager.GenerateEmailConfirmationTokenAsync((int)userDisplay.Id); + var token = await UserManager.GenerateEmailConfirmationTokenAsync((int)userDisplay.Id); var inviteToken = string.Format("{0}{1}{2}", (int)userDisplay.Id, @@ -389,7 +389,7 @@ namespace Umbraco.Web.Editors var emailBody = Services.TextService.Localize("user/inviteEmailCopyFormat", //Ensure the culture of the found user is used for the email! UserExtensions.GetUserCulture(to.Language, Services.TextService), - new[] {userDisplay.Name, from, message, inviteUri.ToString()}); + new[] { userDisplay.Name, from, message, inviteUri.ToString() }); await UserManager.EmailService.SendAsync(new IdentityMessage { @@ -435,7 +435,7 @@ namespace Umbraco.Web.Editors { ModelState.AddModelError("Email", "A user with the email already exists"); hasErrors = true; - } + } if (hasErrors) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); @@ -453,6 +453,47 @@ namespace Umbraco.Web.Editors return display; } + public UserGroupDisplay PostSaveUserGroup(UserGroupSave userGroupSave) + { + if (userGroupSave == null) throw new ArgumentNullException("userGroupSave"); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var intId = userGroupSave.Id.TryConvertTo(); + if (intId.Success == false) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var found = Services.UserService.GetUserGroupById(intId.Result); + if (found == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var hasErrors = false; + + var existing = Services.UserService.GetUserGroupByAlias(userGroupSave.Alias); + if (existing != null && existing.Id != userGroupSave.Id) + { + ModelState.AddModelError("Alias", "A user group with this alias already exists"); + hasErrors = true; + } + //TODO: Validate the name is unique? + + if (hasErrors) + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + + //merge the save data onto the user group + var userGroup = Mapper.Map(userGroupSave, found); + + Services.UserService.Save(userGroup, userGroupSave.Users.ToArray()); + + var display = Mapper.Map(userGroup); + + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/operationSavedHeader"), Services.TextService.Localize("speechBubbles/editUserGroupSaved")); + return display; + } + /// /// Disables the users with the given user ids /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserGroupSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserGroupSave.cs new file mode 100644 index 0000000000..a594e756dd --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UserGroupSave.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "userGroup", Namespace = "")] + public class UserGroupSave : EntityBasic, IValidatableObject + { + [DataMember(Name = "id", IsRequired = true)] + [Required] + public new int Id { get; set; } + + [DataMember(Name = "alias", IsRequired = true)] + [Required] + public override string Alias { get; set; } + + [DataMember(Name = "sections")] + public IEnumerable Sections { get; set; } + + [DataMember(Name = "users")] + public IEnumerable Users { get; set; } + + [DataMember(Name = "startContentId")] + public int StartContentId { get; set; } + + [DataMember(Name = "startMediaId")] + public int StartMediaId { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + //TODO: Add other server side validation + //if (CultureInfo.GetCultureInfo(Culture)) + // yield return new ValidationResult("The culture is invalid", new[] { "Culture" }); + + yield break; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index f5139460ff..07697c2698 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -22,6 +22,22 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + //Used for merging existing UserGroupSave to an existing IUserGroup instance - this will not create an IUserGroup instance! + config.CreateMap() + .IgnoreAllUnmapped() + .ForMember(user => user.Alias, expression => expression.MapFrom(save => save.Alias)) + .ForMember(user => user.Name, expression => expression.MapFrom(save => save.Name)) + .ForMember(user => user.StartMediaId, expression => expression.MapFrom(save => save.StartMediaId)) + .ForMember(user => user.StartContentId, expression => expression.MapFrom(save => save.StartContentId)) + .AfterMap((save, userGroup) => + { + userGroup.ClearAllowedSections(); + foreach (var section in save.Sections) + { + userGroup.AddAllowedSection(section); + } + }); + //Used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! config.CreateMap() .IgnoreAllUnmapped() @@ -194,12 +210,12 @@ namespace Umbraco.Web.Models.Mapping if (group.StartMediaId > 0) { display.StartMediaId = Mapper.Map( - services.EntityService.Get(group.StartMediaId, false)); + services.EntityService.Get(group.StartMediaId, UmbracoObjectTypes.Media)); } if (group.StartContentId > 0) { display.StartContentId = Mapper.Map( - services.EntityService.Get(group.StartContentId, false)); + services.EntityService.Get(group.StartContentId, UmbracoObjectTypes.Document)); } if (display.Icon.IsNullOrWhiteSpace()) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e1b7c47cd9..65172f3b1e 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -391,6 +391,7 @@ +