From 514452ddb9cb954b7d32a5a391024f480dc1591d Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 18 Mar 2019 17:51:04 +0100 Subject: [PATCH 01/48] Better ServerMessenger configuration --- src/Umbraco.Core/CompositionExtensions.cs | 22 +++++ .../BatchedDatabaseServerMessenger.cs | 5 +- ...aseServerRegistrarAndMessengerComponent.cs | 80 ++++++++----------- 3 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index e1c7ad4467..828a577c34 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -203,6 +203,28 @@ namespace Umbraco.Core composition.RegisterUnique(_ => registrar); } + /// + /// Sets the database server messenger options. + /// + /// The composition. + /// A function creating the options. + /// Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default. + public static void SetDatabaseServerMessengerOptions(this Composition composition, Func factory) + { + composition.RegisterUnique(factory); + } + + /// + /// Sets the database server messenger options. + /// + /// The composition. + /// Options. + /// Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default. + public static void SetDatabaseServerMessengerOptions(this Composition composition, DatabaseServerMessengerOptions options) + { + composition.RegisterUnique(_ => options); + } + /// /// Sets the short string helper. /// diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 76d7565862..818e8ecf77 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -27,9 +27,8 @@ namespace Umbraco.Web private readonly IUmbracoDatabaseFactory _databaseFactory; public BatchedDatabaseServerMessenger( - IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, - bool enableDistCalls, DatabaseServerMessengerOptions options) - : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, enableDistCalls, options) + IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, DatabaseServerMessengerOptions options) + : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options) { _databaseFactory = databaseFactory; } diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 086aa9b197..a68e137665 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -38,54 +38,44 @@ namespace Umbraco.Web.Compose public sealed class DatabaseServerRegistrarAndMessengerComposer : ComponentComposer, ICoreComposer { + public static DatabaseServerMessengerOptions GetDefaultOptions(IFactory factory) + { + var logger = factory.GetInstance(); + var indexRebuilder = factory.GetInstance(); + + return new DatabaseServerMessengerOptions + { + //These callbacks will be executed if the server has not been synced + // (i.e. it is a new server or the lastsynced.txt file has been removed) + InitializingCallbacks = new Action[] + { + //rebuild the xml cache file if the server is not synced + () => + { + // rebuild the published snapshot caches entirely, if the server is not synced + // this is equivalent to DistributedCache RefreshAll... but local only + // (we really should have a way to reuse RefreshAll... locally) + // note: refresh all content & media caches does refresh content types too + var svc = Current.PublishedSnapshotService; + svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); + svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _); + svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _); + }, + + //rebuild indexes if the server is not synced + // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific + // indexes then they can adjust this logic themselves. + () => { ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); } + } + }; + } + public override void Compose(Composition composition) { base.Compose(composition); - composition.SetServerMessenger(factory => - { - var runtime = factory.GetInstance(); - var databaseFactory = factory.GetInstance(); - var globalSettings = factory.GetInstance(); - var proflog = factory.GetInstance(); - var scopeProvider = factory.GetInstance(); - var sqlContext = factory.GetInstance(); - var logger = factory.GetInstance(); - var indexRebuilder = factory.GetInstance(); - - return new BatchedDatabaseServerMessenger( - runtime, databaseFactory, scopeProvider, sqlContext, proflog, globalSettings, - true, - //Default options for web including the required callbacks to build caches - new DatabaseServerMessengerOptions - { - //These callbacks will be executed if the server has not been synced - // (i.e. it is a new server or the lastsynced.txt file has been removed) - InitializingCallbacks = new Action[] - { - //rebuild the xml cache file if the server is not synced - () => - { - // rebuild the published snapshot caches entirely, if the server is not synced - // this is equivalent to DistributedCache RefreshAll... but local only - // (we really should have a way to reuse RefreshAll... locally) - // note: refresh all content & media caches does refresh content types too - var svc = Current.PublishedSnapshotService; - svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); - svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _); - svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _); - }, - - //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. - () => - { - ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); - } - } - }); - }); + composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); + composition.SetServerMessenger(); } } @@ -128,7 +118,7 @@ namespace Umbraco.Web.Compose } public void Initialize() - { + { //We will start the whole process when a successful request is made if (_registrar != null || _messenger != null) UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; From 7dcf2b1abb168e98cd1c190b9d7794043fb39901 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 1 Apr 2019 17:05:13 +1100 Subject: [PATCH 02/48] infinity editing dim-layer for umb-modelcolumn --- src/Umbraco.Web.UI.Client/src/less/modals.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 771be1bc2a..8e88774613 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -55,6 +55,16 @@ position: absolute;; } +.--notInFront .umb-modalcolumn::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + background: rgba(0,0,0,.4); +} + /* re-align loader */ .umb-modal .umb-loader-wrapper, .umb-modalcolumn .umb-loader-wrapper, .umb-dialog .umb-loader-wrapper{ position:relative; From 0997fe6732723eefe9872070fad9ee2f47f57eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 2 Apr 2019 10:12:21 +0200 Subject: [PATCH 03/48] checkmark column removed --- .../editor/subheader/umb-editor-sub-header.less | 1 + .../src/less/components/umb-checkmark.less | 10 ++++++---- .../src/views/users/views/users/users.html | 13 ++----------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 2e599252bb..169edbf7f4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -24,6 +24,7 @@ padding-left: 10px; padding-right: 10px; background-color: @pinkLight; + border-color: @pinkLight; border-radius: 3px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less index 75ac13bdd0..021fc8cc9b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less @@ -2,19 +2,21 @@ border: 2px solid @white; width: 25px; height: 25px; - background: @gray-7; - border-radius: 50%; + border: 1px solid @gray-7; + border-radius: 3px; box-sizing: border-box; display: flex; justify-content: center; align-items: center; - color: @white; + color: @gray-7; cursor: pointer; font-size: 15px; } .umb-checkmark--checked { - background: @green; + background: @ui-active; + border-color: @ui-active; + color: @white; } .umb-checkmark--xs { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 7fb036a57a..2991b7621b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -229,12 +229,11 @@
-
+ -
Name
User group
Last login
@@ -246,15 +245,7 @@ ng-click="vm.selectUser(user, vm.selection, $event)" ng-class="{'-selected': user.selected, '-selectable': vm.isSelectable(user)}" class="umb-table-row umb-user-table-row"> -
-
- - -
-
-
+
Date: Wed, 3 Apr 2019 10:45:04 +1100 Subject: [PATCH 04/48] Reload listview after moving items --- .../src/views/propertyeditors/listview/listview.controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 44c0c4ae7b..e52edd6e92 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -583,6 +583,8 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs .then(function () { //executes if all is successful, let's sync the tree if (newPath) { + // reload the current view so the moved items are no longer shown + $scope.reloadView($scope.contentId); //we need to do a double sync here: first refresh the node where the content was moved, // then refresh the node where the content was moved from From eb529783c46475b720309291efb438ecb35e224a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 3 Apr 2019 16:22:13 +1100 Subject: [PATCH 05/48] Don't allow move in members listview --- .../src/views/propertyeditors/listview/listview.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index e52edd6e92..8e233646bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -159,7 +159,7 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, - allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, + allowBulkMove: $scope.entityType !== 'member' && $scope.model.config.bulkActionPermissions.allowBulkMove, allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete, cultureName: $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture }; From 3d949c9a55a1a0bd0c38bd4d3177ce8057a23d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 10:37:31 +0200 Subject: [PATCH 06/48] changed user groups to use umb-table --- .../users/views/groups/groups.controller.js | 50 +++++++----- .../src/views/users/views/groups/groups.html | 80 +++++++++++-------- 2 files changed, 77 insertions(+), 53 deletions(-) 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 539a064060..20f1e406b8 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 @@ -1,7 +1,8 @@ (function () { "use strict"; - function UserGroupsController($scope, $timeout, $location, userService, userGroupsResource, formHelper, localizationService) { + function UserGroupsController($scope, $timeout, $location, userService, userGroupsResource, + formHelper, localizationService, listViewHelper) { var vm = this; @@ -27,7 +28,8 @@ // only allow editing and selection if user is member of the group or admin vm.userGroups = _.map(userGroups, function (ug) { - return { group: ug, hasAccess: user.userGroups.indexOf(ug.alias) !== -1 || user.userGroups.indexOf("admin") !== -1} + ug.hasAccess = user.userGroups.indexOf(ug.alias) !== -1 || user.userGroups.indexOf("admin") !== -1; + return ug; }); vm.loading = false; @@ -42,36 +44,42 @@ // go to create user group $location.path('users/users/group/-1').search("create", "true");; } - - function goToUserGroup(userGroup) { + + function goToUserGroup(userGroup, $event) { + // only allow editing if user is member of the group or admin - if (currentUser.userGroups.indexOf(userGroup.group.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { + if (currentUser.userGroups.indexOf(userGroup.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { return; } - $location.path('users/users/group/' + userGroup.group.id).search("create", null); + $location.path(getEditPath(userGroup)).search("create", null); + } + + function getEditPath(userGroup) { + + // only allow editing if user is member of the group or admin + if (currentUser.userGroups.indexOf(userGroup.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { + return ""; + } + + return 'users/users/group/' + userGroup.id; } - function selectUserGroup(userGroup, selection, event) { - + function selectUserGroup(userGroup, $index, $event) { + + console.log(userGroup, $index); + // Only allow selection if user is member of the group or admin - if (currentUser.userGroups.indexOf(userGroup.group.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { + if (currentUser.userGroups.indexOf(userGroup.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { return; } // Disallow selection of the admin/translators group, the checkbox is not visible in the UI, but clicking(and thus selecting) is still possible. // Currently selection can only be used for deleting, and the Controller will also disallow deleting the admin group. - if (userGroup.group.alias === "admin" || userGroup.group.alias === "translator") + if (userGroup.alias === "admin" || userGroup.alias === "translator") return; - - if (userGroup.selected) { - var index = selection.indexOf(userGroup.group.id); - selection.splice(index, 1); - userGroup.selected = false; - } else { - userGroup.selected = true; - vm.selection.push(userGroup.group.id); - } - - if(event){ + + listViewHelper.selectHandler(userGroup, $index, vm.userGroups, vm.selection, $event); + + if(event) { event.stopPropagation(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 863d9658ae..ddf2e5a802 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -70,38 +70,54 @@
- - - - - - - - - +
-
- - - -
Group
- - - - -
+
+
+
+
Name
+
Sections
+
Content start node
+
Media start node
+
Permissions
+
+
+ +
+
+ +
+ +
+
+
+ {{ section.name }}, + All sections +
+
+ No start node selected + {{ group.contentStartNode.name }} +
+
+ No start node selected + {{ group.mediaStartNode.name }} +
+
+ {{ permission.name }}, +
+
+
+
From 7f61a6fe9f37a01aac736891e62368dc4ccf1886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 10:51:01 +0200 Subject: [PATCH 07/48] ability to open as a new tab --- .../views/users/views/groups/groups.controller.js | 12 +++++++++--- .../src/views/users/views/groups/groups.html | 10 +++++----- 2 files changed, 14 insertions(+), 8 deletions(-) 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 20f1e406b8..ccc18ac4f0 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 @@ -9,8 +9,8 @@ vm.userGroups = []; vm.selection = []; + vm.clickUserGroupName = clickUserGroupName; vm.createUserGroup = createUserGroup; - vm.goToUserGroup = goToUserGroup; vm.clearSelection = clearSelection; vm.selectUserGroup = selectUserGroup; vm.deleteUserGroups = deleteUserGroups; @@ -54,6 +54,14 @@ $location.path(getEditPath(userGroup)).search("create", null); } + function clickUserGroupName(item, $event) { + if(!($event.metaKey || $event.ctrlKey)) { + goToUserGroup(item, $event); + $event.preventDefault(); + } + $event.stopPropagation(); + }; + function getEditPath(userGroup) { // only allow editing if user is member of the group or admin @@ -66,8 +74,6 @@ function selectUserGroup(userGroup, $index, $event) { - console.log(userGroup, $index); - // Only allow selection if user is member of the group or admin if (currentUser.userGroups.indexOf(userGroup.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { return; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index ddf2e5a802..5e77becdf1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -87,16 +87,16 @@
+ ng-class="{'-selected': group.selected, '-selectable': group.hasAccess && group.alias !== 'admin' && group.alias !== 'translator'}">
-
- + From 17e9487bf0fb5025cd687c34d4b32ebaef2ad43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 11:18:15 +0200 Subject: [PATCH 08/48] remove end of none existing anchor element --- src/Umbraco.Web.UI.Client/src/views/components/umb-table.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index befdd8ee78..b1ca6a91c5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -12,7 +12,6 @@
Name -
Status From d2ff96e7ea85e4c1785ca7fd687ccaf1bd542805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 11:18:37 +0200 Subject: [PATCH 09/48] show checkmark icon when checked --- .../src/views/users/views/groups/groups.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 5e77becdf1..00253906dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -90,7 +90,8 @@ ng-class="{'-selected': group.selected, '-selectable': group.hasAccess && group.alias !== 'admin' && group.alias !== 'translator'}">
- + +
- Name - + Name +
Status From db0a069b02810a75e26bd3857dbb785b887cefe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 12:40:15 +0200 Subject: [PATCH 11/48] move highlight css to umb-table-body__link --- .../src/less/components/umb-table.less | 9 +++++++-- .../src/views/users/views/groups/groups.html | 4 ++-- .../src/views/users/views/users/users.html | 4 +++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 6674e01475..73a758310d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -136,9 +136,14 @@ input.umb-table__input { } .umb-table-body__link { + + color: @ui-option-type; + font-size: 14px; + font-weight: bold; text-decoration: none; - - &:hover { + + &:hover, &:focus { + color: @ui-option-type-hover; text-decoration: underline; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 00253906dc..6ad06926fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -75,7 +75,7 @@
-
Name
+
Name
Sections
Content start node
Media start node
@@ -93,7 +93,7 @@
-
+
- + {{user.name}} From df46593ef3713b8e9aa8d3eb1ade8a9f12b19faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 15:00:00 +0200 Subject: [PATCH 12/48] improved list spacing, better text-overflow & center aligned select-all-checkbox for users list --- .../src/less/components/users/umb-user-table.less | 11 ++++------- .../src/views/users/views/groups/groups.html | 6 ++++-- .../src/views/users/views/users/users.html | 12 +++++------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less index bc85ae90a9..0c61a5d113 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less @@ -3,6 +3,10 @@ .umb-user-table-col-avatar { flex: 0 0 32px; padding: 15px 0; + + .umb-checkmark { + margin-left:5px; + } } .umb-table-cell a { @@ -14,15 +18,8 @@ } .umb-table-body .umb-table-cell.umb-table__name { - margin: 0; - padding: 0; a { display: flex; - padding: 6px 2px; - height: 42px; - span { - margin: auto 14px; - } } } .umb-table-cell.umb-table__name a { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 6ad06926fc..cf0a7fbd4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -103,8 +103,10 @@
- {{ section.name }}, - All sections +
+ {{ section.name }}, + All sections +
No start node selected diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 746747e9a8..f328cf052e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -229,7 +229,7 @@
-
+
@@ -245,7 +245,7 @@ ng-click="vm.selectUser(user, vm.selection, $event)" ng-class="{'-selected': user.selected, '-selectable': vm.isSelectable(user)}" class="umb-table-row umb-user-table-row"> -
+ -
{{ userGroup.name }},
+
{{ userGroup.name }},
{{ user.formattedLastLogin }}
Date: Wed, 3 Apr 2019 15:11:36 +0200 Subject: [PATCH 13/48] removed permissions column --- .../src/views/users/views/groups/groups.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index cf0a7fbd4b..5e3542a30e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -79,7 +79,6 @@
Sections
Content start node
Media start node
-
Permissions
@@ -116,9 +115,6 @@ No start node selected {{ group.mediaStartNode.name }}
-
- {{ permission.name }}, -
From bc821db8eb3b6bbdc74b2514da69c1a43b8a5caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 15:11:55 +0200 Subject: [PATCH 14/48] less space for content-node and media-node --- .../src/less/components/umb-table.less | 9 +++++++++ .../src/views/users/views/groups/groups.html | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 73a758310d..f387b6540b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -264,6 +264,15 @@ input.umb-table__input { flex: 0 0 auto !important; } +.umb-table-cell--small { + flex: .5 .5 1%; + max-width: 12.5%; +} +.umb-table-cell--large { + flex: 1 1 25%; + max-width: 25%; +} + .umb-table-cell--faded { opacity: 0.4; } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 5e3542a30e..ba1d6bc9eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -77,8 +77,8 @@
Name
Sections
-
Content start node
-
Media start node
+
Content start node
+
Media start node
@@ -107,11 +107,11 @@ All sections
-
+
No start node selected {{ group.contentStartNode.name }}
-
+
No start node selected {{ group.mediaStartNode.name }}
From 63fb8f59330c5a34f62eb871c512acb24af2def5 Mon Sep 17 00:00:00 2001 From: Andrew Deans Date: Tue, 2 Apr 2019 23:00:08 -0400 Subject: [PATCH 15/48] Fix case of query parameter --- .../Repositories/Implement/ExternalLoginRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 7cb78f4c9f..0fa48e5521 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IIdentityUserLogin PerformGet(int id) { var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.Where(GetBaseWhereClause(), new { id = id }); var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (dto == null) From f985c437b5a55c614490eebf8323411b87c52558 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 29 Mar 2019 08:26:36 +0100 Subject: [PATCH 16/48] Add content.SetValue overload for posted files --- src/Umbraco.Core/ContentExtensions.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index f802c2d0d9..a7d40b0b7d 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -7,6 +7,7 @@ using System.Web; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NPoco.Expressions; using Umbraco.Core.Composing; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -52,8 +53,8 @@ namespace Umbraco.Core return ContentStatus.Unpublished; } - - + + #endregion /// @@ -134,9 +135,14 @@ namespace Umbraco.Core /// /// Sets the posted file value of a property. /// - /// This really is for FileUpload fields only, and should be obsoleted. For anything else, - /// you need to store the file by yourself using Store and then figure out - /// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself. + public static void SetValue(this IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, HttpPostedFileBase postedFile, string culture = null, string segment = null) + { + content.SetValue(contentTypeBaseServiceProvider, propertyTypeAlias, postedFile.FileName, postedFile.InputStream, culture, segment); + } + + /// + /// Sets the posted file value of a property. + /// public static void SetValue(this IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) { if (filename == null || filestream == null) return; From b5796ad23763f4a333c90dce8a9348f6baf5214d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Apr 2019 10:44:36 +1100 Subject: [PATCH 17/48] Enable configuration of NuCache BTree block size #5114 --- .../NuCache/DataSource/BTree.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs index 1b17e0c124..910c0ca737 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs @@ -1,4 +1,5 @@ -using CSharpTest.Net.Collections; +using System.Configuration; +using CSharpTest.Net.Collections; using CSharpTest.Net.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource @@ -14,6 +15,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource CreateFile = exists ? CreatePolicy.IfNeeded : CreatePolicy.Always, FileName = filepath, + // read or write but do *not* keep in memory + CachePolicy = CachePolicy.None, + + // default is 4096, min 2^9 = 512, max 2^16 = 64K + FileBlockSize = GetBlockSize(), + // other options? }; @@ -25,6 +32,28 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return tree; } + private static int GetBlockSize() + { + var blockSize = 4096; + + var appSetting = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.BTree.BlockSize"]; + if (appSetting == null) + return blockSize; + + if (!int.TryParse(appSetting, out blockSize)) + throw new ConfigurationErrorsException($"Invalid block size value \"{appSetting}\": not a number."); + + var bit = 0; + for (var i = blockSize; i != 1; i >>= 1) + bit++; + if (1 << bit != blockSize) + throw new ConfigurationErrorsException($"Invalid block size value \"{blockSize}\": must be a power of two."); + if (blockSize < 512 || blockSize > 65536) + throw new ConfigurationErrorsException($"Invalid block size value \"{blockSize}\": must be >= 512 and <= 65536."); + + return blockSize; + } + /* class ListOfIntSerializer : ISerializer> { From e38095b72796221741e8d0d1c6269bfe216d66b3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Apr 2019 10:49:19 +1100 Subject: [PATCH 18/48] NuCache: fix loading the media cache from local files, was missing media --- src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 2 ++ .../PublishedCache/NuCache/PublishedSnapshotService.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 7ab4a64f31..331ec37248 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -503,6 +503,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // IMPORTANT kits must be sorted out by LEVEL public void SetAll(IEnumerable kits) { var lockInfo = new WriteLockInfo(); @@ -533,6 +534,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // IMPORTANT kits must be sorted out by LEVEL public void SetBranch(int rootContentId, IEnumerable kits) { var lockInfo = new WriteLockInfo(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 9c5587fbd5..7fb51463bf 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -356,6 +356,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading content from database..."); var sw = Stopwatch.StartNew(); + // IMPORTANT GetAllContentSources sorts kits by level var kits = _dataSource.GetAllContentSources(scope); _contentStore.SetAll(kits); sw.Stop(); @@ -370,7 +371,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading content from local db..."); var sw = Stopwatch.StartNew(); - var kits = _localContentDb.Select(x => x.Value).OrderBy(x => x.Node.Level); + var kits = _localContentDb.Select(x => x.Value) + .OrderBy(x => x.Node.Level); // IMPORTANT sort by level _contentStore.SetAll(kits); sw.Stop(); _logger.Debug("Loaded content from local db ({Duration}ms)", sw.ElapsedMilliseconds); @@ -422,6 +424,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading media from database..."); var sw = Stopwatch.StartNew(); + // IMPORTANT GetAllMediaSources sorts kits by level var kits = _dataSource.GetAllMediaSources(scope); _mediaStore.SetAll(kits); sw.Stop(); @@ -436,7 +439,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading media from local db..."); var sw = Stopwatch.StartNew(); - var kits = _localMediaDb.Select(x => x.Value); + var kits = _localMediaDb.Select(x => x.Value) + .OrderBy(x => x.Node.Level); // IMPORTANT sort by level _mediaStore.SetAll(kits); sw.Stop(); _logger.Debug("Loaded media from local db ({Duration}ms)", sw.ElapsedMilliseconds); @@ -647,6 +651,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { // ?? should we do some RV check here? + // IMPORTANT GetbranchContentSources sorts kits by level var kits = _dataSource.GetBranchContentSources(scope, capture.Id); _contentStore.SetBranch(capture.Id, kits); } @@ -738,6 +743,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { // ?? should we do some RV check here? + // IMPORTANT GetbranchContentSources sorts kits by level var kits = _dataSource.GetBranchMediaSources(scope, capture.Id); _mediaStore.SetBranch(capture.Id, kits); } From 1c9b449c46d3327f7be8d128cb2026ba2cb9e494 Mon Sep 17 00:00:00 2001 From: Stephan Date: Sun, 7 Apr 2019 16:24:35 +0200 Subject: [PATCH 19/48] Log more details about boot environment --- src/Umbraco.Core/Runtime/CoreRuntime.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index f00365496a..9c33f1fcfe 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Web; +using System.Web.Hosting; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; @@ -70,10 +71,15 @@ namespace Umbraco.Core.Runtime // objects. using (var timer = profilingLogger.TraceDuration( - $"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()} on {NetworkHelper.MachineName}.", + $"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()}.", "Booted.", "Boot failed.")) { + logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationID}', path '{HostingPhysicalPath}', server '{MachineName}'.", + HostingEnvironment.SiteName, + HostingEnvironment.ApplicationID, + HostingEnvironment.ApplicationPhysicalPath, + NetworkHelper.MachineName); logger.Debug("Runtime: {Runtime}", GetType().FullName); // application environment From 7e1ee26dee591d1ea9faf6166e8cbdab694c2cad Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 9 Apr 2019 10:34:59 +1000 Subject: [PATCH 20/48] Fix spelling mistake --- src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index 93c2ce05ed..4767f6fdcc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -250,7 +250,7 @@
Compatibility
-
This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100%
+
This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100%
{{compatibility.version}} diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 1840883c34..5e0d959b61 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1164,7 +1164,7 @@ To manage your website, simply open the Umbraco back office and start adding con Downloads Likes Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% External sources Author Documentation 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 8700daff03..92780b304e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1163,7 +1163,7 @@ To manage your website, simply open the Umbraco back office and start adding con Downloads Likes Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% External sources Author Documentation From be7f42c454adf437692071c6bb990ed882b08bb9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 10 Apr 2019 09:52:06 +1000 Subject: [PATCH 21/48] Don't show the recycle bin in tree pickers/dialogs --- src/Umbraco.Web/Trees/TreeControllerBase.cs | 2 +- src/Umbraco.Web/Trees/TreeQueryStringParameters.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 4e43e6c093..a815364982 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -366,7 +366,7 @@ namespace Umbraco.Web.Trees /// protected bool IsDialog(FormDataCollection queryStrings) { - return queryStrings.GetValue(TreeQueryStringParameters.IsDialog); + return queryStrings.GetValue(TreeQueryStringParameters.Use) == "dialog"; } /// diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 205871ca20..466aff5a1f 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -5,7 +5,7 @@ /// internal struct TreeQueryStringParameters { - public const string IsDialog = "isDialog"; + public const string Use = "use"; public const string Application = "application"; public const string StartNodeId = "startNodeId"; //public const string OnNodeClick = "OnNodeClick"; From 179afe7e0d1e716db47ec3b50471c7206eb967b1 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 21 Mar 2019 15:01:46 +0100 Subject: [PATCH 22/48] Add IsVisible extension on IPublishedElement --- src/Umbraco.Web/PublishedElementExtensions.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index f2a49f7f60..de7c72d21a 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -170,5 +170,23 @@ namespace Umbraco.Web } #endregion + + #region IsSomething + + /// + /// Gets a value indicating whether the content is visible. + /// + /// The content. + /// A value indicating whether the content is visible. + /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, + /// the content is visible. + public static bool IsVisible(this IPublishedElement content) + { + // rely on the property converter - will return default bool value, ie false, if property + // is not defined, or has no value, else will return its value. + return content.Value(Constants.Conventions.Content.NaviHide) == false; + } + + #endregion } } From 576c10cd50029577ddbe2683c6ef05fd029f6763 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 15 Apr 2019 12:56:45 +1000 Subject: [PATCH 23/48] NuCache: fix vanishing content when refreshing content types --- .../PublishedCache/NuCache/ContentStore.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 331ec37248..48c68ab9bf 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -313,6 +313,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var removedContentTypeNodes = new List(); var refreshedContentTypeNodes = new List(); + // find all the nodes that are either refreshed or removed, + // because of their content type being either refreshed or removed foreach (var link in _contentNodes.Values) { var node = link.Value; @@ -322,39 +324,49 @@ namespace Umbraco.Web.PublishedCache.NuCache if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); } - // all content should have been deleted - but + // perform deletion of content with removed content type + // removing content types should have removed their content already + // but just to be 100% sure, clear again here foreach (var node in removedContentTypeNodes) ClearBranchLocked(node); + // perform deletion of removed content types foreach (var id in removedIdsA) { - if (_contentTypesById.TryGetValue(id, out LinkedNode link) == false || link.Value == null) + if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) continue; SetValueLocked(_contentTypesById, id, null); SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); } + // perform update of refreshed content types foreach (var type in refreshedTypesA) { SetValueLocked(_contentTypesById, type.Id, type); SetValueLocked(_contentTypesByAlias, type.Alias, type); } - // skip missing type - // skip missing parents & unbuildable kits - what else could we do? + // perform update of content with refreshed content type - from the kits + // skip missing type, skip missing parents & unbuildable kits - what else could we do? + // kits are ordered by level, so ParentExits is ok here var visited = new List(); foreach (var kit in kits.Where(x => refreshedIdsA.Contains(x.ContentTypeId) && ParentExistsLocked(x) && BuildKit(x))) { + // replacing the node: must preserve the parents + var node = GetHead(_contentNodes, kit.Node.Id)?.Value; + if (node != null) + kit.Node.ChildContentIds = node.ChildContentIds; + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + visited.Add(kit.Node.Id); if (_localDb != null) RegisterChange(kit.Node.Id, kit); } // all content should have been refreshed - but... - var orphans = refreshedContentTypeNodes.Except(visited); foreach (var id in orphans) ClearBranchLocked(id); From fa655b812ce958fb25a283fcf27534cbd1dbfec6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 15 Apr 2019 17:55:22 +1000 Subject: [PATCH 24/48] Fix composers ordering --- src/Umbraco.Core/Composing/Composers.cs | 133 +++++++++++++----- .../Components/ComponentTests.cs | 28 ++++ src/Umbraco.Web/Runtime/WebFinalComposer.cs | 2 + 3 files changed, 125 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Core/Composing/Composers.cs b/src/Umbraco.Core/Composing/Composers.cs index 14cb0dce8e..0510740e42 100644 --- a/src/Umbraco.Core/Composing/Composers.cs +++ b/src/Umbraco.Core/Composing/Composers.cs @@ -70,7 +70,23 @@ namespace Umbraco.Core.Composing } } - private IEnumerable PrepareComposerTypes() + internal IEnumerable PrepareComposerTypes() + { + var requirements = GetRequirements(); + + // only for debugging, this is verbose + //_logger.Debug(GetComposersReport(requirements)); + + var sortedComposerTypes = SortComposers(requirements); + + // bit verbose but should help for troubleshooting + //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; + _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); + + return sortedComposerTypes; + } + + internal Dictionary> GetRequirements(bool throwOnMissing = true) { // create a list, remove those that cannot be enabled due to runtime level var composerTypeList = _composerTypes @@ -89,25 +105,69 @@ namespace Umbraco.Core.Composing // enable or disable composers EnableDisableComposers(composerTypeList); - // sort the composers according to their dependencies - var requirements = new Dictionary>(); - foreach (var type in composerTypeList) requirements[type] = null; - foreach (var type in composerTypeList) + void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) + where TAttribute : Attribute { - GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements); - GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements); + foreach (var attribute in type.GetCustomAttributes()) + { + var typeInAttribute = getTypeInAttribute(attribute); + if (typeInAttribute != null && // if the attribute references a type ... + typeInAttribute.IsInterface && // ... which is an interface ... + typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... + !iset.Contains(typeInAttribute)) // ... which is not already in the list + { + // add it to the new list + iset.Add(typeInAttribute); + set2.Add(typeInAttribute); + + // add all its interfaces implementing IComposer + foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x))) + { + iset.Add(i); + set2.Add(i); + } + } + } } - // only for debugging, this is verbose - //_logger.Debug(GetComposersReport(requirements)); + // gather interfaces too + var interfaces = new HashSet(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); + composerTypeList.AddRange(interfaces); + var list1 = composerTypeList; + while (list1.Count > 0) + { + var list2 = new List(); + foreach (var t in list1) + { + GatherInterfaces(t, a => a.RequiredType, interfaces, list2); + GatherInterfaces(t, a => a.RequiringType, interfaces, list2); + } + composerTypeList.AddRange(list2); + list1 = list2; + } + // sort the composers according to their dependencies + var requirements = new Dictionary>(); + foreach (var type in composerTypeList) + requirements[type] = null; + foreach (var type in composerTypeList) + { + GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); + GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); + } + + return requirements; + } + + internal IEnumerable SortComposers(Dictionary> requirements) + { // sort composers var graph = new TopoGraph>>(kvp => kvp.Key, kvp => kvp.Value); graph.AddItems(requirements); List sortedComposerTypes; try { - sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).ToList(); + sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); } catch (Exception e) { @@ -117,40 +177,37 @@ namespace Umbraco.Core.Composing throw; } - // bit verbose but should help for troubleshooting - //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; - _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); - return sortedComposerTypes; } - private static string GetComposersReport(Dictionary> requirements) + internal static string GetComposersReport(Dictionary> requirements) { var text = new StringBuilder(); text.AppendLine("Composers & Dependencies:"); + text.AppendLine(" < compose before"); + text.AppendLine(" > compose after"); + text.AppendLine(" : implements"); + text.AppendLine(" = depends"); text.AppendLine(); + bool HasReq(IEnumerable types, Type type) + => types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); + foreach (var kvp in requirements) { var type = kvp.Key; text.AppendLine(type.FullName); foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); - foreach (var i in type.GetInterfaces()) { - text.AppendLine(" : " + i.FullName); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); + var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); + text.AppendLine(" > " + attribute.RequiredType + + (weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); } + foreach (var attribute in type.GetCustomAttributes()) + text.AppendLine(" < " + attribute.RequiringType); + foreach (var i in type.GetInterfaces()) + text.AppendLine(" : " + i.FullName); if (kvp.Value != null) foreach (var t in kvp.Value) text.AppendLine(" = " + t); @@ -221,16 +278,16 @@ namespace Umbraco.Core.Composing types.Remove(kvp.Key); } - private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary> requirements, bool throwOnMissing = true) { // get 'require' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requireAttributes = type + var afterAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. - foreach (var attr in requireAttributes) + foreach (var attr in afterAttributes) { if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) @@ -238,13 +295,13 @@ namespace Umbraco.Core.Composing // unless strong, and then require at least one enabled composer implementing that interface if (attr.RequiredType.IsInterface) { - var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList(); + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList(); if (implems.Count > 0) { if (requirements[type] == null) requirements[type] = new List(); requirements[type].AddRange(implems); } - else if (attr.Weak == false) // if explicitly set to !weak, is strong, else is weak + else if (attr.Weak == false && throwOnMissing) // if explicitly set to !weak, is strong, else is weak throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } // requiring a class = require that the composer is enabled @@ -256,28 +313,28 @@ namespace Umbraco.Core.Composing if (requirements[type] == null) requirements[type] = new List(); requirements[type].Add(attr.RequiredType); } - else if (attr.Weak != true) // if not explicitly set to weak, is strong + else if (attr.Weak != true && throwOnMissing) // if not explicitly set to weak, is strong throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } } } - private static void GatherRequirementsFromRequiredByAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary> requirements) { // get 'required' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requiredAttributes = type + var beforeAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer - foreach (var attr in requiredAttributes) + foreach (var attr in beforeAttributes) { if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) // required by an interface = by any enabled composer implementing this that interface if (attr.RequiringType.IsInterface) { - var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList(); + var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList(); foreach (var implem in implems) { if (requirements[implem] == null) requirements[implem] = new List(); diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index c026e5a157..2ba94d8c78 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Compose; using Umbraco.Core.Composing; using Umbraco.Core.IO; @@ -299,11 +300,19 @@ namespace Umbraco.Tests.Components composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer2) }; composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer12) }; composers = new Composers(composition, types, Mock.Of()); @@ -349,6 +358,25 @@ namespace Umbraco.Tests.Components Assert.AreEqual(typeof(Composer27), Composed[1]); } + [Test] + public void AllComposers() + { + var typeLoader = new TypeLoader(AppCaches.Disabled.RuntimeCache, IOHelper.MapPath("~/App_Data/TEMP"), Mock.Of()); + + var register = MockRegister(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); + + var types = typeLoader.GetTypes().Where(x => x.FullName.StartsWith("Umbraco.Core.") || x.FullName.StartsWith("Umbraco.Web")); + var composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(); + var report = Composers.GetComposersReport(requirements); + Console.WriteLine(report); + var composerTypes = composers.SortComposers(requirements); + + foreach (var type in composerTypes) + Console.WriteLine(type); + } + #region Compothings public class TestComposerBase : IComposer diff --git a/src/Umbraco.Web/Runtime/WebFinalComposer.cs b/src/Umbraco.Web/Runtime/WebFinalComposer.cs index 64d7725848..c69ae1af1a 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComposer.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComposer.cs @@ -3,7 +3,9 @@ namespace Umbraco.Web.Runtime { // web's final composer composes after all user composers + // and *also* after ICoreComposer (in case IUserComposer is disabled) [ComposeAfter(typeof(IUserComposer))] + [ComposeAfter(typeof(ICoreComposer))] public class WebFinalComposer : ComponentComposer { } } From 723d9eddd139a2a11cab9fc0f40abe586a6ef280 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 15 Apr 2019 18:15:52 +1000 Subject: [PATCH 25/48] unbreaks a ctor change preventing 8.0.2 from being dropped into the bin --- src/Umbraco.Web/BatchedDatabaseServerMessenger.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 818e8ecf77..f6d0bfd36a 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Scoping; using Umbraco.Web.Composing; +using System.ComponentModel; namespace Umbraco.Web { @@ -26,6 +27,14 @@ namespace Umbraco.Web { private readonly IUmbracoDatabaseFactory _databaseFactory; + [Obsolete("This overload should not be used, enableDistCalls has no effect")] + [EditorBrowsable(EditorBrowsableState.Never)] + public BatchedDatabaseServerMessenger( + IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, bool enableDistCalls, DatabaseServerMessengerOptions options) + : this(runtime, databaseFactory, scopeProvider, sqlContext, proflog, globalSettings, options) + { + } + public BatchedDatabaseServerMessenger( IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, DatabaseServerMessengerOptions options) : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options) From 02ddf80014093fa4dc4b60c8dd01f954519c9b61 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Apr 2019 15:39:28 +1000 Subject: [PATCH 26/48] Adds a unit test --- .../Repositories/MemberTypeRepositoryTest.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 0c2314fd47..561822bbbe 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -232,6 +232,32 @@ namespace Umbraco.Tests.Persistence.Repositories } } + /// + /// This demonstates an issue found: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698 + /// + [Test] + public void Bug_Changing_Built_In_Member_Type_Property_Type_Aliases_Results_In_Exception() + { + //TODO: Fix this bug and then change this test + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + repository.Save(memberType); + + foreach(var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs()) + { + var prop = memberType.PropertyTypes.First(x => x.Alias == stub.Key); + prop.Alias = prop.Alias + "__0000"; + } + + Assert.Throws(() => repository.Save(memberType)); + } + } + [Test] public void Built_In_Member_Type_Properties_Are_Automatically_Added_When_Creating() { From 6f44e4fd09775c594f0d5ef051a84e77e5e19573 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Apr 2019 17:37:42 +1000 Subject: [PATCH 27/48] Fixes INotifyCollectionChanged collections to raise the correct events, ensures that no duplicate property type aliases can be added/updated to --- src/Umbraco.Core/EnumerableExtensions.cs | 17 +++++++ src/Umbraco.Core/Models/ContentTypeBase.cs | 12 +++++ src/Umbraco.Core/Models/PropertyCollection.cs | 13 ++--- .../Models/PropertyGroupCollection.cs | 17 +++---- .../Models/PropertyTypeCollection.cs | 37 ++++++++++---- src/Umbraco.Tests/Models/ContentTypeTests.cs | 50 +++++++++++++++---- .../Repositories/MemberTypeRepositoryTest.cs | 2 +- 7 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index d71ccb04b9..00bb783486 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -10,6 +10,23 @@ namespace Umbraco.Core ///
public static class EnumerableExtensions { + internal static bool HasDuplicates(this IEnumerable items, bool includeNull) + { + var hs = new HashSet(); + foreach (var item in items) + { + if (item != null || includeNull) + { + if (!hs.Add(item)) + { + return true; + } + } + } + return false; + } + + /// /// Wraps this object instance into an IEnumerable{T} consisting of a single item. /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index ed8a098299..04fac1a855 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -95,6 +95,18 @@ namespace Umbraco.Core.Models protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { + //detect if there are any duplicate aliases - this cannot be allowed + if (e.Action == NotifyCollectionChangedAction.Add + || e.Action == NotifyCollectionChangedAction.Replace) + { + var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); + if (allAliases.HasDuplicates(false)) + { + var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); + throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); + } + } + OnPropertyChanged(nameof(PropertyTypes)); } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index 977600a2f7..c587a45424 100644 --- a/src/Umbraco.Core/Models/PropertyCollection.cs +++ b/src/Umbraco.Core/Models/PropertyCollection.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Models public class PropertyCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { private readonly object _addLocker = new object(); - internal Action OnAdd; + internal Func AdditionValidator { get; set; } /// @@ -49,10 +49,12 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable properties) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var property in properties) Add(property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } /// @@ -60,8 +62,9 @@ namespace Umbraco.Core.Models /// protected override void SetItem(int index, Property property) { + var oldItem = index >= 0 ? this[index] : property; base.SetItem(index, property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property, index)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem)); } /// @@ -120,10 +123,8 @@ namespace Umbraco.Core.Models } } + //collection events will be raised in InsertItem with Add base.Add(property); - - OnAdd?.Invoke(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property)); } } diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index 26e0fef178..5422dfb792 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -19,9 +19,6 @@ namespace Umbraco.Core.Models { private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); - // TODO: this doesn't seem to be used anywhere - internal Action OnAdd; - internal PropertyGroupCollection() { } @@ -37,16 +34,19 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable groups) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var group in groups) Add(group); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } protected override void SetItem(int index, PropertyGroup item) { + var oldItem = index >= 0 ? this[index] : item; base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); } protected override void RemoveItem(int index) @@ -84,6 +84,7 @@ namespace Umbraco.Core.Models if (keyExists) throw new Exception($"Naming conflict: Changing the name of PropertyGroup '{item.Name}' would result in duplicates"); + //collection events will be raised in SetItem SetItem(IndexOfKey(item.Id), item); return; } @@ -96,16 +97,14 @@ namespace Umbraco.Core.Models var exists = Contains(key); if (exists) { + //collection events will be raised in SetItem SetItem(IndexOfKey(key), item); return; } } } - + //collection events will be raised in InsertItem base.Add(item); - OnAdd?.Invoke(); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); } finally { diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 6181ee078b..6e41f0d12b 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -19,9 +19,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); - // TODO: This doesn't seem to be used - [IgnoreDataMember] - internal Action OnAdd; internal PropertyTypeCollection(bool supportsPublishing) { @@ -43,36 +40,44 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable properties) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var property in properties) - Add(property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + Add(property); } protected override void SetItem(int index, PropertyType item) { item.SupportsPublishing = SupportsPublishing; - base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + var oldItem = index >= 0 ? this[index] : item; + base.SetItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + item.PropertyChanged += Item_PropertyChanged; } protected override void RemoveItem(int index) { var removed = this[index]; base.RemoveItem(index); + removed.PropertyChanged -= Item_PropertyChanged; OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); } protected override void InsertItem(int index, PropertyType item) { item.SupportsPublishing = SupportsPublishing; - base.InsertItem(index, item); + base.InsertItem(index, item); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + item.PropertyChanged += Item_PropertyChanged; } protected override void ClearItems() { base.ClearItems(); + foreach (var item in this) + item.PropertyChanged -= Item_PropertyChanged; OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } @@ -91,6 +96,7 @@ namespace Umbraco.Core.Models var exists = Contains(key); if (exists) { + //collection events will be raised in SetItem SetItem(IndexOfKey(key), item); return; } @@ -103,10 +109,8 @@ namespace Umbraco.Core.Models item.SortOrder = this.Max(x => x.SortOrder) + 1; } + //collection events will be raised in InsertItem base.Add(item); - OnAdd?.Invoke(); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); } finally { @@ -115,6 +119,17 @@ namespace Umbraco.Core.Models } } + /// + /// Occurs when a property changes on a PropertyType that exists in this collection + /// + /// + /// + private void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var propType = (PropertyType)sender; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType)); + } + /// /// Determines whether this collection contains a whose alias matches the specified PropertyType. /// diff --git a/src/Umbraco.Tests/Models/ContentTypeTests.cs b/src/Umbraco.Tests/Models/ContentTypeTests.cs index d9e65ba6c6..3b35bb2dfc 100644 --- a/src/Umbraco.Tests/Models/ContentTypeTests.cs +++ b/src/Umbraco.Tests/Models/ContentTypeTests.cs @@ -15,13 +15,45 @@ namespace Umbraco.Tests.Models [TestFixture] public class ContentTypeTests : UmbracoTestBase { + [Test] + public void Cannot_Add_Duplicate_Property_Aliases() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[] + { + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" } + }))); + + Assert.Throws(() => + contentType.PropertyTypeCollection.Add( + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType" })); + + } + + [Test] + public void Cannot_Update_Duplicate_Property_Aliases() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + + contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[] + { + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" } + }))); + + contentType.PropertyTypeCollection.Add(new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType2" }); + + var toUpdate = contentType.PropertyTypeCollection["myPropertyType2"]; + + Assert.Throws(() => toUpdate.Alias = "myPropertyType"); + + } [Test] public void Can_Deep_Clone_Content_Type_Sort() { var contentType = new ContentTypeSort(new Lazy(() => 3), 4, "test"); - var clone = (ContentTypeSort) contentType.DeepClone(); + var clone = (ContentTypeSort)contentType.DeepClone(); Assert.AreNotSame(clone, contentType); Assert.AreEqual(clone, contentType); Assert.AreEqual(clone.Id.Value, contentType.Id.Value); @@ -54,7 +86,7 @@ namespace Umbraco.Tests.Models contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -117,12 +149,12 @@ namespace Umbraco.Tests.Models { group.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -167,12 +199,12 @@ namespace Umbraco.Tests.Models { group.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; - contentType.AllowedContentTypes = new[] {new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2")}; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; + contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -264,12 +296,12 @@ namespace Umbraco.Tests.Models { propertyType.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 561822bbbe..0b655004da 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -248,7 +248,7 @@ namespace Umbraco.Tests.Persistence.Repositories IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); repository.Save(memberType); - foreach(var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs()) + foreach (var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs()) { var prop = memberType.PropertyTypes.First(x => x.Alias == stub.Key); prop.Alias = prop.Alias + "__0000"; From 0787824fba659a56d6f6b6d3bd4f0244fc024bcf Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Apr 2019 11:23:22 +0200 Subject: [PATCH 28/48] Pass default configuration to configuration editor for macro parameter editors --- src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs | 8 +++++++- src/Umbraco.Core/PropertyEditors/DataEditor.cs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index bb58a8ef72..8151753a43 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -13,12 +13,15 @@ namespace Umbraco.Core.PropertyEditors /// public class ConfigurationEditor : IConfigurationEditor { + private IDictionary _defaultConfiguration; + /// /// Initializes a new instance of the class. /// public ConfigurationEditor() { Fields = new List(); + _defaultConfiguration = new Dictionary(); } /// @@ -61,7 +64,10 @@ namespace Umbraco.Core.PropertyEditors /// [JsonProperty("defaultConfig")] - public virtual IDictionary DefaultConfiguration => new Dictionary(); + public virtual IDictionary DefaultConfiguration { + get => _defaultConfiguration; + internal set => _defaultConfiguration = value; + } /// public virtual object DefaultConfigurationObject => DefaultConfiguration; diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index ae6ace996e..43f4b68b99 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -173,7 +173,13 @@ namespace Umbraco.Core.PropertyEditors /// protected virtual IConfigurationEditor CreateConfigurationEditor() { - return new ConfigurationEditor(); + var editor = new ConfigurationEditor(); + // pass the default configuration if this is not a property value editor + if((Type & EditorType.PropertyValue) == 0) + { + editor.DefaultConfiguration = _defaultConfiguration; + } + return editor; } /// From d0701ae34ed9bc53e355830bcd72fae986e2f18b Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Mon, 15 Apr 2019 15:57:35 +0200 Subject: [PATCH 29/48] Add media url provider support --- .../Cache/DistributedCacheBinderTests.cs | 1 + .../PublishedContentCacheTests.cs | 1 + .../PublishedContentSnapshotTestBase.cs | 1 + .../Scoping/ScopedNuCacheTests.cs | 1 + .../Security/BackOfficeCookieManagerTests.cs | 5 +- .../TestControllerActivatorBase.cs | 1 + .../TestHelpers/TestObjects-Mocks.cs | 2 + .../TestHelpers/TestWithDatabaseBase.cs | 1 + .../Testing/TestingTests/MockTests.cs | 2 +- ...RenderIndexActionSelectorAttributeTests.cs | 4 + .../Web/Mvc/SurfaceControllerTests.cs | 4 + .../Web/Mvc/UmbracoViewPageTests.cs | 1 + .../Web/TemplateUtilitiesTests.cs | 1 + .../Web/WebExtensionMethodTests.cs | 4 + src/Umbraco.Web/Composing/Current.cs | 3 + src/Umbraco.Web/CompositionExtensions.cs | 7 ++ .../ImageCropperTemplateExtensions.cs | 8 +- .../Models/PublishedContentBase.cs | 49 ++-------- src/Umbraco.Web/PublishedContentExtensions.cs | 47 +++++++++- .../Routing/DefaultMediaUrlProvider.cs | 76 ++++++++++++++++ src/Umbraco.Web/Routing/IMediaUrlProvider.cs | 31 +++++++ .../Routing/MediaUrlProviderCollection.cs | 13 +++ .../MediaUrlProviderCollectionBuilder.cs | 9 ++ src/Umbraco.Web/Routing/UrlProvider.cs | 91 ++++++++++++++++++- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 3 + src/Umbraco.Web/Umbraco.Web.csproj | 4 + src/Umbraco.Web/UmbracoContext.cs | 4 +- src/Umbraco.Web/UmbracoContextFactory.cs | 6 +- 28 files changed, 322 insertions(+), 58 deletions(-) create mode 100644 src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs create mode 100644 src/Umbraco.Web/Routing/IMediaUrlProvider.cs create mode 100644 src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs create mode 100644 src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs diff --git a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs index c092306473..e446e049b6 100644 --- a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs @@ -159,6 +159,7 @@ namespace Umbraco.Tests.Cache TestObjects.GetUmbracoSettings(), TestObjects.GetGlobalSettings(), new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); // just assert it does not throw diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs index 2a6739df38..fbf828ad20 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs @@ -81,6 +81,7 @@ namespace Umbraco.Tests.Cache.PublishedCache new WebSecurity(_httpContextFactory.HttpContext, Current.Services.UserService, globalSettings), umbracoSettings, Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs index 7a9a882baa..7e6ae75356 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -71,6 +71,7 @@ namespace Umbraco.Tests.PublishedContent new WebSecurity(httpContext, Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index 1dcc928141..d969356ce9 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -118,6 +118,7 @@ namespace Umbraco.Tests.Scoping new WebSecurity(httpContext, Current.Services.UserService, globalSettings), umbracoSettings ?? SettingsForTests.GetDefaultUmbracoSettings(), urlProviders ?? Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs index e32df610b9..2b04a02f46 100644 --- a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs +++ b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Configuration; +using System.Linq; using System.Web; using Microsoft.Owin; using Moq; @@ -33,7 +34,7 @@ namespace Umbraco.Tests.Security Mock.Of(), Mock.Of(), new WebSecurity(Mock.Of(), Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), new List(),globalSettings, + TestObjects.GetUmbracoSettings(), new List(), Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); @@ -53,7 +54,7 @@ namespace Umbraco.Tests.Security Mock.Of(), Mock.Of(), new WebSecurity(Mock.Of(), Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), new List(), globalSettings, + TestObjects.GetUmbracoSettings(), new List(), Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index c6bbebf550..b5712a52aa 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -139,6 +139,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting webSecurity.Object, Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs index 75e9cd60cb..647824ab66 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs @@ -122,6 +122,7 @@ namespace Umbraco.Tests.TestHelpers var umbracoSettings = GetUmbracoSettings(); var globalSettings = GetGlobalSettings(); var urlProviders = new UrlProviderCollection(Enumerable.Empty()); + var mediaUrlProviders = new MediaUrlProviderCollection(Enumerable.Empty()); if (accessor == null) accessor = new TestUmbracoContextAccessor(); @@ -133,6 +134,7 @@ namespace Umbraco.Tests.TestHelpers umbracoSettings, globalSettings, urlProviders, + mediaUrlProviders, Mock.Of()); return umbracoContextFactory.EnsureUmbracoContext(httpContext).UmbracoContext; diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 7f3c855593..94154e7d9c 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -380,6 +380,7 @@ namespace Umbraco.Tests.TestHelpers Factory.GetInstance()), umbracoSettings ?? Factory.GetInstance(), urlProviders ?? Enumerable.Empty(), + Enumerable.Empty(), globalSettings ?? Factory.GetInstance(), new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index 57381eb287..d85f610236 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -80,7 +80,7 @@ namespace Umbraco.Tests.Testing.TestingTests .Returns(UrlInfo.Url("/hello/world/1234")); var urlProvider = urlProviderMock.Object; - var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, umbracoContext.VariationContextAccessor); + var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, Enumerable.Empty(), umbracoContext.VariationContextAccessor); var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); diff --git a/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs b/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs index 0b76de9879..eeb1eb4b37 100644 --- a/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs @@ -72,6 +72,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -101,6 +102,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -130,6 +132,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -159,6 +162,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); diff --git a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs index 7de2dd1aad..1a789023a5 100644 --- a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs @@ -47,6 +47,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -74,6 +75,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -104,6 +106,7 @@ namespace Umbraco.Tests.Web.Mvc Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -141,6 +144,7 @@ namespace Umbraco.Tests.Web.Mvc Mock.Of(section => section.WebRouting == webRoutingSettings), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); diff --git a/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs b/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs index d1395c6f2e..5c291c9601 100644 --- a/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs @@ -440,6 +440,7 @@ namespace Umbraco.Tests.Web.Mvc new WebSecurity(http, Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index 5e9208faf9..5e4112449e 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -105,6 +105,7 @@ namespace Umbraco.Tests.Web Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), globalSettings, new UrlProviderCollection(new[] { testUrlProvider.Object }), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of())) diff --git a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs index 21b72a3832..9ba010642e 100644 --- a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs +++ b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -31,6 +32,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); var r1 = new RouteData(); @@ -49,6 +51,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); @@ -77,6 +80,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 420c0b8a4c..5be5e45ecd 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -101,6 +101,9 @@ namespace Umbraco.Web.Composing public static UrlProviderCollection UrlProviders => Factory.GetInstance(); + public static MediaUrlProviderCollection MediaUrlProviders + => Factory.GetInstance(); + public static HealthCheckCollectionBuilder HealthCheckCollectionBuilder => Factory.GetInstance(); diff --git a/src/Umbraco.Web/CompositionExtensions.cs b/src/Umbraco.Web/CompositionExtensions.cs index 9052b20934..27a56afc1e 100644 --- a/src/Umbraco.Web/CompositionExtensions.cs +++ b/src/Umbraco.Web/CompositionExtensions.cs @@ -90,6 +90,13 @@ namespace Umbraco.Web public static UrlProviderCollectionBuilder UrlProviders(this Composition composition) => composition.WithCollectionBuilder(); + /// + /// Gets the media url providers collection builder. + /// + /// The composition. + public static MediaUrlProviderCollectionBuilder MediaUrlProviders(this Composition composition) + => composition.WithCollectionBuilder(); + /// /// Gets the backoffice sections/applications collection builder. /// diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index ed9903e07a..d7f457287a 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -130,15 +130,15 @@ namespace Umbraco.Web if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) return string.Empty; + var mediaItemUrl = mediaItem.MediaUrl(propertyAlias); + //get the default obj from the value converter var cropperValue = mediaItem.Value(propertyAlias); //is it strongly typed? var stronglyTyped = cropperValue as ImageCropperValue; - string mediaItemUrl; if (stronglyTyped != null) { - mediaItemUrl = stronglyTyped.Src; return GetCropUrl( mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); @@ -149,14 +149,12 @@ namespace Umbraco.Web if (jobj != null) { stronglyTyped = jobj.ToObject(); - mediaItemUrl = stronglyTyped.Src; return GetCropUrl( mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); } //it's a single string - mediaItemUrl = cropperValue.ToString(); return GetCropUrl( mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); @@ -322,7 +320,7 @@ namespace Umbraco.Web if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) return null; - imageProcessorUrl.Append(cropDataSet.Src); + imageProcessorUrl.Append(imageUrl); cropDataSet.AppendCropBaseUrl(imageProcessorUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); if (crop != null & useCropDimensions) diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 39933b49be..cdcfd8a0cd 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -81,54 +81,23 @@ namespace Umbraco.Web.Models /// /// - /// The url of documents are computed by the document url providers. The url of medias are, at the moment, - /// computed here from the 'umbracoFile' property -- but we should move to media url providers at some point. + /// The url of documents are computed by the document url providers. The url of medias are computed by the media url providers /// public virtual string GetUrl(string culture = null) // TODO: consider .GetCulture("fr-FR").Url { + var umbracoContext = UmbracoContextAccessor.UmbracoContext; + + if (umbracoContext == null) + throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext is null."); + if (umbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.UrlProvider is null."); + switch (ItemType) { case PublishedItemType.Content: - var umbracoContext = UmbracoContextAccessor.UmbracoContext; - - if (umbracoContext == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext is null."); - if (umbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.UrlProvider is null."); - return umbracoContext.UrlProvider.GetUrl(this, culture); - case PublishedItemType.Media: - var prop = GetProperty(Constants.Conventions.Media.File); - if (prop?.GetValue() == null) - { - return string.Empty; - } - - var propType = ContentType.GetPropertyType(Constants.Conventions.Media.File); - - // TODO: consider implementing media url providers - // note: that one does not support variations - //This is a hack - since we now have 2 properties that support a URL: upload and cropper, we need to detect this since we always - // want to return the normal URL and the cropper stores data as json - switch (propType.EditorAlias) - { - case Constants.PropertyEditors.Aliases.UploadField: - - return prop.GetValue().ToString(); - case Constants.PropertyEditors.Aliases.ImageCropper: - //get the url from the json format - - var stronglyTyped = prop.GetValue() as ImageCropperValue; - if (stronglyTyped != null) - { - return stronglyTyped.Src; - } - return prop.GetValue()?.ToString(); - } - - return string.Empty; - + return umbracoContext.UrlProvider.GetMediaUrl(this, Constants.Conventions.Media.File, culture); default: throw new NotSupportedException(); } diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 54afb7abbd..b776bb35db 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -45,21 +45,58 @@ namespace Umbraco.Web public static string UrlAbsolute(this IPublishedContent content) { // adapted from PublishedContentBase.Url + + if (Current.UmbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); + if (Current.UmbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); + switch (content.ItemType) { case PublishedItemType.Content: - if (Current.UmbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); - if (Current.UmbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); return Current.UmbracoContext.UrlProvider.GetUrl(content.Id, true); case PublishedItemType.Media: - throw new NotSupportedException("AbsoluteUrl is not supported for media types."); + return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, Constants.Conventions.Media.File, true); default: throw new ArgumentOutOfRangeException(); } } + /// + /// Gets the url for the media. + /// + /// The content. + /// The property alias to resolve the url from. + /// The variation language. + /// The url for the content. + /// Better use the GetMediaUrl method but that method is here to complement MediaUrlAbsolute(). + public static string MediaUrl(this IPublishedContent content, string propertyAlias, string culture = null) + { + if (Current.UmbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); + if (Current.UmbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); + + return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, propertyAlias, culture); + } + + /// + /// Gets the absolute url for the media. + /// + /// The content. + /// The property alias to resolve the url from. + /// The variation language. + /// The absolute url for the media. + public static string MediaUrlAbsolute(this IPublishedContent content, string propertyAlias, string culture = null) + { + if (Current.UmbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); + if (Current.UmbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); + + return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, propertyAlias, true, culture); + } + /// /// Gets the Url segment. /// diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs new file mode 100644 index 0000000000..8616d2fc7f --- /dev/null +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -0,0 +1,76 @@ +using System; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Web.Routing +{ + /// + /// Default media url provider. + /// + public class DefaultMediaUrlProvider : IMediaUrlProvider + { + /// + public virtual UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, + string propertyAlias, + UrlProviderMode mode, string culture, Uri current) + { + var prop = content.GetProperty(propertyAlias); + var value = prop?.GetValue(culture); + if (value == null) + { + return null; + } + + var propType = content.ContentType.GetPropertyType(propertyAlias); + + string path = null; + + switch (propType.EditorAlias) + { + case Constants.PropertyEditors.Aliases.UploadField: + path = value.ToString(); + break; + case Constants.PropertyEditors.Aliases.ImageCropper: + //get the url from the json format + + var stronglyTyped = value as ImageCropperValue; + if (stronglyTyped != null) + { + path = stronglyTyped.Src; + break; + } + path = value.ToString(); + break; + } + + return path == null ? null : UrlInfo.Url(AssembleUrl(path, current, mode).ToString(), culture); + } + + private Uri AssembleUrl(string path, Uri current, UrlProviderMode mode) + { + if (string.IsNullOrEmpty(path)) + return null; + + Uri uri; + + if (current == null) + mode = UrlProviderMode.Relative; // best we can do + + switch (mode) + { + case UrlProviderMode.Absolute: + uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); + break; + case UrlProviderMode.Relative: + case UrlProviderMode.Auto: + uri = new Uri(path, UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + + return uri.Rewrite(UriUtility.ToAbsolute(uri.GetSafeAbsolutePath())); + } + } +} diff --git a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs new file mode 100644 index 0000000000..00d0ea7ec7 --- /dev/null +++ b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides media urls. + /// + public interface IMediaUrlProvider + { + /// + /// Gets the nice url of a media item. + /// + /// The Umbraco context. + /// The published content. + /// The property alias to resolve the url from. + /// The url mode. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// The url provider can ignore the mode and always return an absolute url, + /// e.g. a cdn url provider will most likely always return an absolute url. + /// If the provider is unable to provide a url, it returns null. + /// + UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlProviderMode mode, string culture, Uri current); + } +} diff --git a/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs b/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs new file mode 100644 index 0000000000..7e2362c552 --- /dev/null +++ b/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Routing +{ + public class MediaUrlProviderCollection : BuilderCollectionBase + { + public MediaUrlProviderCollection(IEnumerable items) + : base(items) + { + } + } +} diff --git a/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs b/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs new file mode 100644 index 0000000000..7bfc56ed0d --- /dev/null +++ b/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Routing +{ + public class MediaUrlProviderCollectionBuilder : OrderedCollectionBuilderBase + { + protected override MediaUrlProviderCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 0662d46f49..77f4a3c51a 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Web.Composing; +using Umbraco.Web.Models; namespace Umbraco.Web.Routing { @@ -23,13 +24,15 @@ namespace Umbraco.Web.Routing /// The Umbraco context. /// Routing settings. /// The list of url providers. + /// The list of media url providers. /// The current variation accessor. - public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IVariationContextAccessor variationContextAccessor) + public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor) { if (routingSettings == null) throw new ArgumentNullException(nameof(routingSettings)); _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; + _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); var provider = UrlProviderMode.Auto; Mode = provider; @@ -45,12 +48,14 @@ namespace Umbraco.Web.Routing /// /// The Umbraco context. /// The list of url providers. + /// The list of media url providers /// The current variation accessor. /// An optional provider mode. - public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IVariationContextAccessor variationContextAccessor, UrlProviderMode mode = UrlProviderMode.Auto) + public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor, UrlProviderMode mode = UrlProviderMode.Auto) { _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; + _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor; Mode = mode; @@ -58,6 +63,7 @@ namespace Umbraco.Web.Routing private readonly UmbracoContext _umbracoContext; private readonly IEnumerable _urlProviders; + private readonly IEnumerable _mediaUrlProviders; private readonly IVariationContextAccessor _variationContextAccessor; /// @@ -250,5 +256,86 @@ namespace Umbraco.Web.Routing } #endregion + + #region GetMediaUrl + + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, string propertyAlias, string culture = null, Uri current = null) + => GetMediaUrl(content, propertyAlias, Mode, culture, current); + + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// A value indicating whether the url should be absolute in any case. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, string propertyAlias, bool absolute, string culture = null, Uri current = null) + => GetMediaUrl(content, propertyAlias, GetMode(absolute), culture, current); + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// The url mode. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, + string propertyAlias, UrlProviderMode mode, + string culture = null, Uri current = null) + { + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + + if (content == null) + return ""; + + // this the ONLY place where we deal with default culture - IMediaUrlProvider always receive a culture + // be nice with tests, assume things can be null, ultimately fall back to invariant + // (but only for variant content of course) + if (content.ContentType.VariesByCulture()) + { + if (culture == null) + culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + } + + if (current == null) + current = _umbracoContext.CleanedUmbracoUrl; + + var url = _mediaUrlProviders.Select(provider => + provider.GetMediaUrl(_umbracoContext, content, propertyAlias, mode, culture, current)) + .FirstOrDefault(u => u != null); + + return url?.Text ?? ""; + } + + #endregion } } diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index f1c8fcc12f..5a464701e0 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -183,6 +183,9 @@ namespace Umbraco.Web.Runtime .Append() .Append(); + composition.WithCollectionBuilder() + .Append(); + composition.RegisterUnique(); composition.WithCollectionBuilder() diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b3cab3b2cb..e565f354c8 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -214,7 +214,11 @@ + + + + diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 8ab6f8c946..9ae9a5cb16 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -33,6 +33,7 @@ namespace Umbraco.Web WebSecurity webSecurity, IUmbracoSettingsSection umbracoSettings, IEnumerable urlProviders, + IEnumerable mediaUrlProviders, IGlobalSettings globalSettings, IVariationContextAccessor variationContextAccessor) { @@ -41,6 +42,7 @@ namespace Umbraco.Web if (webSecurity == null) throw new ArgumentNullException(nameof(webSecurity)); if (umbracoSettings == null) throw new ArgumentNullException(nameof(umbracoSettings)); if (urlProviders == null) throw new ArgumentNullException(nameof(urlProviders)); + if (mediaUrlProviders == null) throw new ArgumentNullException(nameof(mediaUrlProviders)); VariationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); @@ -71,7 +73,7 @@ namespace Umbraco.Web // OriginalRequestUrl = GetRequestFromContext()?.Url ?? new Uri("http://localhost"); CleanedUmbracoUrl = UriUtility.UriToUmbraco(OriginalRequestUrl); - UrlProvider = new UrlProvider(this, umbracoSettings.WebRouting, urlProviders, variationContextAccessor); + UrlProvider = new UrlProvider(this, umbracoSettings.WebRouting, urlProviders, mediaUrlProviders, variationContextAccessor); } /// diff --git a/src/Umbraco.Web/UmbracoContextFactory.cs b/src/Umbraco.Web/UmbracoContextFactory.cs index 9bb2a79411..2a812036bf 100644 --- a/src/Umbraco.Web/UmbracoContextFactory.cs +++ b/src/Umbraco.Web/UmbracoContextFactory.cs @@ -29,12 +29,13 @@ namespace Umbraco.Web private readonly IUmbracoSettingsSection _umbracoSettings; private readonly IGlobalSettings _globalSettings; private readonly UrlProviderCollection _urlProviders; + private readonly MediaUrlProviderCollection _mediaUrlProviders; private readonly IUserService _userService; /// /// Initializes a new instance of the class. /// - public UmbracoContextFactory(IUmbracoContextAccessor umbracoContextAccessor, IPublishedSnapshotService publishedSnapshotService, IVariationContextAccessor variationContextAccessor, IDefaultCultureAccessor defaultCultureAccessor, IUmbracoSettingsSection umbracoSettings, IGlobalSettings globalSettings, UrlProviderCollection urlProviders, IUserService userService) + public UmbracoContextFactory(IUmbracoContextAccessor umbracoContextAccessor, IPublishedSnapshotService publishedSnapshotService, IVariationContextAccessor variationContextAccessor, IDefaultCultureAccessor defaultCultureAccessor, IUmbracoSettingsSection umbracoSettings, IGlobalSettings globalSettings, UrlProviderCollection urlProviders, MediaUrlProviderCollection mediaUrlProviders, IUserService userService) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _publishedSnapshotService = publishedSnapshotService ?? throw new ArgumentNullException(nameof(publishedSnapshotService)); @@ -44,6 +45,7 @@ namespace Umbraco.Web _umbracoSettings = umbracoSettings ?? throw new ArgumentNullException(nameof(umbracoSettings)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); _urlProviders = urlProviders ?? throw new ArgumentNullException(nameof(urlProviders)); + _mediaUrlProviders = mediaUrlProviders ?? throw new ArgumentNullException(nameof(mediaUrlProviders)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); } @@ -55,7 +57,7 @@ namespace Umbraco.Web var webSecurity = new WebSecurity(httpContext, _userService, _globalSettings); - return new UmbracoContext(httpContext, _publishedSnapshotService, webSecurity, _umbracoSettings, _urlProviders, _globalSettings, _variationContextAccessor); + return new UmbracoContext(httpContext, _publishedSnapshotService, webSecurity, _umbracoSettings, _urlProviders, _mediaUrlProviders, _globalSettings, _variationContextAccessor); } /// From a3ee7188246ede5bd1b257a1a6f81443236b8416 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Tue, 16 Apr 2019 13:52:39 +0200 Subject: [PATCH 30/48] Add media url provider tests --- .../Routing/MediaUrlProviderTests.cs | 152 ++++++++++++++++++ .../TestHelpers/TestWithDatabaseBase.cs | 4 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs diff --git a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs new file mode 100644 index 0000000000..5de99fdd38 --- /dev/null +++ b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Tests.PublishedContent; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Tests.Testing; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Routing +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] + public class MediaUrlProviderTests : BaseWebTest + { + private DefaultMediaUrlProvider _mediaUrlProvider; + + public override void SetUp() + { + base.SetUp(); + + _mediaUrlProvider = new DefaultMediaUrlProvider(); + } + + public override void TearDown() + { + base.TearDown(); + + _mediaUrlProvider = null; + } + + [Test] + public void Get_Media_Url_Resolves_Url_From_Upload_Property_Editor() + { + const string expected = "/media/rfeiw584/test.jpg"; + + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Resolves_Url_From_Image_Cropper_Property_Editor() + { + const string expected = "/media/rfeiw584/test.jpg"; + + var configuration = new ImageCropperConfiguration(); + var imageCropperValue = JsonConvert.SerializeObject(new ImageCropperValue + { + Src = expected + }); + + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.ImageCropper, imageCropperValue, configuration); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Can_Resolve_Absolute_Url() + { + const string mediaUrl = "/media/rfeiw584/test.jpg"; + var expected = $"http://localhost{mediaUrl}"; + + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, mediaUrl, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Absolute, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Returns_Empty_String_When_PropertyType_Is_Not_Supported() + { + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.Boolean, "0", null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "test", UrlProviderMode.Absolute, null, null); + + Assert.AreEqual(string.Empty, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Can_Resolve_Variant_Property_Url() + { + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + + var umbracoFilePropertyType = CreatePropertyType(Constants.PropertyEditors.Aliases.UploadField, null, ContentVariation.Culture); + + const string enMediaUrl = "/media/rfeiw584/en.jpg"; + const string daMediaUrl = "/media/uf8ewud2/da.jpg"; + + var property = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "umbracoFile", + PropertyType = umbracoFilePropertyType, + }; + + property.SetValue("en", enMediaUrl, true); + property.SetValue("da", daMediaUrl); + + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), new [] { umbracoFilePropertyType }, ContentVariation.Culture); + var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}}; + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, "da", null); + Assert.AreEqual(daMediaUrl, resolvedUrl); + } + + private static TestPublishedContent CreatePublishedContent(string propertyEditorAlias, object propertyValue, object dataTypeConfiguration) + { + var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing); + + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), + new[] {umbracoFilePropertyType}, ContentVariation.Nothing); + + return new TestPublishedContent(contentType, 1234, Guid.NewGuid(), + new Dictionary {{"umbracoFile", propertyValue } }, false); + } + + private static PublishedPropertyType CreatePropertyType(string propertyEditorAlias, object dataTypeConfiguration, ContentVariation variation) + { + var uploadDataType = new PublishedDataType(1234, propertyEditorAlias, new Lazy(() => dataTypeConfiguration)); + + var propertyValueConverters = new PropertyValueConverterCollection(new IPropertyValueConverter[] + { + new UploadPropertyConverter(), + new ImageCropperValueConverter(), + }); + + var publishedModelFactory = Mock.Of(); + var publishedContentTypeFactory = new Mock(); + publishedContentTypeFactory.Setup(x => x.GetDataType(It.IsAny())) + .Returns(uploadDataType); + + return new PublishedPropertyType("umbracoFile", 42, true, variation, propertyValueConverters, publishedModelFactory, publishedContentTypeFactory.Object); + } + } +} diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 94154e7d9c..146cb23c1f 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -353,7 +353,7 @@ namespace Umbraco.Tests.TestHelpers } } - protected UmbracoContext GetUmbracoContext(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null, IGlobalSettings globalSettings = null, IPublishedSnapshotService snapshotService = null) + protected UmbracoContext GetUmbracoContext(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null, IEnumerable mediaUrlProviders = null, IGlobalSettings globalSettings = null, IPublishedSnapshotService snapshotService = null) { // ensure we have a PublishedCachesService var service = snapshotService ?? PublishedSnapshotService as PublishedSnapshotService; @@ -380,7 +380,7 @@ namespace Umbraco.Tests.TestHelpers Factory.GetInstance()), umbracoSettings ?? Factory.GetInstance(), urlProviders ?? Enumerable.Empty(), - Enumerable.Empty(), + mediaUrlProviders ?? Enumerable.Empty(), globalSettings ?? Factory.GetInstance(), new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 6188f577bb..ddd67df6e5 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -143,6 +143,7 @@ + From 4d8d10f07a5efdae27d11cf99d928c208c5af707 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Apr 2019 16:03:36 +1000 Subject: [PATCH 31/48] adds comments, ignores test for now --- .../Persistence/Factories/MemberTypeReadOnlyFactory.cs | 7 +++++++ .../Persistence/Repositories/MemberTypeRepositoryTest.cs | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index c7ce98a89c..9beeddde63 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -54,6 +54,12 @@ namespace Umbraco.Core.Persistence.Factories // no id, no dataTypeDefinitionId - ouch! - better notify caller of the situation needsSaving = true; + //fixme - this is wrong, this will add the standard prop without a group, yet by default it is attempted to be added to the + // group with the `Membership` alias just like we do in the MemberTypeRepository.PersistNewItem: + // //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType + // entity.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName); + // that's exactly how it would need to be done here too, but... need to decide where/how this is done properly. + //Add the standard PropertyType to the current list propertyTypes.Add(standardPropertyType.Value); @@ -154,6 +160,7 @@ namespace Umbraco.Core.Persistence.Factories foreach (var typeDto in dto.PropertyTypes.Where(x => (x.PropertyTypeGroupId.HasValue == false || x.PropertyTypeGroupId.Value == 0) && x.Id.HasValue)) { //Internal dictionary for adding "MemberCanEdit" and "VisibleOnProfile" properties to each PropertyType + memberType.MemberTypePropertyTypes.Add(typeDto.Alias, new MemberTypePropertyProfileAccess(typeDto.ViewOnProfile, typeDto.CanEdit, typeDto.IsSensitive)); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 0b655004da..f84c477fa9 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -236,6 +236,7 @@ namespace Umbraco.Tests.Persistence.Repositories /// This demonstates an issue found: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698 /// [Test] + [Ignore("Still testing")] public void Bug_Changing_Built_In_Member_Type_Property_Type_Aliases_Results_In_Exception() { //TODO: Fix this bug and then change this test @@ -254,7 +255,9 @@ namespace Umbraco.Tests.Persistence.Repositories prop.Alias = prop.Alias + "__0000"; } - Assert.Throws(() => repository.Save(memberType)); + repository.Save(memberType); + + //Assert.Throws(() => ); } } From 522fdefb8c0535594be34d61c6b2b2be1056be1c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Apr 2019 16:42:17 +1000 Subject: [PATCH 32/48] Ensures that ALL membership properties are created by the installer, removes the logic that updates a membertype while fetching it, fixes the logic of mapping membership properties when fetching and they don't exist. --- .../Migrations/Install/DatabaseDataCreator.cs | 2 + .../Factories/MemberTypeReadOnlyFactory.cs | 45 +++++++++---------- .../Implement/MemberTypeRepository.cs | 8 +--- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index f0e6dd2e5b..1de983636b 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -226,6 +226,8 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); } diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index 9beeddde63..bafd73067f 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -9,11 +9,10 @@ namespace Umbraco.Core.Persistence.Factories { internal static class MemberTypeReadOnlyFactory { - public static IMemberType BuildEntity(MemberTypeReadOnlyDto dto, out bool needsSaving) + public static IMemberType BuildEntity(MemberTypeReadOnlyDto dto) { var standardPropertyTypes = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); - needsSaving = false; - + var memberType = new MemberType(dto.ParentId); try @@ -41,33 +40,29 @@ namespace Umbraco.Core.Persistence.Factories var propertyTypeGroupCollection = GetPropertyTypeGroupCollection(dto, memberType, standardPropertyTypes); memberType.PropertyGroups = propertyTypeGroupCollection; - var propertyTypes = GetPropertyTypes(dto, memberType, standardPropertyTypes); + memberType.NoGroupPropertyTypes = GetNoGroupPropertyTypes(dto, memberType, standardPropertyTypes); - //By Convention we add 9 standard PropertyTypes - This is only here to support loading of types that didn't have these conventions before. + // By Convention we add 9 standard PropertyTypes - This is only here to support loading of types that didn't have these conventions before. + // In theory this should not happen! The only reason this did happen was because: + // A) we didn't install all of the default membership properties by default + // B) the existing data is super old when we didn't store membership properties at all (very very old) + // So what to do? We absolutely do not want to update the database when only a read was requested, this will cause problems + // so we should just add these virtual properties, they will have no ids but they will have aliases and this should be perfectly + // fine for any membership provider logic to work since neither membership providers or asp.net identity care about whether a property + // has an ID or not. + // When the member type is saved, all the properties will correctly be added. + + //This will add this group if it doesn't exist, no need to error check here + memberType.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName); foreach (var standardPropertyType in standardPropertyTypes) { - if (dto.PropertyTypes.Any(x => x.Alias.Equals(standardPropertyType.Key))) continue; + //This will add the property if it doesn't exist, no need to error check here + memberType.AddPropertyType(standardPropertyType.Value, Constants.Conventions.Member.StandardPropertiesGroupName); - // beware! - // means that we can return a memberType "from database" that has some property types - // that do *not* come from the database and therefore are incomplete eg have no key, - // no id, no dataTypeDefinitionId - ouch! - better notify caller of the situation - needsSaving = true; - - //fixme - this is wrong, this will add the standard prop without a group, yet by default it is attempted to be added to the - // group with the `Membership` alias just like we do in the MemberTypeRepository.PersistNewItem: - // //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType - // entity.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName); - // that's exactly how it would need to be done here too, but... need to decide where/how this is done properly. - - //Add the standard PropertyType to the current list - propertyTypes.Add(standardPropertyType.Value); - //Internal dictionary for adding "MemberCanEdit", "VisibleOnProfile", "IsSensitive" properties to each PropertyType - memberType.MemberTypePropertyTypes.Add(standardPropertyType.Key, - new MemberTypePropertyProfileAccess(false, false, false)); + if (!memberType.MemberTypePropertyTypes.TryGetValue(standardPropertyType.Key, out var memberTypePropertyProfile)) + memberType.MemberTypePropertyTypes[standardPropertyType.Key] = new MemberTypePropertyProfileAccess(false, false, false); } - memberType.NoGroupPropertyTypes = propertyTypes; return memberType; } @@ -153,7 +148,7 @@ namespace Umbraco.Core.Persistence.Factories return propertyGroups; } - private static List GetPropertyTypes(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary standardProps) + private static List GetNoGroupPropertyTypes(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary standardProps) { //Find PropertyTypes that does not belong to a PropertyTypeGroup var propertyTypes = new List(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index afb6ac8b43..ffeff13207 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -308,13 +308,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dtos == null || dtos.Any() == false) return Enumerable.Empty(); - return dtos.Select(x => - { - bool needsSaving; - var memberType = MemberTypeReadOnlyFactory.BuildEntity(x, out needsSaving); - if (needsSaving) PersistUpdatedItem(memberType); - return memberType; - }).ToList(); + return dtos.Select(MemberTypeReadOnlyFactory.BuildEntity).ToList(); } /// From 0b6a369047cc301574f403e6a8d96791dacbc0ed Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Apr 2019 17:12:34 +1000 Subject: [PATCH 33/48] When removing a property group, we need to first remove the group and then re-assign the property types so that duplicates are not added --- src/Umbraco.Core/Models/ContentTypeBase.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 04fac1a855..23e83e0e21 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -409,15 +409,16 @@ namespace Umbraco.Core.Models var group = PropertyGroups[propertyGroupName]; if (group == null) return; - // re-assign the group's properties to no group + // first remove the group + PropertyGroups.RemoveItem(propertyGroupName); + + // Then re-assign the group's properties to no group foreach (var property in group.PropertyTypes) { property.PropertyGroupId = null; _noGroupPropertyTypes.Add(property); } - // actually remove the group - PropertyGroups.RemoveItem(propertyGroupName); OnPropertyChanged(nameof(PropertyGroups)); } From a2a247b41337ca4b1d70e7188757dd47a7448f65 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Apr 2019 21:00:15 +1000 Subject: [PATCH 34/48] removes the enforcement of non duplicate property type aliases for now --- src/Umbraco.Core/Models/ContentTypeBase.cs | 25 +++++++++++--------- src/Umbraco.Tests/Models/ContentTypeTests.cs | 2 ++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 23e83e0e21..725ebe1555 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -95,17 +95,20 @@ namespace Umbraco.Core.Models protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { - //detect if there are any duplicate aliases - this cannot be allowed - if (e.Action == NotifyCollectionChangedAction.Add - || e.Action == NotifyCollectionChangedAction.Replace) - { - var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); - if (allAliases.HasDuplicates(false)) - { - var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); - throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); - } - } + //enable this to detect duplicate property aliases. We do want this, however making this change in a + //patch release might be a little dangerous + + ////detect if there are any duplicate aliases - this cannot be allowed + //if (e.Action == NotifyCollectionChangedAction.Add + // || e.Action == NotifyCollectionChangedAction.Replace) + //{ + // var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); + // if (allAliases.HasDuplicates(false)) + // { + // var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); + // throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); + // } + //} OnPropertyChanged(nameof(PropertyTypes)); } diff --git a/src/Umbraco.Tests/Models/ContentTypeTests.cs b/src/Umbraco.Tests/Models/ContentTypeTests.cs index 3b35bb2dfc..9c3b976bf3 100644 --- a/src/Umbraco.Tests/Models/ContentTypeTests.cs +++ b/src/Umbraco.Tests/Models/ContentTypeTests.cs @@ -16,6 +16,7 @@ namespace Umbraco.Tests.Models public class ContentTypeTests : UmbracoTestBase { [Test] + [Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")] public void Cannot_Add_Duplicate_Property_Aliases() { var contentType = MockedContentTypes.CreateBasicContentType(); @@ -32,6 +33,7 @@ namespace Umbraco.Tests.Models } [Test] + [Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")] public void Cannot_Update_Duplicate_Property_Aliases() { var contentType = MockedContentTypes.CreateBasicContentType(); From 74b468905955cc8f37773a0e17a3356f50e4671e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 23 Apr 2019 10:59:53 +0200 Subject: [PATCH 35/48] corrected spacing --- .../src/less/components/umb-group-builder.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index e2a35c5fb5..4c23aef5f0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -137,7 +137,7 @@ input.umb-group-builder__group-title-input:disabled:hover { .umb-group-builder__group-add-property { min-height: 46px; - margin-right: 30px; + margin-right: 45px; margin-left: 270px; border-radius: 3px; From 5d0010a99311fe6f0e14384e70a8aed3a4cc3f04 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 23 Apr 2019 11:11:53 +0200 Subject: [PATCH 36/48] Fix paging error in NuCache --- .../NuCache/PublishedSnapshotService.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index bf16074040..073428336d 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1381,19 +1381,24 @@ WHERE cmsContentNu.nodeId IN ( long total; do { + // the tree is locked, counting and comparing to total is safe var descendants = _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); var items = new List(); + var count = 0; foreach (var c in descendants) { // always the edited version items.Add(GetDto(c, false)); + // and also the published version if it makes any sense if (c.Published) items.Add(GetDto(c, true)); + + count++; } db.BulkInsertRecords(items); - processed += items.Count; + processed += count; } while (processed < total); } @@ -1448,10 +1453,11 @@ WHERE cmsContentNu.nodeId IN ( long total; do { + // the tree is locked, counting and comparing to total is safe var descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDto(m, false)).ToArray(); + var items = descendants.Select(m => GetDto(m, false)).ToList(); db.BulkInsertRecords(items); - processed += items.Length; + processed += items.Count; } while (processed < total); } From 0cf857e8bcfbc7d7a7297a88b6174e844c4ebb8c Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 23 Apr 2019 12:09:13 +0200 Subject: [PATCH 37/48] Cleanup --- src/Umbraco.Core/EnumerableExtensions.cs | 11 +++------- .../src/less/modals.less | 22 +++++-------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 00bb783486..3719bb0750 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -15,13 +15,8 @@ namespace Umbraco.Core var hs = new HashSet(); foreach (var item in items) { - if (item != null || includeNull) - { - if (!hs.Add(item)) - { - return true; - } - } + if ((item != null || includeNull) && !hs.Add(item)) + return true; } return false; } @@ -117,7 +112,7 @@ namespace Umbraco.Core } } - + /// /// Returns true if all items in the other collection exist in this collection /// diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index c86ca7e2d7..52a573d8c4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -52,17 +52,7 @@ bottom: 0px; left: 0px; right: 0px; - position: absolute; -} - -.--notInFront .umb-modalcolumn::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; - background: rgba(0,0,0,.4); + position: absolute; } .--notInFront .umb-modalcolumn::after { @@ -77,7 +67,7 @@ /* re-align loader */ .umb-modal .umb-loader-wrapper, .umb-modalcolumn .umb-loader-wrapper, .umb-dialog .umb-loader-wrapper{ - position:relative; + position:relative; margin: 20px -20px; } @@ -116,7 +106,7 @@ .umb-dialog-footer{ position: absolute; overflow:auto; - text-align: right; + text-align: right; height: 32px; left: 0px; right: 0px; @@ -136,7 +126,7 @@ .umbracoDialog form{height: 100%;} /*ensures dialogs doesnt have side-by-side labels*/ -.umbracoDialog .controls-row, +.umbracoDialog .controls-row, .umb-modal .controls-row{margin-left: 0px !important;} /* modal and umb-modal are used for right.hand dialogs */ @@ -184,7 +174,7 @@ padding: 20px; background: @white; border: none; - height: auto; + height: auto; } .umb-modal .umb-panel-body{ padding: 0px 20px 0px 20px; @@ -196,7 +186,7 @@ } .umb-modal i { font-size: 20px; -} +} .umb-modal .breadcrumb { background: none; padding: 0 From 509b1151a2afc17b13cd00bd71a248c6f51c78bd Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 23 Apr 2019 12:09:22 +0200 Subject: [PATCH 38/48] Fix tests --- src/Umbraco.Tests/Composing/TypeLoaderTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 7b7574ce47..7459ae848b 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -27,10 +27,7 @@ namespace Umbraco.Tests.Composing public void Initialize() { // this ensures it's reset - _typeLoader = new TypeLoader(NoAppCache.Instance, IOHelper.MapPath("~/App_Data/TEMP"), new ProfilingLogger(Mock.Of(), Mock.Of())); - - foreach (var file in Directory.GetFiles(IOHelper.MapPath(SystemDirectories.TempData.EnsureEndsWith('/') + "TypesCache"))) - File.Delete(file); + _typeLoader = new TypeLoader(NoAppCache.Instance, IOHelper.MapPath("~/App_Data/TEMP"), new ProfilingLogger(Mock.Of(), Mock.Of()), false); // for testing, we'll specify which assemblies are scanned for the PluginTypeResolver // TODO: Should probably update this so it only searches this assembly and add custom types to be found From 417b270f59462ceb9267d4f1454060a2e8904f16 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 23 Apr 2019 13:05:04 +0200 Subject: [PATCH 39/48] V8: Allow sorting manually by name in listviews (#5252) --- .../src/views/components/umb-table.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index 75bce1c7a5..79c9a8bbe9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -11,14 +11,18 @@
- Name - + + Name + +
Status
- From 3185f7ae97723056b8fa1045f476297ed3eb2034 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 23 Apr 2019 13:07:57 +0200 Subject: [PATCH 40/48] Fix member type builtin properties handling --- .../Implement/ContentTypeCommonRepository.cs | 21 ++++- .../Repositories/MemberTypeRepositoryTest.cs | 89 ++++++++++++++++--- 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 194a00b7f2..645ab9f924 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -240,6 +240,25 @@ namespace Umbraco.Core.Persistence.Repositories.Implement propertyIx++; } contentType.NoGroupPropertyTypes = noGroupPropertyTypes; + + // ensure builtin properties + if (contentType is MemberType memberType) + { + // ensure that the group exists (ok if it already exists) + memberType.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName); + + // ensure that property types exist (ok if they already exist) + foreach (var (alias, propertyType) in builtinProperties) + { + var added = memberType.AddPropertyType(propertyType, Constants.Conventions.Member.StandardPropertiesGroupName); + + if (added) + { + var access = new MemberTypePropertyProfileAccess(false, false, false); + memberType.MemberTypePropertyTypes[alias] = access; + } + } + } } } @@ -264,7 +283,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (contentType is MemberType memberType) { var access = new MemberTypePropertyProfileAccess(dto.ViewOnProfile, dto.CanEdit, dto.IsSensitive); - memberType.MemberTypePropertyTypes.Add(dto.Alias, access); + memberType.MemberTypePropertyTypes[dto.Alias] = access; } return new PropertyType(dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, dto.Alias) diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 81d3b219ce..79e8e43804 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -171,7 +171,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - //NOTE: This tests for left join logic (rev 7b14e8eacc65f82d4f184ef46c23340c09569052) [Test] public void Can_Get_All_Members_When_No_Properties_Assigned() @@ -200,7 +199,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - [Test] public void Can_Get_Member_Type_By_Id() { @@ -233,51 +231,114 @@ namespace Umbraco.Tests.Persistence.Repositories } } - /// - /// This demonstates an issue found: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698 - /// + // See: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698 [Test] - [Ignore("Still testing")] public void Bug_Changing_Built_In_Member_Type_Property_Type_Aliases_Results_In_Exception() { - //TODO: Fix this bug and then change this test + var stubs = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) + using (provider.CreateScope()) { var repository = CreateRepository(provider); - IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType("mtype"); + + // created without the stub properties + Assert.AreEqual(1, memberType.PropertyGroups.Count); + Assert.AreEqual(3, memberType.PropertyTypes.Count()); + + // saving *new* member type adds the stub properties repository.Save(memberType); - foreach (var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs()) + // saving has added (and saved) the stub properties + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + + foreach (var stub in stubs) { var prop = memberType.PropertyTypes.First(x => x.Alias == stub.Key); prop.Alias = prop.Alias + "__0000"; } + // saving *existing* member type does *not* ensure stub properties repository.Save(memberType); - //Assert.Throws(() => ); + // therefore, nothing has changed + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + + // fetching ensures that the stub properties are there + memberType = repository.Get("mtype"); + Assert.IsNotNull(memberType); + + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count * 2, memberType.PropertyTypes.Count()); } } [Test] public void Built_In_Member_Type_Properties_Are_Automatically_Added_When_Creating() { + var stubs = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) + using (provider.CreateScope()) { var repository = CreateRepository(provider); IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + + // created without the stub properties + Assert.AreEqual(1, memberType.PropertyGroups.Count); + Assert.AreEqual(3, memberType.PropertyTypes.Count()); + + // saving *new* member type adds the stub properties repository.Save(memberType); + // saving has added (and saved) the stub properties + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + // getting with stub properties memberType = repository.Get(memberType.Id); - Assert.That(memberType.PropertyTypes.Count(), Is.EqualTo(3 + Constants.Conventions.Member.GetStandardPropertyTypeStubs().Count)); - Assert.That(memberType.PropertyGroups.Count(), Is.EqualTo(2)); + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + } + } + + [Test] + public void Built_In_Member_Type_Properties_Missing_Are_Automatically_Added_When_Creating() + { + var stubs = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + + var provider = TestObjects.GetScopeProvider(Logger); + using (provider.CreateScope()) + { + var repository = CreateRepository(provider); + + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + + // created without the stub properties + Assert.AreEqual(1, memberType.PropertyGroups.Count); + Assert.AreEqual(3, memberType.PropertyTypes.Count()); + + // add one stub property, others are still missing + memberType.AddPropertyType(stubs.First().Value, Constants.Conventions.Member.StandardPropertiesGroupName); + + // saving *new* member type adds the (missing) stub properties + repository.Save(memberType); + + // saving has added (and saved) the (missing) stub properties + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); + + // getting with stub properties + memberType = repository.Get(memberType.Id); + + Assert.AreEqual(2, memberType.PropertyGroups.Count); + Assert.AreEqual(3 + stubs.Count, memberType.PropertyTypes.Count()); } } From 547229724536222960729a2264f8cbffcf729180 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 13 Apr 2019 19:45:47 +0200 Subject: [PATCH 41/48] Add busy state to OK button --- .../components/umbconfirm.directive.js | 10 ++++++++++ .../src/views/components/umb-confirm.html | 20 +++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js index ca9e7a6712..ce6b90c1ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js @@ -60,6 +60,7 @@ function confirmDirective() { link: function (scope, element, attr, ctrl) { scope.showCancel = false; scope.showConfirm = false; + scope.confirmButtonState = "init"; if (scope.onConfirm) { scope.showConfirm = true; @@ -68,6 +69,15 @@ function confirmDirective() { if (scope.onCancel) { scope.showCancel = true; } + + scope.confirm = function () { + if (!scope.onConfirm) { + return; + } + + scope.confirmButtonState = "busy"; + scope.onConfirm(); + } } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html index 8426d03acf..b89b7f559b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html @@ -1,9 +1,21 @@ From 9280c740624fce4bd32bc7b5b953deefeade732e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 23 Apr 2019 14:19:08 +0200 Subject: [PATCH 42/48] V8: Use umb-checkbox for RTE configuration (#5119) --- src/Umbraco.Web.UI.Client/src/less/rte.less | 11 +++++++-- .../rte/rte.prevalues.controller.js | 19 ++++++--------- .../propertyeditors/rte/rte.prevalues.html | 23 ++++++++----------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index a4d6387012..ccf52acc53 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -66,6 +66,13 @@ /* pre-value editor */ -.rte-editor-preval .control-group .controls > div > label .mce-ico { - line-height: 20px; +.rte-editor-preval .control-group .controls > div > label { + cursor: pointer !important; + + .mce-cmd .checkbox { + padding-right: 0; + } + .mce-ico { + line-height: 20px; + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 1b489d6283..41097f9e9a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -31,26 +31,21 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", var icon = getFontIcon(obj.alias); return angular.extend(obj, { fontIcon: icon.name, - isCustom: icon.isCustom + isCustom: icon.isCustom, + selected: $scope.model.value.toolbar.indexOf(obj.alias) >= 0 }); }); }); stylesheetResource.getAll().then(function(stylesheets){ $scope.stylesheets = stylesheets; + + _.each($scope.stylesheets, function (stylesheet) { + // support both current format (full stylesheet path) and legacy format (stylesheet name only) + stylesheet.selected = $scope.model.value.stylesheets.indexOf(stylesheet.path) >= 0 ||$scope.model.value.stylesheets.indexOf(stylesheet.name) >= 0; + }); }); - $scope.commandSelected = function(cmd) { - cmd.selected = $scope.model.value.toolbar.indexOf(cmd.alias) >= 0; - return cmd.selected; - }; - - $scope.cssSelected = function (css) { - // support both current format (full stylesheet path) and legacy format (stylesheet name only) - css.selected = $scope.model.value.stylesheets.indexOf(css.path) >= 0 ||$scope.model.value.stylesheets.indexOf(css.name) >= 0; - return css.selected; - } - $scope.selectCommand = function(command){ var index = $scope.model.value.toolbar.indexOf(command.alias); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 463b1a03e0..ca6178fea9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -3,11 +3,10 @@
@@ -38,7 +38,8 @@ type="button" action="vm.save(vm.notifyOptions)" state="vm.saveState" - button-style="action"> + button-style="success" + disabled="!vm.canSave">
From 67e046e477d7cc836347b5bcf9bc845f094c9ad8 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 23 Apr 2019 16:14:26 +0200 Subject: [PATCH 46/48] V8: Allow nested content without blueprints (#4777) * Don't assume there is at least one NC blueprint * NC disabled icon styling (hover pointer type) * Allow and handle NC configs without blueprints --- .../src/less/components/umb-nested-content.less | 3 +++ .../nestedcontent/nestedcontent.doctypepicker.html | 2 +- .../propertyeditors/nestedcontent/nestedcontent.html | 2 +- .../NestedContentManyValueConverter.cs | 12 ++++++------ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index e0abd3fd26..db0d96b79d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -127,6 +127,9 @@ border-radius: 200px; text-decoration: none !important; } +.umb-nested-content__icon.umb-nested-content__icon--disabled:hover { + cursor: default; +} .umb-nested-content__icon:hover, .umb-nested-content__icon--active diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html index a4cf675c8e..8701005743 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html @@ -34,7 +34,7 @@ - + Delete diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index 572021aebd..bca767f650 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -37,7 +37,7 @@ diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index 93e4afc7e5..2181c4fd49 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -35,9 +35,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public override Type GetPropertyValueType(PublishedPropertyType propertyType) { var contentTypes = propertyType.DataType.ConfigurationAs().ContentTypes; - return contentTypes.Length > 1 - ? typeof (IEnumerable) - : typeof (IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)); + return contentTypes.Length == 1 + ? typeof (IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)) + : typeof (IEnumerable); } /// @@ -57,9 +57,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.ContentTypes; - var elements = contentTypes.Length > 1 - ? new List() - : PublishedModelFactory.CreateModelList(contentTypes[0].Alias); + var elements = contentTypes.Length == 1 + ? PublishedModelFactory.CreateModelList(contentTypes[0].Alias) + : new List(); var value = (string)inter; if (string.IsNullOrWhiteSpace(value)) return elements; From c19be0c64e0a5ce185e787d7770fc4e2c61c5dbd Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 23 Apr 2019 16:23:06 +0200 Subject: [PATCH 47/48] Misc cleanup --- .../Routing/DefaultMediaUrlProvider.cs | 17 +++++------------ src/Umbraco.Web/Routing/IMediaUrlProvider.cs | 2 +- src/Umbraco.Web/Routing/IUrlProvider.cs | 2 +- .../Routing/MediaUrlProviderCollection.cs | 3 +-- src/Umbraco.Web/Routing/UrlProvider.cs | 12 ++++++------ src/Umbraco.Web/UriUtility.cs | 9 +++++++++ 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs index 8616d2fc7f..a9c961c280 100644 --- a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -22,8 +22,7 @@ namespace Umbraco.Web.Routing return null; } - var propType = content.ContentType.GetPropertyType(propertyAlias); - + var propType = prop.PropertyType; string path = null; switch (propType.EditorAlias) @@ -33,18 +32,12 @@ namespace Umbraco.Web.Routing break; case Constants.PropertyEditors.Aliases.ImageCropper: //get the url from the json format - - var stronglyTyped = value as ImageCropperValue; - if (stronglyTyped != null) - { - path = stronglyTyped.Src; - break; - } - path = value.ToString(); + path = value is ImageCropperValue stronglyTyped ? stronglyTyped.Src : value.ToString(); break; } - return path == null ? null : UrlInfo.Url(AssembleUrl(path, current, mode).ToString(), culture); + var url = AssembleUrl(path, current, mode); + return url == null ? null : UrlInfo.Url(url.ToString(), culture); } private Uri AssembleUrl(string path, Uri current, UrlProviderMode mode) @@ -70,7 +63,7 @@ namespace Umbraco.Web.Routing throw new ArgumentOutOfRangeException(nameof(mode)); } - return uri.Rewrite(UriUtility.ToAbsolute(uri.GetSafeAbsolutePath())); + return UriUtility.MediaUriFromUmbraco(uri); } } } diff --git a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs index 00d0ea7ec7..419e4d78df 100644 --- a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.Routing public interface IMediaUrlProvider { /// - /// Gets the nice url of a media item. + /// Gets the url of a media item. /// /// The Umbraco context. /// The published content. diff --git a/src/Umbraco.Web/Routing/IUrlProvider.cs b/src/Umbraco.Web/Routing/IUrlProvider.cs index 55d39880d6..9459c2552d 100644 --- a/src/Umbraco.Web/Routing/IUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IUrlProvider.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Routing public interface IUrlProvider { /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The Umbraco context. /// The published content. diff --git a/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs b/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs index 7e2362c552..eef0cbd3bc 100644 --- a/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs +++ b/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs @@ -7,7 +7,6 @@ namespace Umbraco.Web.Routing { public MediaUrlProviderCollection(IEnumerable items) : base(items) - { - } + { } } } diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 77f4a3c51a..525695c274 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -90,7 +90,7 @@ namespace Umbraco.Web.Routing => GetUrl(content, Mode, culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content. /// A value indicating whether the url should be absolute in any case. @@ -115,7 +115,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), Mode, culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content identifier. /// A value indicating whether the url should be absolute in any case. @@ -130,7 +130,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), GetMode(absolute), culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content identifier. /// The url mode. @@ -151,7 +151,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), Mode, culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content identifier. /// A value indicating whether the url should be absolute in any case. @@ -166,7 +166,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), GetMode(absolute), culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content identifier. /// The url mode. @@ -177,7 +177,7 @@ namespace Umbraco.Web.Routing => GetUrl(GetDocument(id), mode, culture, current); /// - /// Gets the nice url of a published content. + /// Gets the url of a published content. /// /// The published content. /// The url mode. diff --git a/src/Umbraco.Web/UriUtility.cs b/src/Umbraco.Web/UriUtility.cs index 04357a3a5a..9e94a4a20a 100644 --- a/src/Umbraco.Web/UriUtility.cs +++ b/src/Umbraco.Web/UriUtility.cs @@ -72,6 +72,15 @@ namespace Umbraco.Web return uri.Rewrite(path); } + // maps a media umbraco uri to a public uri + // ie with virtual directory - that is all for media + public static Uri MediaUriFromUmbraco(Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + path = ToAbsolute(path); + return uri.Rewrite(path); + } + // maps a public uri to an internal umbraco uri // ie no virtual directory, no .aspx, lowercase... public static Uri UriToUmbraco(Uri uri) From fab025bf6686398c4b1ccf30a8f6d32cc64f28a5 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 23 Apr 2019 17:27:34 +0200 Subject: [PATCH 48/48] V8: Don't use floats in the slider config as it only supports integers (#4821) --- .../PropertyEditors/SliderConfiguration.cs | 10 +++++----- .../views/propertyeditors/slider/slider.controller.js | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs index 55ca199121..dc71c2a48f 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs @@ -9,18 +9,18 @@ public bool EnableRange { get; set; } [ConfigurationField("initVal1", "Initial value", "number")] - public int InitialValue { get; set; } + public decimal InitialValue { get; set; } [ConfigurationField("initVal2", "Initial value 2", "number", Description = "Used when range is enabled")] - public int InitialValue2 { get; set; } + public decimal InitialValue2 { get; set; } [ConfigurationField("minVal", "Minimum value", "number")] - public int MinimumValue { get; set; } + public decimal MinimumValue { get; set; } [ConfigurationField("maxVal", "Maximum value", "number")] - public int MaximumValue { get; set; } + public decimal MaximumValue { get; set; } [ConfigurationField("step", "Step increments", "number")] - public int StepIncrements { get; set; } + public decimal StepIncrements { get; set; } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js index 515eddf0a3..6c57cecdf1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js @@ -38,7 +38,10 @@ const tooltips = $scope.model.config.enableRange ? [true, true] : [true]; const min = $scope.model.config.minVal ? [$scope.model.config.minVal] : [$scope.model.config.minVal]; const max = $scope.model.config.maxVal ? [$scope.model.config.maxVal] : [$scope.model.config.maxVal]; - + // don't render values with decimal places if the step increment in a whole number + var stepDecimalPlaces = $scope.model.config.step % 1 == 0 + ? 0 + : _.last($scope.model.config.step.toString().replace(",", ".").split(".")).length; // setup default $scope.sliderOptions = { "start": start, @@ -46,10 +49,10 @@ "tooltips": tooltips, "format": { to: function (value) { - return Math.round(value); + return value.toFixed(stepDecimalPlaces); }, from: function (value) { - return Math.round(value); + return value; } }, "range": {