diff --git a/src/Umbraco.Web.UI.Client/src/views/media/delete.html b/src/Umbraco.Web.UI.Client/src/views/media/delete.html
index 03bf10cdff11..ec9bd5f7bd43 100644
--- a/src/Umbraco.Web.UI.Client/src/views/media/delete.html
+++ b/src/Umbraco.Web.UI.Client/src/views/media/delete.html
@@ -14,7 +14,7 @@
Are you sure you want to delete{{currentNode.name}}?
-
+
When items are deleted from the recycle bin, they will be gone forever.
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js
index 41f81996f1ae..3143f42bbefd 100644
--- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js
@@ -6,10 +6,16 @@
* @description
* The controller for deleting content
*/
-function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, overlayService) {
+function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, overlayService,localizationService) {
$scope.checkingReferences = true;
+ $scope.warningText = "The item or one of the underlying items is being used.";
+
+ localizationService.localize("references_deleteWarning").then(function(value) {
+ $scope.warningText = value;
+ });
+
$scope.performDelete = function() {
// stop from firing again on double-click
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
index 6482745133a8..349d3b8df5c5 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
@@ -2295,6 +2295,7 @@ To manage your website, simply open the Umbraco back office and start adding con
One of the underlying items is being used in a media itemOne of the underlying items is being used in a content itemOne of the underlying items is being used in a member
+ The item or one of the underlying items is being used. Deleting it can cause issuesDelete Saved Search
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 4766902ca299..96b215186864 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
@@ -2317,6 +2317,7 @@ To manage your website, simply open the Umbraco back office and start adding con
One of the underlying items is being used in a media itemOne of the underlying items is being used in a content itemOne of the underlying items is being used in a member
+ The item or one of the underlying items is being used. Deleting it can cause issuesDelete Saved Search
From d5b960909c47827940542b7f00f1fa4aaefaa6f2 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Thu, 8 Oct 2020 17:02:44 +0200
Subject: [PATCH 14/80] Show references in delete dialog
---
.../src/views/content/overlays/unpublish.controller.js | 10 ++++++++++
.../src/views/content/overlays/unpublish.html | 4 ++++
2 files changed, 14 insertions(+)
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
index 63c5b2da263a..3d46e9efee07 100644
--- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
@@ -4,8 +4,18 @@
function UnpublishController($scope, localizationService) {
var vm = this;
+
var autoSelectedVariants = [];
+ vm.id = $scope.content.id;
+ vm.checkingReferences = true;
+
+ vm.warningText = "The item or one of the underlying items is being used.";
+
+ localizationService.localize("references_deleteWarning").then(function (value) {
+ vm.warningText = value;
+ });
+
vm.changeSelection = changeSelection;
function onInit() {
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html
index 5ab32abf001f..9335965e8f92 100644
--- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html
+++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html
@@ -5,12 +5,16 @@
+
+
+
+
From 6ebc28ecac7aac07b24db64ef1a7b736e8662887 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Fri, 9 Oct 2020 10:11:41 +0200
Subject: [PATCH 15/80] Hide unpublish button during checking of references
---
.../src/views/content/overlays/unpublish.controller.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
index 3d46e9efee07..3c449d204be3 100644
--- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
@@ -136,6 +136,12 @@
});
});
+ $scope.$watch('vm.checkingReferences',
+ function(newVal) {
+
+ $scope.model.hideSubmitButton = Object.toBoolean(newVal);
+ });
+
onInit();
}
From 17cb8fc4ed73e1ba7adf2ed6af5a187f5ca47c4c Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Fri, 9 Oct 2020 10:16:48 +0200
Subject: [PATCH 16/80] Use localized text for unpublish warning
---
.../src/views/content/overlays/unpublish.controller.js | 2 +-
src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 3 ++-
src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 3 ++-
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
index 3c449d204be3..ce67b7c712c2 100644
--- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js
@@ -12,7 +12,7 @@
vm.warningText = "The item or one of the underlying items is being used.";
- localizationService.localize("references_deleteWarning").then(function (value) {
+ localizationService.localize("references_unpublishWarning").then(function (value) {
vm.warningText = value;
});
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
index 349d3b8df5c5..84a5a19e9618 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
@@ -2295,7 +2295,8 @@ To manage your website, simply open the Umbraco back office and start adding con
One of the underlying items is being used in a media itemOne of the underlying items is being used in a content itemOne of the underlying items is being used in a member
- The item or one of the underlying items is being used. Deleting it can cause issues
+ The item or one of the underlying items is being used. Deleting it can cause issues.
+ The item or one of the underlying items is being used. Unpublishing it can cause issues.Delete Saved Search
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 96b215186864..98df2c3f966b 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
@@ -2317,7 +2317,8 @@ To manage your website, simply open the Umbraco back office and start adding con
One of the underlying items is being used in a media itemOne of the underlying items is being used in a content itemOne of the underlying items is being used in a member
- The item or one of the underlying items is being used. Deleting it can cause issues
+ The item or one of the underlying items is being used. Deleting it can cause issues.
+ The item or one of the underlying items is being used. Unpublishing it can cause issues.Delete Saved Search
From a59ac2dac12c2ece6075747000d247b6acbb1460 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Fri, 9 Oct 2020 10:19:29 +0200
Subject: [PATCH 17/80] Added some styling so warning messages has bottom
margin
---
src/Umbraco.Web.UI.Client/src/less/components/overlays.less | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less
index 035bf02f910c..3112a4f1304a 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less
@@ -10,6 +10,10 @@
.scoped-view{
display: none;
}
+
+ .abstract {
+ margin-bottom : 20px;
+ }
}
.umb-overlay__form {
From 99e05601f36f283146e08846408ff4017a4b78f2 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Fri, 9 Oct 2020 16:28:48 +0200
Subject: [PATCH 18/80] Added new component for showing the references table
---
.../umbtrackedreferencestable.component.js | 32 ++++++++++++++++++
.../umb-tracked-references-table.html | 33 +++++++++++++++++++
2 files changed, 65 insertions(+)
create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencestable.component.js
create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencestable.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencestable.component.js
new file mode 100644
index 000000000000..537a32900ecb
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencestable.component.js
@@ -0,0 +1,32 @@
+
+(function () {
+ 'use strict';
+
+ angular
+ .module('umbraco.directives')
+ .component('umbTrackedReferencesTable', {
+ transclude: true,
+ templateUrl: 'views/components/references/umb-tracked-references-table.html',
+ controller: UmbTrackedReferencesTableController,
+ controllerAs: 'vm',
+ bindings: {
+ pageNumber: "<",
+ totalPages : "<",
+ title: "<",
+ items : "<",
+ onPageChanged: "&"
+ }
+ });
+
+ function UmbTrackedReferencesTableController()
+ {
+ var vm = this;
+
+ vm.changePageNumber = changePageNumber;
+
+ function changePageNumber(pageNumber) {
+ vm.onPageChanged({ 'pageNumber' : pageNumber });
+ }
+ }
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html
new file mode 100644
index 000000000000..a49a99a81eaf
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html
@@ -0,0 +1,33 @@
+
From d56b156a8587c3e81af10cb0c6b6ef1e9729a402 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Sat, 24 Oct 2020 17:02:32 +0200
Subject: [PATCH 21/80] Removed comments
---
src/Umbraco.Web/Editors/TrackedReferencesController.cs | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/src/Umbraco.Web/Editors/TrackedReferencesController.cs b/src/Umbraco.Web/Editors/TrackedReferencesController.cs
index d7b6a66b5078..7321d621c8b4 100644
--- a/src/Umbraco.Web/Editors/TrackedReferencesController.cs
+++ b/src/Umbraco.Web/Editors/TrackedReferencesController.cs
@@ -62,11 +62,8 @@ public PagedResult GetPagedReferences(int id, string entityType, in
[HttpGet]
public bool HasReferencesInDescendants(int id, string entityType)
{
- //this.Services.EntityService.GetChildren()
- //var childCount = this.Services.ContentService.CountChildren(id);
var currentEntity = this.Services.EntityService.Get(id);
-
-
+
if (currentEntity != null)
{
var currentObjectType = ObjectTypes.GetUmbracoObjectType(currentEntity.NodeObjectType);
@@ -83,7 +80,6 @@ public bool HasReferencesInDescendants(int id, string entityType)
IEntitySlim[] entities;
-
do
{
entities = this.Services.EntityService.GetPagedDescendants(id, currentObjectType, currentPage, pageSize, out _)
@@ -104,8 +100,7 @@ public bool HasReferencesInDescendants(int id, string entityType)
} while (entities.Length == pageSize);
}
-
-
+
return false;
}
}
From 951de85b5648eeaa0ac4944ab2da71c97ad4a0f4 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Sat, 24 Oct 2020 17:04:15 +0200
Subject: [PATCH 22/80] Use list instead of array
---
src/Umbraco.Web/Editors/TrackedReferencesController.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Umbraco.Web/Editors/TrackedReferencesController.cs b/src/Umbraco.Web/Editors/TrackedReferencesController.cs
index 7321d621c8b4..7229273d7f94 100644
--- a/src/Umbraco.Web/Editors/TrackedReferencesController.cs
+++ b/src/Umbraco.Web/Editors/TrackedReferencesController.cs
@@ -78,12 +78,12 @@ public bool HasReferencesInDescendants(int id, string entityType)
var pageSize = 1000;
var currentPage = 0;
- IEntitySlim[] entities;
+ var entities = new List();
do
{
entities = this.Services.EntityService.GetPagedDescendants(id, currentObjectType, currentPage, pageSize, out _)
- .ToArray();
+ .ToList();
var ids = entities.Select(x => x.Id).ToArray();
@@ -98,7 +98,7 @@ public bool HasReferencesInDescendants(int id, string entityType)
currentPage++;
- } while (entities.Length == pageSize);
+ } while (entities.Count == pageSize);
}
return false;
From bfa03ed246e98cfc4d9024c38f08103c9281b374 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Sat, 24 Oct 2020 17:07:26 +0200
Subject: [PATCH 23/80] Changed text to from underlying items to descendants
---
.../views/components/references/umb-tracked-references.html | 6 +++---
src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 6 +++---
src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 6 +++---
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html
index d7d273d7f6b3..7c6dfd3db12b 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html
@@ -54,7 +54,7 @@
- One of the underlying items is being used in content item
+ One or more of this item's descendants is being used in a content item.
@@ -102,7 +102,7 @@
- One of the underlying items is being used in a member
+ One or more of this item's descendants is being used in a a member
@@ -150,7 +150,7 @@
- One of the underlying items is being used in a media item
+ One or more of this item's descendants is being used in a media item
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
index 6482745133a8..8ef6f3351eef 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
@@ -2292,9 +2292,9 @@ To manage your website, simply open the Umbraco back office and start adding con
Used in DocumentsUsed in MembersUsed in Media
- One of the underlying items is being used in a media item
- One of the underlying items is being used in a content item
- One of the underlying items is being used in a member
+ One or more of this item's descendants is being used in a media item
+ One or more of this item's descendants is being used in a content item
+ One or more of this item's descendants is being used in a memberDelete Saved Search
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 4766902ca299..eb1971745bb1 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
@@ -2314,9 +2314,9 @@ To manage your website, simply open the Umbraco back office and start adding con
Used in DocumentsUsed in MembersUsed in Media
- One of the underlying items is being used in a media item
- One of the underlying items is being used in a content item
- One of the underlying items is being used in a member
+ One or more of this item's descendants is being used in a media item
+ One or more of this item's descendants is being used in a content item
+ One or more of this item's descendants is being used in a memberDelete Saved Search
From 5fb651d96405c22b463dcea27699963e8638f415 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Sat, 24 Oct 2020 17:42:54 +0200
Subject: [PATCH 24/80] Added unit test for relation service method
GetPagedParentEntitiesByChildIds
---
.../Services/RelationServiceTests.cs | 53 +++++++++++++++++++
1 file changed, 53 insertions(+)
diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs
index 06de405cecb9..aeeaf059cd4f 100644
--- a/src/Umbraco.Tests/Services/RelationServiceTests.cs
+++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using System.Web.WebSockets;
+using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Models;
@@ -190,6 +192,57 @@ public void Update_Bulk_Relations()
Assert.IsTrue(newRelations.All(x => x.UpdateDate == newDate));
}
+ [Test]
+ public void Return_List_Of_Parent_Entities_When_Getting_By_Multiple_Child_Ids()
+ {
+ // Create content
+ var createdContent = new List();
+ var contentType = MockedContentTypes.CreateBasicContentType("blah");
+ ServiceContext.ContentTypeService.Save(contentType);
+ for (int i = 0; i < 10; i++)
+ {
+ var c1 = MockedContent.CreateBasicContent(contentType);
+ ServiceContext.ContentService.Save(c1);
+ createdContent.Add(c1);
+ }
+
+ //Create media
+ var createdMedia = new List();
+ var imageType = MockedContentTypes.CreateImageMediaType("myImage");
+ ServiceContext.MediaTypeService.Save(imageType);
+ for (int i = 0; i < 10; i++)
+ {
+ var c1 = MockedMedia.CreateMediaImage(imageType, -1);
+ ServiceContext.MediaService.Save(c1);
+ createdMedia.Add(c1);
+ }
+
+ var relType = ServiceContext.RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias);
+
+ // Relate content to media
+ foreach (var content in createdContent)
+ foreach (var media in createdMedia)
+ ServiceContext.RelationService.Relate(content.Id, media.Id, relType);
+
+ var mediaItemIds = createdMedia.Select(x => x.Id).ToArray();
+
+ var paged = ServiceContext.RelationService.GetPagedParentEntitiesByChildIds(mediaItemIds, 0, 6,
+ out var totalRecs, new[] {Constants.Conventions.RelationTypes.RelatedMediaAlias}).ToList();
+
+ Assert.AreEqual(10, totalRecs);
+ Assert.AreEqual(6, paged.Count);
+
+ //next page
+ paged.AddRange(ServiceContext.RelationService.GetPagedParentEntitiesByChildIds(mediaItemIds, 1, 6,
+ out totalRecs, new[] { Constants.Conventions.RelationTypes.RelatedMediaAlias }));
+
+ Assert.AreEqual(10, totalRecs);
+ Assert.AreEqual(10, paged.Count);
+
+ Assert.IsTrue(createdContent.Select(x => x.Id).ContainsAll(paged.Select(x => x.Id)));
+ }
+
+
private IRelation CreateAndSaveRelation(string name, string alias)
{
var rs = ServiceContext.RelationService;
From 960f0b1f24804133e2439368201ff62618e55caa Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs
Date: Mon, 26 Oct 2020 16:29:46 +0100
Subject: [PATCH 25/80] Fix language xml
---
src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 af4b094e6e3f..b4786586c929 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
@@ -2317,7 +2317,7 @@ To manage your website, simply open the Umbraco back office and start adding con
One or more of this item's descendants is being used in a media itemOne or more of this item's descendants is being used in a content itemOne or more of this item's descendants is being used in a member
- The current item or one or more of this item's descendants is being used. Deleting this item can lead to broken links on your website. tem's descendants is being used in a content item
+ The current item or one or more of this item's descendants is being used. Deleting this item can lead to broken links on your website.Delete Saved Search
From 97d20baecb07dbac6436b915333540dbc44d3995 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Wed, 20 Jan 2021 15:30:41 +0100
Subject: [PATCH 26/80] skipValidation for content save
---
.../components/content/edit.controller.js | 22 ++++++++++++-------
.../services/contenteditinghelper.service.js | 19 ++++++++++------
2 files changed, 26 insertions(+), 15 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
index f2dc0622c7b1..ac470642e760 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
@@ -271,7 +271,7 @@
* @param {any} app the active content app
*/
function createButtons(content) {
-
+
var isBlueprint = content.isBlueprint;
if ($scope.page.isNew && $location.path().search(/contentBlueprints/i) !== -1) {
@@ -456,7 +456,8 @@
create: $scope.page.isNew,
action: args.action,
showNotifications: args.showNotifications,
- softRedirect: true
+ softRedirect: true,
+ skipValidation: args.skipValidation
}).then(function (data) {
//success
init();
@@ -468,7 +469,9 @@
eventsService.emit("content.saved", { content: $scope.content, action: args.action });
- resetNestedFieldValiation(fieldsToRollback);
+ if($scope.contentForm.$invalid !== true) {
+ resetNestedFieldValiation(fieldsToRollback);
+ }
ensureDirtyIsSetIfAnyVariantIsDirty();
return $q.when(data);
@@ -476,7 +479,9 @@
function (err) {
syncTreeNode($scope.content, $scope.content.path);
- resetNestedFieldValiation(fieldsToRollback);
+ if($scope.contentForm.$invalid !== true) {
+ resetNestedFieldValiation(fieldsToRollback);
+ }
return $q.reject(err);
});
@@ -729,9 +734,9 @@
clearNotifications($scope.content);
// TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant
if (hasVariants($scope.content)) {
- //before we launch the dialog we want to execute all client side validations first
- if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog" })) {
+ //before we launch the dialog we want to execute all client side validations first
+ if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog", skipValidation:true, keepServerValidation:true })) {
var dialog = {
parentScope: $scope,
view: "views/content/overlays/save.html",
@@ -778,7 +783,8 @@
$scope.page.saveButtonState = "busy";
return performSave({
saveMethod: $scope.saveMethod(),
- action: "save"
+ action: "save",
+ skipValidation: true
}).then(function () {
$scope.page.saveButtonState = "success";
}, function (err) {
@@ -981,7 +987,7 @@
$scope.appChanged = function (activeApp) {
$scope.activeApp = activeApp;
-
+
_.forEach($scope.content.apps, function (app) {
app.active = false;
if (app.alias === $scope.activeApp.alias) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
index 6d41ea087d00..fd0bd3efd988 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
@@ -84,7 +84,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
//when true, the url will change but it won't actually re-route
//this is merely here for compatibility, if only the content/media/members used this service we'd prob be ok but tons of editors
//use this service unfortunately and probably packages too.
- args.softRedirect = false;
+ args.softRedirect = false;
}
@@ -93,7 +93,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
//we will use the default one for content if not specified
var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback;
- if (formHelper.submitForm({ scope: args.scope, action: args.action })) {
+ var formSubmitOptions = { scope: args.scope, action: args.action };
+ if(args.skipValidation === true) {
+ formSubmitOptions.skipValidation = true;
+ formSubmitOptions.keepServerValidation = true;
+ }
+ if (formHelper.submitForm(formSubmitOptions)) {
return args.saveMethod(args.content, args.create, fileManager.getFiles(), args.showNotifications)
.then(function (data) {
@@ -298,7 +303,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
}
// if publishing is allowed also allow schedule publish
- // we add this manually becuase it doesn't have a permission so it wont
+ // we add this manually becuase it doesn't have a permission so it wont
// get picked up by the loop through permissions
if (_.contains(args.content.allowedActions, "U")) {
buttons.subButtons.push(createButtonDefinition("SCHEDULE"));
@@ -622,7 +627,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
if (!args.err) {
throw "args.err cannot be null";
}
-
+
//When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors.
//Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
//Or, some strange server error
@@ -640,7 +645,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) {
// If we are not redirecting it's because this is not newly created content, else in some cases we are
- // soft-redirecting which means the URL will change but the route wont (i.e. creating content).
+ // soft-redirecting which means the URL will change but the route wont (i.e. creating content).
// In this case we need to detect what properties have changed and re-bind them with the server data.
if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
@@ -687,7 +692,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id, args.softRedirect) || args.softRedirect) {
// If we are not redirecting it's because this is not newly created content, else in some cases we are
- // soft-redirecting which means the URL will change but the route wont (i.e. creating content).
+ // soft-redirecting which means the URL will change but the route wont (i.e. creating content).
// In this case we need to detect what properties have changed and re-bind them with the server data.
if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
@@ -723,7 +728,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
navigationService.setSoftRedirect();
}
//change to new path
- $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id);
+ $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id);
//don't add a browser history for this
$location.replace();
return true;
From 3185a285d93a2835044b0cdc7b95301dd2929fa0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Mon, 1 Feb 2021 11:56:51 +0100
Subject: [PATCH 27/80] Correcting merge
---
.../common/directives/components/content/edit.controller.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
index c3e23e4cfda1..fb70143fa517 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
@@ -481,13 +481,14 @@
syncTreeNode($scope.content, $scope.content.path);
+ if($scope.contentForm.$invalid !== true) {
+ resetNestedFieldValiation(fieldsToRollback);
+ }
if (err.status === 400 && err.data) {
// content was saved but is invalid.
eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: false });
}
- resetNestedFieldValiation(fieldsToRollback);
-
return $q.reject(err);
});
}
From d2a26bf4da986e712a8b18fadf4e142ac768b082 Mon Sep 17 00:00:00 2001
From: Elitsa Marinovska
Date: Thu, 25 Mar 2021 11:49:38 +0100
Subject: [PATCH 28/80] Caching RecycleBinSmells
---
src/Umbraco.Core/Cache/CacheKeys.cs | 3 +++
.../Cache/ContentCacheRefresher.cs | 1 +
src/Umbraco.Web/Cache/MediaCacheRefresher.cs | 1 +
.../Trees/ContentTreeController.cs | 2 ++
.../Trees/ContentTreeControllerBase.cs | 25 +++++++++++++++++--
src/Umbraco.Web/Trees/MediaTreeController.cs | 1 +
6 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs
index 0e9a9a386295..642bef6d0c72 100644
--- a/src/Umbraco.Core/Cache/CacheKeys.cs
+++ b/src/Umbraco.Core/Cache/CacheKeys.cs
@@ -17,5 +17,8 @@ public static class CacheKeys
public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes";
public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths";
public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths";
+
+ public const string ContentRecycleBinCacheKey = "recycleBin_content";
+ public const string MediaRecycleBinCacheKey = "recycleBin_media";
}
}
diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs
index 6abad820c944..5e8bd83c5dc7 100644
--- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs
@@ -46,6 +46,7 @@ public ContentCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publ
public override void Refresh(JsonPayload[] payloads)
{
AppCaches.RuntimeCache.ClearOfType();
+ AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey);
var idsRemoved = new HashSet();
var isolatedCache = AppCaches.IsolatedCaches.GetOrCreate();
diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs
index 1f54b62c5b09..b0845f2a9a74 100644
--- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs
@@ -48,6 +48,7 @@ public override void Refresh(JsonPayload[] payloads)
if (anythingChanged)
{
Current.AppCaches.ClearPartialViewCache();
+ AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey);
var mediaCache = AppCaches.IsolatedCaches.Get();
diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs
index d82166b9a3a6..3a4033e72479 100644
--- a/src/Umbraco.Web/Trees/ContentTreeController.cs
+++ b/src/Umbraco.Web/Trees/ContentTreeController.cs
@@ -44,6 +44,8 @@ public class ContentTreeController : ContentTreeControllerBase, ISearchableTree
protected override bool RecycleBinSmells => Services.ContentService.RecycleBinSmells();
+ public override string RecycleBinSmellsCacheKey => CacheKeys.ContentRecycleBinCacheKey;
+
private int[] _userStartNodes;
protected override int[] UserStartNodes
diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
index 57c702759812..c2a9019544d1 100644
--- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
+++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
@@ -26,9 +26,11 @@ namespace Umbraco.Web.Trees
{
public abstract class ContentTreeControllerBase : TreeController
{
+ private readonly AppCaches _appCaches;
protected ContentTreeControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
{
+ _appCaches = appCaches;
}
protected ContentTreeControllerBase()
@@ -148,6 +150,11 @@ private void GetUserStartNodes(out int[] startNodeIds, out string[] startNodePat
///
protected abstract bool RecycleBinSmells { get; }
+ ///
+ /// Gets the name of the recycle bin cache key.
+ ///
+ public abstract string RecycleBinSmellsCacheKey { get; }
+
///
/// Returns the user's start node for this tree
///
@@ -327,15 +334,29 @@ protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCol
//and for some reason when there are no dashboards, this parameter is missing
if (IsDialog(queryStrings) == false && id == Constants.System.RootString && queryStrings.HasKey("application"))
{
+ var cache = _appCaches.RuntimeCache;
+
+ var hasChildren = cache.GetCacheItem(RecycleBinSmellsCacheKey);
+ bool recycleBinSmells;
+
+ if (!(hasChildren is null))
+ {
+ recycleBinSmells = (bool) hasChildren;
+ }
+ else
+ {
+ recycleBinSmells = RecycleBinSmells;
+ cache.InsertCacheItem(RecycleBinSmellsCacheKey, () => recycleBinSmells);
+ }
+
nodes.Add(CreateTreeNode(
RecycleBinId.ToInvariantString(),
id,
queryStrings,
Services.TextService.Localize("general/recycleBin"),
"icon-trash",
- RecycleBinSmells,
+ recycleBinSmells,
queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/recyclebin"));
-
}
return nodes;
diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs
index 43b5a83282fc..93e1fc22b68c 100644
--- a/src/Umbraco.Web/Trees/MediaTreeController.cs
+++ b/src/Umbraco.Web/Trees/MediaTreeController.cs
@@ -47,6 +47,7 @@ public MediaTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings glo
protected override int RecycleBinId => Constants.System.RecycleBinMedia;
protected override bool RecycleBinSmells => Services.MediaService.RecycleBinSmells();
+ public override string RecycleBinSmellsCacheKey => CacheKeys.MediaRecycleBinCacheKey;
private int[] _userStartNodes;
protected override int[] UserStartNodes
From 19c17836aaae59b23428cadc190de927c6bc81c7 Mon Sep 17 00:00:00 2001
From: Elitsa Marinovska
Date: Thu, 25 Mar 2021 12:54:46 +0100
Subject: [PATCH 29/80] Adding recycle bin smells caches at repository level
---
.../Repositories/IDocumentRepository.cs | 5 ++++
.../Repositories/IMediaRepository.cs | 1 +
.../Implement/DocumentRepository.cs | 21 +++++++++++++++++
.../Repositories/Implement/MediaRepository.cs | 23 +++++++++++++++++++
.../Services/ContentServiceExtensions.cs | 20 ----------------
src/Umbraco.Core/Services/IContentService.cs | 5 ++++
src/Umbraco.Core/Services/IMediaService.cs | 5 ++++
.../Services/Implement/ContentService.cs | 9 ++++++++
.../Services/Implement/MediaService.cs | 9 ++++++++
.../Trees/ContentTreeController.cs | 2 --
.../Trees/ContentTreeControllerBase.cs | 22 +-----------------
src/Umbraco.Web/Trees/MediaTreeController.cs | 1 -
12 files changed, 79 insertions(+), 44 deletions(-)
diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs
index 0971b2047a7c..6746d6a4295f 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs
@@ -73,5 +73,10 @@ public interface IDocumentRepository : IContentRepository, IReadR
///
///
void AddOrUpdatePermissions(ContentPermissionSet permission);
+
+ ///
+ /// Returns true if there is any content in the recycle bin
+ ///
+ bool RecycleBinSmells();
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs
index d4ec08a0dfe0..6f36102a4002 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs
@@ -6,5 +6,6 @@ namespace Umbraco.Core.Persistence.Repositories
public interface IMediaRepository : IContentRepository, IReadRepository
{
IMedia GetMediaByPath(string mediaPath);
+ bool RecycleBinSmells();
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
index 09d41a49a099..43034560964b 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -912,6 +912,27 @@ public bool IsPathPublished(IContent content)
public override int RecycleBinId => Constants.System.RecycleBinContent;
+ public bool RecycleBinSmells()
+ {
+ var cache = _appCaches.RuntimeCache;
+ var cacheKey = CacheKeys.ContentRecycleBinCacheKey;
+
+ var hasChildren = cache.GetCacheItem(cacheKey);
+ bool recycleBinSmells;
+
+ if (!(hasChildren is null))
+ {
+ recycleBinSmells = (bool) hasChildren;
+ }
+ else
+ {
+ recycleBinSmells = CountChildren(Constants.System.RecycleBinContent) > 0;
+ cache.InsertCacheItem(cacheKey, () => recycleBinSmells);
+ }
+
+ return recycleBinSmells;
+ }
+
#endregion
#region Read Repository implementation for Guid keys
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
index 02bef366cbf5..b8b891b251fd 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
@@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
///
internal class MediaRepository : ContentRepositoryBase, IMediaRepository
{
+ private readonly AppCaches _cache;
private readonly IMediaTypeRepository _mediaTypeRepository;
private readonly ITagRepository _tagRepository;
private readonly MediaByGuidReadRepository _mediaByGuidReadRepository;
@@ -32,6 +33,7 @@ public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger lo
Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories)
: base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories)
{
+ _cache = cache;
_mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository));
_tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository));
_mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, logger);
@@ -369,6 +371,27 @@ protected override void PersistDeletedItem(IMedia entity)
public override int RecycleBinId => Constants.System.RecycleBinMedia;
+ public bool RecycleBinSmells()
+ {
+ var cache = _cache.RuntimeCache;
+ var cacheKey = CacheKeys.MediaRecycleBinCacheKey;
+
+ var hasChildren = cache.GetCacheItem(cacheKey);
+ bool recycleBinSmells;
+
+ if (!(hasChildren is null))
+ {
+ recycleBinSmells = (bool) hasChildren;
+ }
+ else
+ {
+ recycleBinSmells = CountChildren(Constants.System.RecycleBinMedia) > 0;
+ cache.InsertCacheItem(cacheKey, () => recycleBinSmells);
+ }
+
+ return recycleBinSmells;
+ }
+
#endregion
#region Read Repository implementation for Guid keys
diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
index 1d980b036b07..6ca894e16082 100644
--- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
@@ -89,25 +89,5 @@ public static void RemoveContentPermissions(this IContentService contentService,
{
contentService.SetPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection()));
}
-
- ///
- /// Returns true if there is any content in the recycle bin
- ///
- ///
- ///
- public static bool RecycleBinSmells(this IContentService contentService)
- {
- return contentService.CountChildren(Constants.System.RecycleBinContent) > 0;
- }
-
- ///
- /// Returns true if there is any media in the recycle bin
- ///
- ///
- ///
- public static bool RecycleBinSmells(this IMediaService mediaService)
- {
- return mediaService.CountChildren(Constants.System.RecycleBinMedia) > 0;
- }
}
}
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index 58279fb4daf4..c2915005334e 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -326,6 +326,11 @@ IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int
/// Optional Id of the User emptying the Recycle Bin
OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
+ ///
+ /// Returns true if there is any content in the recycle bin
+ ///
+ bool RecycleBinSmells();
+
///
/// Sorts documents.
///
diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs
index 3fecb2003565..5bbfb76735b3 100644
--- a/src/Umbraco.Core/Services/IMediaService.cs
+++ b/src/Umbraco.Core/Services/IMediaService.cs
@@ -172,6 +172,11 @@ IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out
/// Optional Id of the User emptying the Recycle Bin
OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
+ ///
+ /// Returns true if there is any media in the recycle bin
+ ///
+ bool RecycleBinSmells();
+
///
/// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin.
///
diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs
index a809b83f23a8..82b14dd45c7e 100644
--- a/src/Umbraco.Core/Services/Implement/ContentService.cs
+++ b/src/Umbraco.Core/Services/Implement/ContentService.cs
@@ -2118,6 +2118,15 @@ public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUser
return OperationResult.Succeed(evtMsgs);
}
+ public bool RecycleBinSmells()
+ {
+ using (var scope = ScopeProvider.CreateScope(autoComplete: true))
+ {
+ scope.ReadLock(Constants.Locks.ContentTree);
+ return _documentRepository.RecycleBinSmells();
+ }
+ }
+
#endregion
#region Others
diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs
index ac9c83458ddc..1ce0eb8bfdb9 100644
--- a/src/Umbraco.Core/Services/Implement/MediaService.cs
+++ b/src/Umbraco.Core/Services/Implement/MediaService.cs
@@ -1088,6 +1088,15 @@ public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUser
return OperationResult.Succeed(evtMsgs);
}
+ public bool RecycleBinSmells()
+ {
+ using (var scope = ScopeProvider.CreateScope(autoComplete: true))
+ {
+ scope.ReadLock(Constants.Locks.MediaTree);
+ return _mediaRepository.RecycleBinSmells();
+ }
+ }
+
#endregion
#region Others
diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs
index 3a4033e72479..d82166b9a3a6 100644
--- a/src/Umbraco.Web/Trees/ContentTreeController.cs
+++ b/src/Umbraco.Web/Trees/ContentTreeController.cs
@@ -44,8 +44,6 @@ public class ContentTreeController : ContentTreeControllerBase, ISearchableTree
protected override bool RecycleBinSmells => Services.ContentService.RecycleBinSmells();
- public override string RecycleBinSmellsCacheKey => CacheKeys.ContentRecycleBinCacheKey;
-
private int[] _userStartNodes;
protected override int[] UserStartNodes
diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
index c2a9019544d1..1b47eaba554b 100644
--- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
+++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs
@@ -150,11 +150,6 @@ private void GetUserStartNodes(out int[] startNodeIds, out string[] startNodePat
///
protected abstract bool RecycleBinSmells { get; }
- ///
- /// Gets the name of the recycle bin cache key.
- ///
- public abstract string RecycleBinSmellsCacheKey { get; }
-
///
/// Returns the user's start node for this tree
///
@@ -334,28 +329,13 @@ protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCol
//and for some reason when there are no dashboards, this parameter is missing
if (IsDialog(queryStrings) == false && id == Constants.System.RootString && queryStrings.HasKey("application"))
{
- var cache = _appCaches.RuntimeCache;
-
- var hasChildren = cache.GetCacheItem(RecycleBinSmellsCacheKey);
- bool recycleBinSmells;
-
- if (!(hasChildren is null))
- {
- recycleBinSmells = (bool) hasChildren;
- }
- else
- {
- recycleBinSmells = RecycleBinSmells;
- cache.InsertCacheItem(RecycleBinSmellsCacheKey, () => recycleBinSmells);
- }
-
nodes.Add(CreateTreeNode(
RecycleBinId.ToInvariantString(),
id,
queryStrings,
Services.TextService.Localize("general/recycleBin"),
"icon-trash",
- recycleBinSmells,
+ RecycleBinSmells,
queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/recyclebin"));
}
diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs
index 93e1fc22b68c..43b5a83282fc 100644
--- a/src/Umbraco.Web/Trees/MediaTreeController.cs
+++ b/src/Umbraco.Web/Trees/MediaTreeController.cs
@@ -47,7 +47,6 @@ public MediaTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings glo
protected override int RecycleBinId => Constants.System.RecycleBinMedia;
protected override bool RecycleBinSmells => Services.MediaService.RecycleBinSmells();
- public override string RecycleBinSmellsCacheKey => CacheKeys.MediaRecycleBinCacheKey;
private int[] _userStartNodes;
protected override int[] UserStartNodes
From 05e9710ca679bd20e786401ede9a0f072145c168 Mon Sep 17 00:00:00 2001
From: Elitsa Marinovska
Date: Thu, 25 Mar 2021 13:29:27 +0100
Subject: [PATCH 30/80] Using an atomic operation when calling GetCacheItem - a
callback is specified when the item isn't found
---
.../Implement/DocumentRepository.cs | 16 ++--------------
.../Repositories/Implement/MediaRepository.cs | 18 +++---------------
2 files changed, 5 insertions(+), 29 deletions(-)
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
index 43034560964b..e196a8a13c22 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -916,21 +916,9 @@ public bool RecycleBinSmells()
{
var cache = _appCaches.RuntimeCache;
var cacheKey = CacheKeys.ContentRecycleBinCacheKey;
-
- var hasChildren = cache.GetCacheItem(cacheKey);
- bool recycleBinSmells;
- if (!(hasChildren is null))
- {
- recycleBinSmells = (bool) hasChildren;
- }
- else
- {
- recycleBinSmells = CountChildren(Constants.System.RecycleBinContent) > 0;
- cache.InsertCacheItem(cacheKey, () => recycleBinSmells);
- }
-
- return recycleBinSmells;
+ // always cache either true or false
+ return cache.GetCacheItem(cacheKey, () => CountChildren(Constants.System.RecycleBinContent) > 0);
}
#endregion
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
index b8b891b251fd..c9001eed31da 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
@@ -375,21 +375,9 @@ public bool RecycleBinSmells()
{
var cache = _cache.RuntimeCache;
var cacheKey = CacheKeys.MediaRecycleBinCacheKey;
-
- var hasChildren = cache.GetCacheItem(cacheKey);
- bool recycleBinSmells;
- if (!(hasChildren is null))
- {
- recycleBinSmells = (bool) hasChildren;
- }
- else
- {
- recycleBinSmells = CountChildren(Constants.System.RecycleBinMedia) > 0;
- cache.InsertCacheItem(cacheKey, () => recycleBinSmells);
- }
-
- return recycleBinSmells;
+ // always cache either true or false
+ return cache.GetCacheItem(cacheKey, () => CountChildren(Constants.System.RecycleBinMedia) > 0);
}
#endregion
@@ -523,7 +511,7 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac
var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId));
if (cached != null && cached.VersionId == dto.ContentVersionDto.Id)
{
- content[i] = (Models.Media) cached;
+ content[i] = (Models.Media)cached;
continue;
}
}
From 8d8fb2d15c087943a5b5fb5bb8da7d42f48e550a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Mon, 12 Apr 2021 12:27:12 +0200
Subject: [PATCH 31/80] =?UTF-8?q?Dont=20show=20block=20catalogue=20if=20on?=
=?UTF-8?q?ly=20one=20block=20is=20available.=20Enable=20ente=E2=80=A6=20(?=
=?UTF-8?q?#9575)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Dont show block catalogue if only one block is available. Enable entering clipboard directly.
* corrected button states
* jump clipboard icon when adding items to the clipboard.
* fix merge issue
* add missing eventsService
* correcting missing parts from Merge
Co-authored-by: Niels Lyngsø
Co-authored-by: Mads Rasmussen
---
.../blockpicker/blockpicker.controller.js | 11 +-
.../umb-block-list-property-editor.html | 32 +++--
.../umb-block-list-property-editor.less | 110 ++++++++++++++----
.../umbBlockListPropertyEditor.component.js | 97 ++++++++++++---
src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 3 +
src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +
.../Umbraco/config/lang/en_us.xml | 2 +
7 files changed, 207 insertions(+), 50 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js
index 90803a376578..5e6613c0f447 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js
@@ -17,7 +17,6 @@ angular.module("umbraco")
"alias": "empty",
"name": data[0],
"icon": "icon-add",
- "active": true,
"view": ""
},
{
@@ -28,10 +27,16 @@ angular.module("umbraco")
"disabled": vm.model.clipboardItems.length === 0
}];
- vm.activeTab = vm.navigation[0];
+ if (vm.model.openClipboard === true) {
+ vm.activeTab = vm.navigation[1];
+ } else {
+ vm.activeTab = vm.navigation[0];
+ }
+
+ vm.activeTab.active = true;
}
);
-
+
vm.onNavigationChanged = function (tab) {
vm.activeTab.active = false;
vm.activeTab = tab;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html
index 9726daf5e650..87c0026d568f 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html
@@ -10,7 +10,7 @@
You need to add at least {{model.config.minNumber}} items
-
+
You can only have {{model.config.maxNumber}} items selected
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js
index 30b6fc4c8fbe..af1dea167a52 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js
@@ -161,7 +161,14 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM
else {
$scope.model.value = null;
}
- angularHelper.getCurrentForm($scope).$setDirty();
+
+ setDirty();
+ }
+
+ function setDirty() {
+ if ($scope.datePickerForm) {
+ $scope.datePickerForm.datepicker.$setDirty();
+ }
}
/** Sets the value of the date picker control adn associated viewModel objects based on the model value */
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
index 70c74d0391cb..4df8f7e596d2 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
@@ -31,7 +31,7 @@ angular.module('umbraco')
};
//set form to dirty to track changes
- $scope.imageCropperForm.$setDirty();
+ setDirty();
}
/**
@@ -67,7 +67,13 @@ angular.module('umbraco')
function onFileSelected(value, files) {
setModelValueWithSrc(value);
//set form to dirty to track changes
- $scope.imageCropperForm.$setDirty();
+ setDirty();
+ }
+
+ function setDirty() {
+ if ($scope.imageCropperForm) {
+ $scope.imageCropperForm.modelValue.$setDirty();
+ }
}
function imageLoaded(isCroppable, hasDimensions) {
@@ -84,7 +90,7 @@ angular.module('umbraco')
if (files && files[0]) {
$scope.imageSrc = files[0].fileSrc;
//set form to dirty to track changes
- $scope.imageCropperForm.$setDirty();
+ setDirty();
}
}
@@ -138,7 +144,7 @@ angular.module('umbraco')
$scope.currentPoint = null;
//set form to dirty to track changes
- $scope.imageCropperForm.$setDirty();
+ setDirty();
}
else {
// we have a crop open already - close the crop (this will discard any changes made)
@@ -168,7 +174,7 @@ angular.module('umbraco')
$scope.close();
//set form to dirty to track changes
- $scope.imageCropperForm.$setDirty();
+ setDirty();
};
function reset() {
@@ -201,7 +207,7 @@ angular.module('umbraco')
}
//set form to dirty to track changes
- $scope.imageCropperForm.$setDirty();
+ setDirty();
};
function isCustomCrop(crop) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html
index 84ddf7ee3b92..241d61660eae 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html
@@ -2,6 +2,9 @@
ng-controller="Umbraco.PropertyEditors.ImageCropperController">
+
+
+
-
-
+
+
-
+
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js
index f3a57224e21f..ca46f30bb7ed 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js
@@ -1,7 +1,7 @@
//this controller simply tells the dialogs service to open a mediaPicker window
//with a specified callback, this callback will receive an object with a selection on it
angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController",
- function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, angularHelper, overlayService) {
+ function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService) {
var vm = this;
@@ -13,6 +13,9 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
vm.editItem = editItem;
vm.showAdd = showAdd;
+ vm.mediaItems = [];
+ let selectedIds = [];
+
//check the pre-values for multi-picker
var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false;
var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false;
@@ -22,9 +25,6 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
$scope.allowAddMedia = false;
function setupViewModel() {
- $scope.mediaItems = [];
- $scope.ids = [];
-
$scope.isMultiPicker = multiPicker;
if ($scope.model.value) {
@@ -77,12 +77,12 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
media.thumbnail = mediaHelper.resolveFileFromEntity(media, true);
}
- $scope.mediaItems.push(media);
+ vm.mediaItems.push(media);
if ($scope.model.config.idType === "udi") {
- $scope.ids.push(media.udi);
+ selectedIds.push(media.udi);
} else {
- $scope.ids.push(media.id);
+ selectedIds.push(media.id);
}
});
@@ -92,12 +92,14 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
}
function sync() {
- $scope.model.value = $scope.ids.join();
- removeAllEntriesAction.isDisabled = $scope.ids.length === 0;
+ $scope.model.value = selectedIds.join();
+ removeAllEntriesAction.isDisabled = selectedIds.length === 0;
}
function setDirty() {
- angularHelper.getCurrentForm($scope).$setDirty();
+ if (vm.modelValueForm) {
+ vm.modelValueForm.modelValue.$setDirty();
+ }
}
function reloadUpdatedMediaItems(updatedMediaNodes) {
@@ -105,7 +107,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
// reload. We only reload the images that is already picked but has been updated.
// We have to get the entities from the server because the media
// can be edited without being selected
- $scope.mediaItems.forEach(media => {
+ vm.mediaItems.forEach(media => {
if (updatedMediaNodes.indexOf(media.udi) !== -1) {
media.loading = true;
entityResource.getById(media.udi, "Media")
@@ -155,8 +157,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
}
function remove(index) {
- $scope.mediaItems.splice(index, 1);
- $scope.ids.splice(index, 1);
+ vm.mediaItems.splice(index, 1);
+ selectedIds.splice(index, 1);
sync();
setDirty();
}
@@ -174,7 +176,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
.then(function (mediaEntity) {
// if an image is selecting more than once
// we need to update all the media items
- $scope.mediaItems.forEach(media => {
+ vm.mediaItems.forEach(media => {
if (media.id === model.mediaNode.id) {
angular.extend(media, mediaEntity);
media.thumbnail = mediaHelper.resolveFileFromEntity(media, true);
@@ -208,13 +210,13 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
media.thumbnail = mediaHelper.resolveFileFromEntity(media, true);
}
- $scope.mediaItems.push(media);
+ vm.mediaItems.push(media);
if ($scope.model.config.idType === "udi") {
- $scope.ids.push(media.udi);
+ selectedIds.push(media.udi);
}
else {
- $scope.ids.push(media.id);
+ selectedIds.push(media.id);
}
});
@@ -250,8 +252,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
overlayService.close();
},
submit: function () {
- $scope.mediaItems.length = 0;// AngularJS way to empty the array.
- $scope.ids.length = 0;// AngularJS way to empty the array.
+ vm.mediaItems.length = 0;// AngularJS way to empty the array.
+ selectedIds.length = 0;// AngularJS way to empty the array.
sync();
setDirty();
overlayService.close();
@@ -291,7 +293,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
// TODO: Instead of doing this with a timeout would be better to use a watch like we do in the
// content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the
// watch do all the rest.
- $scope.ids = $scope.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id);
+ selectedIds = vm.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id);
sync();
});
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html
index c09d7c7613f7..22a683fa4967 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html
@@ -1,11 +1,11 @@
-
-
+
+
-
-
+
+
@@ -46,13 +46,15 @@
-
+
-
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js
index 73def3cc653b..5362cb1f1006 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js
@@ -18,6 +18,12 @@ function memberGroupPicker($scope, editorService, memberGroupResource){
});
}
+ function setDirty() {
+ if ($scope.modelValueForm) {
+ $scope.modelValueForm.modelValue.$setDirty();
+ }
+ }
+
$scope.openMemberGroupPicker = function() {
var memberGroupPicker = {
multiPicker: true,
@@ -32,6 +38,7 @@ function memberGroupPicker($scope, editorService, memberGroupResource){
if (newGroupIds && newGroupIds.length) {
memberGroupResource.getByIds(newGroupIds).then(function (groups) {
$scope.renderModel = _.union($scope.renderModel, groups);
+ setDirty();
editorService.close();
});
}
@@ -47,10 +54,13 @@ function memberGroupPicker($scope, editorService, memberGroupResource){
editorService.memberGroupPicker(memberGroupPicker);
};
- $scope.remove =function(index){
+ // TODO: I don't believe this is used
+ $scope.remove = function(index){
$scope.renderModel.splice(index, 1);
+ setDirty();
};
+ // TODO: I don't believe this is used
$scope.add = function (item) {
var currIds = _.map($scope.renderModel, function (i) {
return i.id;
@@ -58,11 +68,14 @@ function memberGroupPicker($scope, editorService, memberGroupResource){
if (currIds.indexOf(item) < 0) {
$scope.renderModel.push({ name: item, id: item, icon: 'icon-users' });
+ setDirty();
}
};
+ // TODO: I don't believe this is used
$scope.clear = function() {
$scope.renderModel = [];
+ setDirty();
};
function renderModelIds() {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html
index b1cafafb0d84..5a0788149e38 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html
@@ -1,21 +1,22 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js
index f41f22a1a9c9..88d112e2d638 100644
--- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js
@@ -2,29 +2,29 @@
* @ngdoc controller
* @name Umbraco.Editors.Media.EditController
* @function
- *
+ *
* @description
* The controller for the media editor
*/
-function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource,
- entityResource, navigationService, notificationsService, localizationService,
- serverValidationManager, contentEditingHelper, fileManager, formHelper,
+function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource,
+ entityResource, navigationService, notificationsService, localizationService,
+ serverValidationManager, contentEditingHelper, fileManager, formHelper,
editorState, umbRequestHelper, eventsService) {
-
+
var evts = [];
var nodeId = null;
var create = false;
var infiniteMode = $scope.model && $scope.model.infiniteMode;
- // when opening the editor through infinite editing get the
+ // when opening the editor through infinite editing get the
// node id from the model instead of the route param
if(infiniteMode && $scope.model.id) {
nodeId = $scope.model.id;
} else {
nodeId = $routeParams.id;
}
-
- // when opening the editor through infinite editing get the
+
+ // when opening the editor through infinite editing get the
// create option from the model instead of the route param
if(infiniteMode) {
create = $scope.model.create;
@@ -72,22 +72,22 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
}
function init() {
-
+
var content = $scope.content;
-
+
// we need to check whether an app is present in the current data, if not we will present the default app.
var isAppPresent = false;
-
+
// on first init, we dont have any apps. but if we are re-initializing, we do, but ...
if ($scope.app) {
-
+
// lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.)
content.apps.forEach(app => {
if (app === $scope.app) {
isAppPresent = true;
}
});
-
+
// if we did reload our DocType, but still have the same app we will try to find it by the alias.
if (isAppPresent === false) {
content.apps.forEach(app => {
@@ -98,9 +98,9 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
}
});
}
-
+
}
-
+
// if we still dont have a app, lets show the first one:
if (isAppPresent === false) {
content.apps[0].active = true;
@@ -108,16 +108,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
}
editorState.set($scope.content);
-
+
bindEvents();
}
-
+
function bindEvents() {
//bindEvents can be called more than once and we don't want to have multiple bound events
for (var e in evts) {
eventsService.unsubscribe(evts[e]);
}
-
+
evts.push(eventsService.on("editors.mediaType.saved", function(name, args) {
// if this media item uses the updated media type we need to reload the media item
if(args && args.mediaType && args.mediaType.key === $scope.content.contentType.key) {
@@ -131,7 +131,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
}));
}
$scope.page.submitButtonLabelKey = "buttons_save";
-
+
/** Syncs the content item to it's tree node - this occurs on first load and after saving */
function syncTreeNode(content, path, initialLoad) {
@@ -149,7 +149,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
//it's a child item, just sync the ui node to the parent
navigationService.syncTree({ tree: "media", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true });
- //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
+ //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
// from the server so that we can load in the actions menu.
umbRequestHelper.resourcePromise(
$http.get(content.treeNodeUrl),
@@ -176,7 +176,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
$scope.save = function () {
if (formHelper.submitForm({ scope: $scope })) {
-
+
$scope.page.saveButtonState = "busy";
mediaResource.save($scope.content, create, fileManager.getFiles())
@@ -200,12 +200,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
editorState.set($scope.content);
syncTreeNode($scope.content, data.path);
-
+
$scope.page.saveButtonState = "success";
init();
}
+ eventsService.emit("editors.media.saved", {media: data});
+
+ return data;
+
}, function(err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
@@ -213,16 +217,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
err: err,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data)
});
-
+
editorState.set($scope.content);
$scope.page.saveButtonState = "error";
});
}
else {
- showValidationNotification();
+ showValidationNotification();
}
-
+
};
function loadMedia() {
@@ -231,7 +235,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
.then(function (data) {
$scope.content = data;
-
+
if (data.isChildOfListView && data.trashed === false) {
$scope.page.listViewPath = ($routeParams.page)
? "/media/media/edit/" + data.parentId + "?page=" + $routeParams.page
@@ -247,9 +251,9 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
serverValidationManager.notifyAndClearAllSubscriptions();
if(!infiniteMode) {
- syncTreeNode($scope.content, data.path, true);
+ syncTreeNode($scope.content, data.path, true);
}
-
+
if ($scope.content.parentId && $scope.content.parentId !== -1 && $scope.content.parentId !== -21) {
//We fetch all ancestors of the node to generate the footer breadcrump navigation
entityResource.getAncestors(nodeId, "media")
@@ -279,7 +283,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
$scope.appChanged = function (app) {
$scope.app = app;
-
+
// setup infinite mode
if(infiniteMode) {
$scope.page.submitButtonLabelKey = "buttons_saveAndClose";
@@ -296,7 +300,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat
$location.path($scope.page.listViewPath.split("?")[0]);
}
};
-
+
//ensure to unregister from all events!
$scope.$on('$destroy', function () {
for (var e in evts) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html
index d9d8cad9821b..6e67c947936f 100644
--- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html
+++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html
@@ -4,6 +4,7 @@
type="number"
ng-model="model.value.min"
placeholder="0"
+ min="0"
ng-max="model.value.max"
fix-number />
–
@@ -11,7 +12,7 @@
type="number"
ng-model="model.value.max"
placeholder="∞"
- ng-min="model.value.min"
+ ng-min="model.value.min || 0"
fix-number />
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js
index dcc9add395d8..d02e626bfa2f 100644
--- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js
@@ -99,6 +99,11 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe
eventsService.unsubscribe(evts[e]);
}
});
+
+ if ($scope.model.config.itemType) {
+ currentItemType = $scope.model.config.itemType;
+ init();
+ }
}
angular.module('umbraco').controller("Umbraco.PrevalueEditors.TreeSourceTypePickerController", TreeSourceTypePickerController);
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less
index 019a772fddfb..66ef23c7440f 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less
@@ -10,7 +10,7 @@
.umb-block-list__wrapper {
position: relative;
- max-width: 1024px;
+ .umb-property-editor--limit-width();
> .ui-sortable > .ui-sortable-helper > .umb-block-list__block > .umb-block-list__block--content > * {
box-shadow: 0px 5px 10px 0 rgba(0,0,0,.2);
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js
index c485f4bbc6e9..4f1016e68028 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js
@@ -11,11 +11,14 @@
*
*/
function fileUploadController($scope, fileManager) {
-
+
$scope.fileChanged = onFileChanged;
//declare a special method which will be called whenever the value has changed from the server
$scope.model.onValueChanged = onValueChanged;
+
+ $scope.fileExtensionsString = $scope.model.config.fileExtensions ? $scope.model.config.fileExtensions.map(x => "."+x.value).join(",") : "";
+
/**
* Called when the file selection value changes
* @param {any} value
@@ -38,12 +41,12 @@
files: []
});
}
-
+
};
angular.module("umbraco")
.controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController)
- .run(function (mediaHelper, umbRequestHelper, assetsService) {
+ .run(function (mediaHelper) {
if (mediaHelper && mediaHelper.registerFileResolver) {
//NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html
index 522278e99ec4..36509e894796 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html
@@ -4,6 +4,7 @@
property-alias="{{model.alias}}"
value="model.value"
required="model.validation.mandatory"
- on-files-selected="fileChanged(value)">
+ on-files-selected="fileChanged(value)"
+ accept-file-ext="fileExtensionsString">
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
index 4df8f7e596d2..e9d9950bdd61 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
@@ -1,6 +1,6 @@
angular.module('umbraco')
.controller("Umbraco.PropertyEditors.ImageCropperController",
- function ($scope, fileManager, $timeout) {
+ function ($scope, fileManager, $timeout, mediaHelper) {
var config = Utilities.copy($scope.model.config);
@@ -18,6 +18,8 @@ angular.module('umbraco')
//declare a special method which will be called whenever the value has changed from the server
$scope.model.onValueChanged = onValueChanged;
+ var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings;
+ $scope.acceptFileExt = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes);
/**
* Called when the umgImageGravity component updates the focal point value
* @param {any} left
@@ -150,7 +152,7 @@ angular.module('umbraco')
// we have a crop open already - close the crop (this will discard any changes made)
close();
- // the crop editor needs a digest cycle to close down properly, otherwise its state
+ // the crop editor needs a digest cycle to close down properly, otherwise its state
// is reused for the new crop... and that's really bad
$timeout(function () {
crop(targetCrop);
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html
index 241d61660eae..9dc1a3b91ad9 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html
@@ -13,11 +13,12 @@
on-files-selected="filesSelected(value, files)"
on-files-changed="filesChanged(files)"
on-init="fileUploaderInit(value, files)"
- hide-selection="true">
+ hide-selection="true"
+ accept-file-ext="acceptFileExt">
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less
new file mode 100644
index 000000000000..d02c0b055c90
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less
@@ -0,0 +1,13 @@
+.umb-mediapicker3 {
+
+ .umb-media-card-grid {
+ padding: 20px;
+ border: 1px solid @inputBorder;
+ box-sizing: border-box;
+ .umb-property-editor--limit-width();
+
+ &.--singleMode {
+ max-width: 202px;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js
new file mode 100644
index 000000000000..675381d46e3b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js
@@ -0,0 +1,431 @@
+(function () {
+ "use strict";
+
+
+ /**
+ * @ngdoc directive
+ * @name umbraco.directives.directive:umbMediaPicker3PropertyEditor
+ * @function
+ *
+ * @description
+ * The component for the Media Picker property editor.
+ */
+ angular
+ .module("umbraco")
+ .component("umbMediaPicker3PropertyEditor", {
+ templateUrl: "views/propertyeditors/MediaPicker3/umb-media-picker3-property-editor.html",
+ controller: MediaPicker3Controller,
+ controllerAs: "vm",
+ bindings: {
+ model: "="
+ },
+ require: {
+ propertyForm: "^form",
+ umbProperty: "?^umbProperty",
+ umbVariantContent: '?^^umbVariantContent',
+ umbVariantContentEditors: '?^^umbVariantContentEditors',
+ umbElementEditorContent: '?^^umbElementEditorContent'
+ }
+ });
+
+ function MediaPicker3Controller($scope, editorService, clipboardService, localizationService, overlayService, userService, entityResource) {
+
+ var unsubscribe = [];
+
+ // Property actions:
+ var copyAllMediasAction = null;
+ var removeAllMediasAction = null;
+
+ var vm = this;
+
+ vm.loading = true;
+
+ vm.supportCopy = clipboardService.isSupported();
+
+
+ vm.labels = {};
+
+ localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) {
+ vm.labels.grid_addElement = data[0];
+ vm.labels.content_createEmpty = data[1];
+ });
+
+ vm.$onInit = function() {
+
+ vm.validationLimit = vm.model.config.validationLimit || {};
+ // If single-mode we only allow 1 item as the maximum:
+ if(vm.model.config.multiple === false) {
+ vm.validationLimit.max = 1;
+ }
+ vm.model.config.crops = vm.model.config.crops || [];
+ vm.singleMode = vm.validationLimit.max === 1;
+ vm.allowedTypes = vm.model.config.filter ? vm.model.config.filter.split(",") : null;
+
+ copyAllMediasAction = {
+ labelKey: "clipboard_labelForCopyAllEntries",
+ labelTokens: [vm.model.label],
+ icon: "documents",
+ method: requestCopyAllMedias,
+ isDisabled: true
+ };
+
+ removeAllMediasAction = {
+ labelKey: 'clipboard_labelForRemoveAllEntries',
+ labelTokens: [],
+ icon: 'trash',
+ method: requestRemoveAllMedia,
+ isDisabled: true
+ };
+
+ var propertyActions = [];
+ if(vm.supportCopy) {
+ propertyActions.push(copyAllMediasAction);
+ }
+ propertyActions.push(removeAllMediasAction);
+
+ if (vm.umbProperty) {
+ vm.umbProperty.setPropertyActions(propertyActions);
+ }
+
+ if(vm.model.value === null || !Array.isArray(vm.model.value)) {
+ vm.model.value = [];
+ }
+
+ vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry));
+
+ userService.getCurrentUser().then(function (userData) {
+
+ if (!vm.model.config.startNodeId) {
+ if (vm.model.config.ignoreUserStartNodes === true) {
+ vm.model.config.startNodeId = -1;
+ vm.model.config.startNodeIsVirtual = true;
+ } else {
+ vm.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0];
+ vm.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1;
+ }
+ }
+
+ // only allow users to add and edit media if they have access to the media section
+ var hasAccessToMedia = userData.allowedSections.indexOf("media") !== -1;
+ vm.allowEdit = hasAccessToMedia;
+ vm.allowAdd = hasAccessToMedia;
+
+ vm.loading = false;
+ });
+
+ };
+
+ function setDirty() {
+ if (vm.propertyForm) {
+ vm.propertyForm.$setDirty();
+ }
+ }
+
+ vm.addMediaAt = addMediaAt;
+ function addMediaAt(createIndex, $event) {
+ var mediaPicker = {
+ startNodeId: vm.model.config.startNodeId,
+ startNodeIsVirtual: vm.model.config.startNodeIsVirtual,
+ dataTypeKey: vm.model.dataTypeKey,
+ multiPicker: vm.singleMode !== true,
+ clickPasteItem: function(item, mouseEvent) {
+
+ if (Array.isArray(item.data)) {
+ var indexIncrementor = 0;
+ item.data.forEach(function (entry) {
+ if (requestPasteFromClipboard(createIndex + indexIncrementor, entry, item.type)) {
+ indexIncrementor++;
+ }
+ });
+ } else {
+ requestPasteFromClipboard(createIndex, item.data, item.type);
+ }
+ if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) {
+ mediaPicker.close();
+ }
+ },
+ submit: function (model) {
+ editorService.close();
+
+ var indexIncrementor = 0;
+ model.selection.forEach((entry) => {
+ var mediaEntry = {};
+ mediaEntry.key = String.CreateGuid();
+ mediaEntry.mediaKey = entry.key;
+ updateMediaEntryData(mediaEntry);
+ vm.model.value.splice(createIndex + indexIncrementor, 0, mediaEntry);
+ indexIncrementor++;
+ });
+
+ setDirty();
+ },
+ close: function () {
+ editorService.close();
+ }
+ }
+
+ if(vm.model.config.filter) {
+ mediaPicker.filter = vm.model.config.filter;
+ }
+
+ mediaPicker.clickClearClipboard = function ($event) {
+ clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, vm.allowedTypes || null);
+ };
+
+ mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, vm.allowedTypes || null);
+ mediaPicker.clipboardItems.sort( (a, b) => {
+ return b.date - a.date
+ });
+
+ editorService.mediaPicker(mediaPicker);
+ }
+
+ // To be used by infinite editor. (defined here cause we need configuration from property editor)
+ function changeMediaFor(mediaEntry, onSuccess) {
+ var mediaPicker = {
+ startNodeId: vm.model.config.startNodeId,
+ startNodeIsVirtual: vm.model.config.startNodeIsVirtual,
+ dataTypeKey: vm.model.dataTypeKey,
+ multiPicker: false,
+ submit: function (model) {
+ editorService.close();
+
+ model.selection.forEach((entry) => {// only one.
+ mediaEntry.mediaKey = entry.key;
+ });
+
+ // reset focal and crops:
+ mediaEntry.crops = null;
+ mediaEntry.focalPoint = null;
+ updateMediaEntryData(mediaEntry);
+
+ if(onSuccess) {
+ onSuccess();
+ }
+ },
+ close: function () {
+ editorService.close();
+ }
+ }
+
+ if(vm.model.config.filter) {
+ mediaPicker.filter = vm.model.config.filter;
+ }
+
+ editorService.mediaPicker(mediaPicker);
+ }
+
+ function resetCrop(cropEntry) {
+ Object.assign(cropEntry, vm.model.config.crops.find( c => c.alias === cropEntry.alias));
+ cropEntry.coordinates = null;
+ setDirty();
+ }
+
+ function updateMediaEntryData(mediaEntry) {
+
+ mediaEntry.crops = mediaEntry.crops || [];
+ mediaEntry.focalPoint = mediaEntry.focalPoint || {
+ left: 0.5,
+ top: 0.5
+ };
+
+ // Copy config and only transfer coordinates.
+ var newCrops = Utilities.copy(vm.model.config.crops);
+ newCrops.forEach(crop => {
+ var oldCrop = mediaEntry.crops.filter(x => x.alias === crop.alias).shift();
+ if (oldCrop && oldCrop.height === crop.height && oldCrop.width === crop.width) {
+ crop.coordinates = oldCrop.coordinates;
+ }
+ });
+ mediaEntry.crops = newCrops;
+
+ }
+
+ vm.removeMedia = removeMedia;
+ function removeMedia(media) {
+ var index = vm.model.value.indexOf(media);
+ if(index !== -1) {
+ vm.model.value.splice(index, 1);
+ }
+ }
+ function deleteAllMedias() {
+ vm.model.value = [];
+ }
+
+ vm.activeMediaEntry = null;
+ function setActiveMedia(mediaEntryOrNull) {
+ vm.activeMediaEntry = mediaEntryOrNull;
+ }
+
+ vm.editMedia = editMedia;
+ function editMedia(mediaEntry, options, $event) {
+
+ if($event)
+ $event.stopPropagation();
+
+ options = options || {};
+
+ setActiveMedia(mediaEntry);
+
+ var documentInfo = getDocumentNameAndIcon();
+
+ // make a clone to avoid editing model directly.
+ var mediaEntryClone = Utilities.copy(mediaEntry);
+
+ var mediaEditorModel = {
+ $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing
+ $parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form)
+ createFlow: options.createFlow === true,
+ documentName: documentInfo.name,
+ mediaEntry: mediaEntryClone,
+ propertyEditor: {
+ changeMediaFor: changeMediaFor,
+ resetCrop: resetCrop
+ },
+ enableFocalPointSetter: vm.model.config.enableLocalFocalPoint || false,
+ view: "views/common/infiniteeditors/mediaEntryEditor/mediaEntryEditor.html",
+ size: "large",
+ submit: function(model) {
+ vm.model.value[vm.model.value.indexOf(mediaEntry)] = mediaEntryClone;
+ setActiveMedia(null)
+ editorService.close();
+ },
+ close: function(model) {
+ if(model.createFlow === true) {
+ // This means that the user cancelled the creation and we should remove the media item.
+ // TODO: remove new media item.
+ }
+ setActiveMedia(null)
+ editorService.close();
+ }
+ };
+
+ // open property settings editor
+ editorService.open(mediaEditorModel);
+ }
+
+ var getDocumentNameAndIcon = function() {
+ // get node name
+ var contentNodeName = "?";
+ var contentNodeIcon = null;
+ if(vm.umbVariantContent) {
+ contentNodeName = vm.umbVariantContent.editor.content.name;
+ if(vm.umbVariantContentEditors) {
+ contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(" ")[0];
+ } else if (vm.umbElementEditorContent) {
+ contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0];
+ }
+ } else if (vm.umbElementEditorContent) {
+ contentNodeName = vm.umbElementEditorContent.model.documentType.name;
+ contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0];
+ }
+
+ return {
+ name: contentNodeName,
+ icon: contentNodeIcon
+ }
+ }
+
+ var requestCopyAllMedias = function() {
+ var mediaKeys = vm.model.value.map(x => x.mediaKey)
+ entityResource.getByIds(mediaKeys, "Media").then(function (entities) {
+
+ // gather aliases
+ var aliases = entities.map(mediaEntity => mediaEntity.metaData.ContentTypeAlias);
+
+ // remove duplicate aliases
+ aliases = aliases.filter((item, index) => aliases.indexOf(item) === index);
+
+ var documentInfo = getDocumentNameAndIcon();
+
+ localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, documentInfo.name]).then(function(localizedLabel) {
+ clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, vm.model.value, localizedLabel, documentInfo.icon || "icon-thumbnail-list", vm.model.id);
+ });
+ });
+ }
+
+ vm.copyMedia = copyMedia;
+ function copyMedia(mediaEntry) {
+ entityResource.getById(mediaEntry.mediaKey, "Media").then(function (mediaEntity) {
+ clipboardService.copy(clipboardService.TYPES.MEDIA, mediaEntity.metaData.ContentTypeAlias, mediaEntry, mediaEntity.name, mediaEntity.icon, mediaEntry.key);
+ });
+ }
+ function requestPasteFromClipboard(createIndex, pasteEntry, pasteType) {
+
+ if (pasteEntry === undefined) {
+ return false;
+ }
+
+ pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType);
+
+ pasteEntry.key = String.CreateGuid();
+ updateMediaEntryData(pasteEntry);
+ vm.model.value.splice(createIndex, 0, pasteEntry);
+
+
+ return true;
+
+ }
+
+ function requestRemoveAllMedia() {
+ localizationService.localizeMany(["mediaPicker_confirmRemoveAllMediaEntryMessage", "general_remove"]).then(function (data) {
+ overlayService.confirmDelete({
+ title: data[1],
+ content: data[0],
+ close: function () {
+ overlayService.close();
+ },
+ submit: function () {
+ deleteAllMedias();
+ overlayService.close();
+ }
+ });
+ });
+ }
+
+
+ vm.sortableOptions = {
+ cursor: "grabbing",
+ handle: "umb-media-card",
+ cancel: "input,textarea,select,option",
+ classes: ".umb-media-card--dragging",
+ distance: 5,
+ tolerance: "pointer",
+ scroll: true,
+ update: function (ev, ui) {
+ setDirty();
+ }
+ };
+
+
+ function onAmountOfMediaChanged() {
+
+ // enable/disable property actions
+ if (copyAllMediasAction) {
+ copyAllMediasAction.isDisabled = vm.model.value.length === 0;
+ }
+ if (removeAllMediasAction) {
+ removeAllMediasAction.isDisabled = vm.model.value.length === 0;
+ }
+
+ // validate limits:
+ if (vm.propertyForm && vm.validationLimit) {
+
+ var isMinRequirementGood = vm.validationLimit.min === null || vm.model.value.length >= vm.validationLimit.min;
+ vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood);
+
+ var isMaxRequirementGood = vm.validationLimit.max === null || vm.model.value.length <= vm.validationLimit.max;
+ vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood);
+ }
+ }
+
+ unsubscribe.push($scope.$watch(() => vm.model.value.length, onAmountOfMediaChanged));
+
+ $scope.$on("$destroy", function () {
+ for (const subscription of unsubscribe) {
+ subscription();
+ }
+ });
+ }
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js
new file mode 100644
index 000000000000..b561784d9f06
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js
@@ -0,0 +1,18 @@
+(function () {
+ "use strict";
+
+ angular
+ .module("umbraco")
+ .controller("Umbraco.PropertyEditors.MediaPicker3PropertyEditor.CreateButtonController",
+ function Controller($scope) {
+
+ var vm = this;
+ vm.plusPosY = 0;
+
+ vm.onMouseMove = function($event) {
+ vm.plusPosY = $event.offsetY;
+ }
+
+ });
+
+})();
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
index 737181c66821..4abcdf8a40bf 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
@@ -1100,6 +1100,17 @@ Mange hilsner fra Umbraco robotten
Du har valgt et medie som er slettet eller lagt i papirkurvenDu har valgt medier som er slettede eller lagt i papirkurvenSlettet
+ Åben i mediebiblioteket
+ Skift medie
+ Nulstil medie beskæring
+ Rediger %0% på %1%
+ Annuller indsættelse?
+
+ Du har foretaget ændringer til bruge af dette media. Er du sikker på at du vil annullere?
+ Fjern?
+ Fjern brugen af alle medier?
+ Udklipsholder
+ Ikke tilladtindtast eksternt link
@@ -1845,6 +1856,7 @@ Mange hilsner fra Umbraco robotten
Kopier %0%%0% fra %1%
+ Samling af %0%Fjern alle elementerRyd udklipsholder
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
index 3f6c985a0fe5..cbb6902d744b 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
@@ -1353,6 +1353,17 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
You have picked a media item currently deleted or in the recycle binYou have picked media items currently deleted or in the recycle binTrashed
+ Open in Media Library
+ Change Media Item
+ Reset media crop
+ Edit %0% on %1%
+ Discard creation?
+
+ You have made changes to this content. Are you sure you want to discard them?
+ Remove?
+ Remove all medias?
+ Clipboard
+ Not allowedenter external link
@@ -2377,6 +2388,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Copy %0%%0% from %1%
+ Collection of %0%Remove all itemsClear clipboard
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 87b58e506364..590a24839347 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
@@ -1363,6 +1363,17 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
You have picked a media item currently deleted or in the recycle binYou have picked media items currently deleted or in the recycle binTrashed
+ Open in Media Library
+ Change Media Item
+ Reset media crop
+ Edit %0% on %1%
+ Discard creation?
+
+ You have made changes to this content. Are you sure you want to discard them?
+ Remove?
+ Remove all medias?
+ Clipboard
+ Not allowedenter external link
@@ -2396,6 +2407,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Copy %0%%0% from %1%
+ Collection of %0%Remove all itemsClear clipboard
diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs
index 8d13ccd4d79d..7160a87351ab 100644
--- a/src/Umbraco.Web/Editors/MediaController.cs
+++ b/src/Umbraco.Web/Editors/MediaController.cs
@@ -704,7 +704,32 @@ public async Task PostAddFile()
if (result.FormData["contentTypeAlias"] == Constants.Conventions.MediaTypes.AutoSelect)
{
- if (Current.Configs.Settings().Content.ImageFileTypes.Contains(ext))
+ var mediaTypes = Services.MediaTypeService.GetAll();
+ // Look up MediaTypes
+ foreach (var mediaTypeItem in mediaTypes)
+ {
+ var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == "umbracoFile");
+ if (fileProperty != null) {
+ var dataTypeKey = fileProperty.DataTypeKey;
+ var dataType = Services.DataTypeService.GetDataType(dataTypeKey);
+
+ if (dataType != null && dataType.Configuration is IFileExtensionsConfig fileExtensionsConfig) {
+ var fileExtensions = fileExtensionsConfig.FileExtensions;
+ if (fileExtensions != null)
+ {
+ if (fileExtensions.Where(x => x.Value == ext).Count() != 0)
+ {
+ mediaType = mediaTypeItem.Alias;
+ break;
+ }
+ }
+ }
+ }
+
+ }
+
+ // If media type is still File then let's check if it's an image.
+ if (mediaType == Constants.Conventions.MediaTypes.File && Current.Configs.Settings().Content.ImageFileTypes.Contains(ext))
{
mediaType = Constants.Conventions.MediaTypes.Image;
}
diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs
index f39b267e18b0..766cb1e99f91 100644
--- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs
+++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs
@@ -28,6 +28,11 @@ public static string GetCropUrl(this IPublishedContent mediaItem, string cropAli
return mediaItem.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true);
}
+ public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator, ImageCropperValue imageCropperValue)
+ {
+ return mediaItem.Url().GetCropUrl(imageUrlGenerator, imageCropperValue, cropAlias: cropAlias, useCropDimensions: true);
+ }
+
///
/// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item.
///
@@ -375,5 +380,11 @@ public static string GetCropUrl(
return imageUrlGenerator.GetImageUrl(options);
}
+
+ public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, string alias, IImageUrlGenerator imageUrlGenerator, string cacheBusterValue)
+ {
+ return mediaWithCrops.LocalCrops.Src + mediaWithCrops.LocalCrops.GetCropUrl(alias, imageUrlGenerator, cacheBusterValue: cacheBusterValue);
+
+ }
}
}
diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs
index dad2f9e3f335..51845946f133 100644
--- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs
+++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs
@@ -1,7 +1,5 @@
using System;
-using Newtonsoft.Json.Linq;
using System.Globalization;
-using System.Text;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Composing;
@@ -32,6 +30,8 @@ public static class ImageCropperTemplateExtensions
///
public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator);
+ public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, ImageCropperValue imageCropperValue) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator, imageCropperValue);
+
///
/// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item.
///
@@ -118,6 +118,13 @@ public static string GetCropUrl(
ImageCropRatioMode? ratioMode = null,
bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale);
+ public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops,
+ string alias,
+ string cacheBusterValue = null)
+ => ImageCropperTemplateCoreExtensions.GetLocalCropUrl(mediaWithCrops, alias, Current.ImageUrlGenerator, cacheBusterValue);
+
+
+
///
/// Gets the ImageProcessor URL from the image path.
///
diff --git a/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs b/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs
new file mode 100644
index 000000000000..859b3b35ebe6
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs
@@ -0,0 +1,13 @@
+using Newtonsoft.Json;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ public class FileExtensionConfigItem : IFileExtensionConfigItem
+ {
+ [JsonProperty("id")]
+ public int Id { get; set; }
+
+ [JsonProperty("value")]
+ public string Value { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs b/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs
new file mode 100644
index 000000000000..55f947797a26
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ ///
+ /// Represents the configuration for the file upload address value editor.
+ ///
+ public class FileUploadConfiguration : IFileExtensionsConfig
+ {
+ [ConfigurationField("fileExtensions", "Accepted file extensions", "multivalues")]
+ public List FileExtensions { get; set; } = new List();
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs
new file mode 100644
index 000000000000..abbd19a79315
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs
@@ -0,0 +1,12 @@
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ ///
+ /// Represents the configuration editor for the file upload value editor.
+ ///
+ public class FileUploadConfigurationEditor : ConfigurationEditor
+ {
+
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs
index 052af18aa10a..a105d490be26 100644
--- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs
+++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs
@@ -32,6 +32,10 @@ public FileUploadPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem
_uploadAutoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, contentSection);
}
+
+ ///
+ protected override IConfigurationEditor CreateConfigurationEditor() => new FileUploadConfigurationEditor();
+
///
/// Creates the corresponding property value editor.
///
diff --git a/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs
new file mode 100644
index 000000000000..c4934540c793
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using Umbraco.Web.PropertyEditors;
+
+namespace Umbraco.Core.PropertyEditors
+{
+ ///
+ /// Marker interface for any editor configuration that supports defining file extensions
+ ///
+ public interface IFileExtensionsConfig
+ {
+ List FileExtensions { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs
new file mode 100644
index 000000000000..682e8815659a
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs
@@ -0,0 +1,11 @@
+using Newtonsoft.Json;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ public interface IFileExtensionConfigItem
+ {
+ int Id { get; set; }
+
+ string Value { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs
new file mode 100644
index 000000000000..4c3c6564a5da
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs
@@ -0,0 +1,60 @@
+using Newtonsoft.Json;
+using Umbraco.Core;
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ ///
+ /// Represents the configuration for the media picker value editor.
+ ///
+ public class MediaPicker3Configuration : IIgnoreUserStartNodesConfig
+ {
+ [ConfigurationField("filter", "Accepted types", "treesourcetypepicker",
+ Description = "Limit to specific types")]
+ public string Filter { get; set; }
+
+ [ConfigurationField("multiple", "Pick multiple items", "boolean", Description = "Outputs a IEnumerable")]
+ public bool Multiple { get; set; }
+
+ [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")]
+ public NumberRange ValidationLimit { get; set; } = new NumberRange();
+
+ public class NumberRange
+ {
+ [JsonProperty("min")]
+ public int? Min { get; set; }
+
+ [JsonProperty("max")]
+ public int? Max { get; set; }
+ }
+
+ [ConfigurationField("startNodeId", "Start node", "mediapicker")]
+ public Udi StartNodeId { get; set; }
+
+ [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes,
+ "Ignore User Start Nodes", "boolean",
+ Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")]
+ public bool IgnoreUserStartNodes { get; set; }
+
+ [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")]
+ public bool EnableLocalFocalPoint { get; set; }
+
+ [ConfigurationField("crops", "Image Crops", "views/propertyeditors/MediaPicker3/prevalue/mediapicker3.crops.html", Description = "Local crops, stored on document")]
+ public CropConfiguration[] Crops { get; set; }
+
+ public class CropConfiguration
+ {
+ [JsonProperty("alias")]
+ public string Alias { get; set; }
+
+ [JsonProperty("label")]
+ public string Label { get; set; }
+
+ [JsonProperty("width")]
+ public int Width { get; set; }
+
+ [JsonProperty("height")]
+ public int Height { get; set; }
+ }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs
new file mode 100644
index 000000000000..37063aa1536f
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ ///
+ /// Represents the configuration editor for the media picker value editor.
+ ///
+ public class MediaPicker3ConfigurationEditor : ConfigurationEditor
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MediaPicker3ConfigurationEditor()
+ {
+ // configure fields
+ // this is not part of ContentPickerConfiguration,
+ // but is required to configure the UI editor (when editing the configuration)
+
+ Field(nameof(MediaPicker3Configuration.StartNodeId))
+ .Config = new Dictionary { { "idType", "udi" } };
+
+ Field(nameof(MediaPicker3Configuration.Filter))
+ .Config = new Dictionary { { "itemType", "media" } };
+ }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs
new file mode 100644
index 000000000000..526b4830c8cd
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models.Editors;
+using Umbraco.Core.PropertyEditors;
+using Umbraco.Web.PropertyEditors.ValueConverters;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ ///
+ /// Represents a media picker property editor.
+ ///
+ [DataEditor(
+ Constants.PropertyEditors.Aliases.MediaPicker3,
+ EditorType.PropertyValue,
+ "Media Picker v3",
+ "mediapicker3",
+ ValueType = ValueTypes.Json,
+ Group = Constants.PropertyEditors.Groups.Media,
+ Icon = Constants.Icons.MediaImage)]
+ public class MediaPicker3PropertyEditor : DataEditor
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MediaPicker3PropertyEditor(ILogger logger)
+ : base(logger)
+ {
+ }
+
+ ///
+ protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPicker3ConfigurationEditor();
+
+ protected override IDataValueEditor CreateValueEditor() => new MediaPicker3PropertyValueEditor(Attribute);
+
+ internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference
+ {
+ ///
+ /// Note: no FromEditor() and ToEditor() methods
+ /// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string
+ ///
+ public MediaPicker3PropertyValueEditor(DataEditorAttribute attribute) : base(attribute)
+ {
+ }
+
+ public IEnumerable GetReferences(object value)
+ {
+ var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
+
+ if (rawJson.IsNullOrWhiteSpace())
+ yield break;
+
+ var mediaWithCropsDtos = JsonConvert.DeserializeObject(rawJson);
+
+ foreach (var mediaWithCropsDto in mediaWithCropsDtos)
+ {
+ yield return new UmbracoEntityReference(GuidUdi.Create(Constants.UdiEntityType.Media, mediaWithCropsDto.MediaKey));
+ }
+ }
+
+ }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs
new file mode 100644
index 000000000000..f9b2ad75e169
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs
@@ -0,0 +1,119 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.PublishedContent;
+using Umbraco.Core.PropertyEditors;
+using Umbraco.Core.PropertyEditors.ValueConverters;
+using Umbraco.Web.PublishedCache;
+
+namespace Umbraco.Web.PropertyEditors.ValueConverters
+{
+ [DefaultPropertyValueConverter]
+ public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase
+ {
+
+ private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
+
+ public MediaPickerWithCropsValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor)
+ {
+ _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
+ }
+
+ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
+
+ ///
+ /// Enusre this property value convertor is for the New Media Picker with Crops aka MediaPicker 3
+ ///
+ public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3);
+
+ ///
+ /// Check if the raw JSON value is not an empty array
+ ///
+ public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]";
+
+ ///
+ /// What C# model type does the raw JSON return for Models & Views
+ ///
+ public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
+ {
+ // Check do we want to return IPublishedContent collection still or a NEW model ?
+ var isMultiple = IsMultipleDataType(propertyType.DataType);
+ return isMultiple
+ ? typeof(IEnumerable)
+ : typeof(MediaWithCrops);
+ }
+
+ public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString();
+
+ public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
+ {
+ var mediaItems = new List();
+ var isMultiple = IsMultipleDataType(propertyType.DataType);
+ if (inter == null)
+ {
+ return isMultiple ? mediaItems: null;
+ }
+
+ var dtos = JsonConvert.DeserializeObject>(inter.ToString());
+
+ foreach(var media in dtos)
+ {
+ var item = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(media.MediaKey);
+ if (item != null)
+ {
+ mediaItems.Add(new MediaWithCrops
+ {
+ MediaItem = item,
+ LocalCrops = new ImageCropperValue
+ {
+ Crops = media.Crops,
+ FocalPoint = media.FocalPoint,
+ Src = item.Url()
+ }
+ });
+ }
+ }
+
+ return isMultiple ? mediaItems : FirstOrDefault(mediaItems);
+ }
+
+ ///
+ /// Is the media picker configured to pick multiple media items
+ ///
+ ///
+ ///
+ private bool IsMultipleDataType(PublishedDataType dataType)
+ {
+ var config = dataType.ConfigurationAs();
+ return config.Multiple;
+ }
+
+ private object FirstOrDefault(IList mediaItems)
+ {
+ return mediaItems.Count == 0 ? null : mediaItems[0];
+ }
+
+
+ ///
+ /// Model/DTO that represents the JSON that the MediaPicker3 stores
+ ///
+ [DataContract]
+ internal class MediaWithCropsDto
+ {
+ [DataMember(Name = "key")]
+ public Guid Key { get; set; }
+
+ [DataMember(Name = "mediaKey")]
+ public Guid MediaKey { get; set; }
+
+ [DataMember(Name = "crops")]
+ public IEnumerable Crops { get; set; }
+
+ [DataMember(Name = "focalPoint")]
+ public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; }
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index a6cbefa825e5..ff988cf5bf7d 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -254,9 +254,17 @@
+
+
+
+
+
+
+
+
@@ -266,6 +274,7 @@
+
diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs
index 0f5b0557f4a1..592c88945bae 100644
--- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs
+++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs
@@ -262,6 +262,32 @@ public static IHtmlString GetCropUrl(this UrlHelper urlHelper,
return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
}
+ public static IHtmlString GetCropUrl(this UrlHelper urlHelper,
+ ImageCropperValue imageCropperValue,
+ string cropAlias,
+ int? width = null,
+ int? height = null,
+ int? quality = null,
+ ImageCropMode? imageCropMode = null,
+ ImageCropAnchor? imageCropAnchor = null,
+ bool preferFocalPoint = false,
+ bool useCropDimensions = true,
+ string cacheBusterValue = null,
+ string furtherOptions = null,
+ ImageCropRatioMode? ratioMode = null,
+ bool upScale = true,
+ bool htmlEncode = true)
+ {
+ if (imageCropperValue == null) return EmptyHtmlString;
+
+ var imageUrl = imageCropperValue.Src;
+ var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode,
+ imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode,
+ upScale);
+ return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
+ }
+
+
#endregion
///
From 4369747c720d2b5fa7e3c8b7c3617e3ba29a4421 Mon Sep 17 00:00:00 2001
From: Mads Rasmussen
Date: Thu, 22 Apr 2021 16:55:18 +0200
Subject: [PATCH 43/80] skip client side validation
---
.../components/content/edit.controller.js | 87 +++++++++----------
1 file changed, 39 insertions(+), 48 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
index 196c885b4e1b..bce797d5c819 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
@@ -442,7 +442,6 @@
// This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish
function performSave(args) {
-
//Used to check validility of nested form - coming from Content Apps mostly
//Set them all to be invalid
var fieldsToRollback = checkValidility();
@@ -476,8 +475,6 @@
return $q.when(data);
},
function (err) {
-
-
syncTreeNode($scope.content, $scope.content.path);
if($scope.contentForm.$invalid !== true) {
@@ -739,54 +736,48 @@
clearNotifications($scope.content);
// TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant
if (hasVariants($scope.content)) {
-
- //before we launch the dialog we want to execute all client side validations first
- if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog", skipValidation:true, keepServerValidation:true })) {
- var dialog = {
- parentScope: $scope,
- view: "views/content/overlays/save.html",
- variants: $scope.content.variants, //set a model property for the dialog
- skipFormValidation: true, //when submitting the overlay form, skip any client side validation
- submitButtonLabelKey: "buttons_save",
- submit: function (model) {
- model.submitButtonState = "busy";
+ var dialog = {
+ parentScope: $scope,
+ view: "views/content/overlays/save.html",
+ variants: $scope.content.variants, //set a model property for the dialog
+ skipFormValidation: true, //when submitting the overlay form, skip any client side validation
+ submitButtonLabelKey: "buttons_save",
+ submit: function (model) {
+ model.submitButtonState = "busy";
+ clearNotifications($scope.content);
+ //we need to return this promise so that the dialog can handle the result and wire up the validation response
+ return performSave({
+ saveMethod: $scope.saveMethod(),
+ action: "save",
+ showNotifications: false,
+ skipValidation: true
+ }).then(function (data) {
+ //show all notifications manually here since we disabled showing them automatically in the save method
+ formHelper.showNotifications(data);
clearNotifications($scope.content);
- //we need to return this promise so that the dialog can handle the result and wire up the validation response
- return performSave({
- saveMethod: $scope.saveMethod(),
- action: "save",
- showNotifications: false
- }).then(function (data) {
- //show all notifications manually here since we disabled showing them automatically in the save method
- formHelper.showNotifications(data);
- clearNotifications($scope.content);
- overlayService.close();
- return $q.when(data);
- },
- function (err) {
- clearDirtyState($scope.content.variants);
- //model.submitButtonState = "error";
- // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely.
- if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) {
- model.submitButtonState = "success";
- } else {
- model.submitButtonState = "error";
- }
- //re-map the dialog model since we've re-bound the properties
- dialog.variants = $scope.content.variants;
- handleHttpException(err);
- });
- },
- close: function (oldModel) {
overlayService.close();
- }
- };
+ return $q.when(data);
+ },
+ function (err) {
+ clearDirtyState($scope.content.variants);
+ //model.submitButtonState = "error";
+ // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely.
+ if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) {
+ model.submitButtonState = "success";
+ } else {
+ model.submitButtonState = "error";
+ }
+ //re-map the dialog model since we've re-bound the properties
+ dialog.variants = $scope.content.variants;
+ handleHttpException(err);
+ });
+ },
+ close: function (oldModel) {
+ overlayService.close();
+ }
+ };
- overlayService.open(dialog);
- }
- else {
- showValidationNotification();
- }
+ overlayService.open(dialog);
}
else {
//ensure the flags are set
From d8d4be9e8e5d8d31f52487dec916eb89f48fa108 Mon Sep 17 00:00:00 2001
From: Mole
Date: Mon, 3 May 2021 10:22:42 +0200
Subject: [PATCH 44/80] Backport cache key fix and optimizations (#10199)
* Add GetKey
* Update Usages of GetKey and remove GetKey(object)
We shouldn't have to retain this since RepositoryCacheKeys is internal.
* Apply changes to DefaultRepositoryCachePolicy
* Add check for default/less than -1 on UserRepository PerformGet
Co-authored-by: Nikolaj
---
.../Cache/DefaultRepositoryCachePolicy.cs | 42 +++++++++++--------
.../Implement/ConsentRepository.cs | 2 +-
.../Implement/DictionaryRepository.cs | 16 +++----
.../Implement/DocumentRepository.cs | 2 +-
.../Repositories/Implement/MediaRepository.cs | 2 +-
.../Implement/MemberRepository.cs | 6 +--
.../Implement/RepositoryCacheKeys.cs | 19 +++++++--
.../Repositories/Implement/UserRepository.cs | 10 ++++-
.../Cache/ContentCacheRefresher.cs | 4 +-
src/Umbraco.Web/Cache/MacroCacheRefresher.cs | 2 +-
src/Umbraco.Web/Cache/MediaCacheRefresher.cs | 4 +-
src/Umbraco.Web/Cache/MemberCacheRefresher.cs | 10 ++---
.../Cache/RelationTypeCacheRefresher.cs | 4 +-
src/Umbraco.Web/Cache/UserCacheRefresher.cs | 4 +-
.../Cache/UserGroupCacheRefresher.cs | 2 +-
15 files changed, 79 insertions(+), 50 deletions(-)
diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs
index c11309c82733..94756ce9750f 100644
--- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs
+++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Cache
internal class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase
where TEntity : class, IEntity
{
- private static readonly TEntity[] EmptyEntities = new TEntity[0]; // const
+ private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const
private readonly RepositoryCachePolicyOptions _options;
public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
@@ -29,17 +29,25 @@ public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeA
_options = options ?? throw new ArgumentNullException(nameof(options));
}
- protected string GetEntityCacheKey(object id)
- {
- if (id == null) throw new ArgumentNullException(nameof(id));
- return GetEntityTypeCacheKey() + id;
- }
+ protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id;
- protected string GetEntityTypeCacheKey()
+ protected string GetEntityCacheKey(TId id)
{
- return $"uRepo_{typeof (TEntity).Name}_";
+ if (EqualityComparer.Default.Equals(id, default))
+ {
+ return string.Empty;
+ }
+
+ if (typeof(TId).IsValueType)
+ {
+ return EntityTypeCacheKey + id;
+ }
+
+ return EntityTypeCacheKey + id.ToString().ToUpperInvariant();
}
+ protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_";
+
protected virtual void InsertEntity(string cacheKey, TEntity entity)
{
Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
@@ -52,7 +60,7 @@ protected virtual void InsertEntities(TId[] ids, TEntity[] entities)
// getting all of them, and finding nothing.
// if we can cache a zero count, cache an empty array,
// for as long as the cache is not cleared (no expiration)
- Cache.Insert(GetEntityTypeCacheKey(), () => EmptyEntities);
+ Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities);
}
else
{
@@ -81,7 +89,7 @@ public override void Create(TEntity entity, Action persistNew)
}
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
- Cache.Clear(GetEntityTypeCacheKey());
+ Cache.Clear(EntityTypeCacheKey);
}
catch
{
@@ -91,7 +99,7 @@ public override void Create(TEntity entity, Action persistNew)
Cache.Clear(GetEntityCacheKey(entity.Id));
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
- Cache.Clear(GetEntityTypeCacheKey());
+ Cache.Clear(EntityTypeCacheKey);
throw;
}
@@ -113,7 +121,7 @@ public override void Update(TEntity entity, Action persistUpdated)
}
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
- Cache.Clear(GetEntityTypeCacheKey());
+ Cache.Clear(EntityTypeCacheKey);
}
catch
{
@@ -123,7 +131,7 @@ public override void Update(TEntity entity, Action persistUpdated)
Cache.Clear(GetEntityCacheKey(entity.Id));
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
- Cache.Clear(GetEntityTypeCacheKey());
+ Cache.Clear(EntityTypeCacheKey);
throw;
}
@@ -144,7 +152,7 @@ public override void Delete(TEntity entity, Action persistDeleted)
var cacheKey = GetEntityCacheKey(entity.Id);
Cache.Clear(cacheKey);
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
- Cache.Clear(GetEntityTypeCacheKey());
+ Cache.Clear(EntityTypeCacheKey);
}
}
@@ -195,7 +203,7 @@ public override TEntity[] GetAll(TId[] ids, Func> pe
else
{
// get everything we have
- var entities = Cache.GetCacheItemsByKeySearch(GetEntityTypeCacheKey())
+ var entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey)
.ToArray(); // no need for null checks, we are not caching nulls
if (entities.Length > 0)
@@ -218,7 +226,7 @@ public override TEntity[] GetAll(TId[] ids, Func> pe
{
// if none of them were in the cache
// and we allow zero count - check for the special (empty) entry
- var empty = Cache.GetCacheItem(GetEntityTypeCacheKey());
+ var empty = Cache.GetCacheItem(EntityTypeCacheKey);
if (empty != null) return empty;
}
}
@@ -238,7 +246,7 @@ public override TEntity[] GetAll(TId[] ids, Func> pe
///
public override void ClearAll()
{
- Cache.ClearByKey(GetEntityTypeCacheKey());
+ Cache.ClearByKey(EntityTypeCacheKey);
}
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs
index 57d5dfa86493..d73b360677ba 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs
@@ -86,7 +86,7 @@ protected override void PersistUpdatedItem(IConsent entity)
Database.Update(dto);
entity.ResetDirtyProperties();
- IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id));
+ IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id));
}
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs
index 0b5866395279..0e3521f8bc48 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs
@@ -130,7 +130,7 @@ protected override void PersistNewItem(IDictionaryItem entity)
foreach (var translation in dictionaryItem.Translations)
translation.Value = translation.Value.ToValidXmlString();
-
+
var dto = DictionaryItemFactory.BuildDto(dictionaryItem);
var id = Convert.ToInt32(Database.Insert(dto));
@@ -152,7 +152,7 @@ protected override void PersistUpdatedItem(IDictionaryItem entity)
foreach (var translation in entity.Translations)
translation.Value = translation.Value.ToValidXmlString();
-
+
var dto = DictionaryItemFactory.BuildDto(entity);
Database.Update(dto);
@@ -174,8 +174,8 @@ protected override void PersistUpdatedItem(IDictionaryItem entity)
entity.ResetDirtyProperties();
//Clear the cache entries that exist by uniqueid/item key
- IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey));
- IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key));
+ IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey));
+ IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key));
}
protected override void PersistDeletedItem(IDictionaryItem entity)
@@ -186,8 +186,8 @@ protected override void PersistDeletedItem(IDictionaryItem entity)
Database.Delete("WHERE id = @Id", new { Id = entity.Key });
//Clear the cache entries that exist by uniqueid/item key
- IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey));
- IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key));
+ IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey));
+ IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key));
entity.DeleteDate = DateTime.Now;
}
@@ -203,8 +203,8 @@ private void RecursiveDelete(Guid parentId)
Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId });
//Clear the cache entries that exist by uniqueid/item key
- IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key));
- IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId));
+ IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key));
+ IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId));
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
index a97569d571c9..f5d993070c04 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -1163,7 +1163,7 @@ private IEnumerable MapDtosToContent(List dtos,
if (withCache)
{
// if the cache contains the (proper version of the) item, use it
- var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId));
+ var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId));
if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id)
{
content[i] = (Content)cached;
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
index ac180d54ef14..c456a0b2ad98 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs
@@ -508,7 +508,7 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac
if (withCache)
{
// if the cache contains the (proper version of the) item, use it
- var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId));
+ var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId));
if (cached != null && cached.VersionId == dto.ContentVersionDto.Id)
{
content[i] = (Models.Media)cached;
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs
index 78dbbe317acd..15c707c624ff 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs
@@ -331,7 +331,7 @@ protected override void PersistNewItem(IMember entity)
}
protected override void PersistUpdatedItem(IMember entity)
- {
+ {
// update
entity.UpdatingEntity();
@@ -534,7 +534,7 @@ public void SetLastLogin(string username, DateTime date)
var sqlSelectTemplateVersion = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin2", s => s
.Select(x => x.Id)
- .From()
+ .From()
.InnerJoin().On((l, r) => l.NodeId == r.NodeId)
.InnerJoin().On((l, r) => l.NodeId == r.NodeId)
.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType"))
@@ -606,7 +606,7 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac
if (withCache)
{
// if the cache contains the (proper version of the) item, use it
- var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId));
+ var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId));
if (cached != null && cached.VersionId == dto.ContentVersionDto.Id)
{
content[i] = (Member) cached;
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryCacheKeys.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryCacheKeys.cs
index 09a7c021f81f..e2d0e26274ec 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryCacheKeys.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryCacheKeys.cs
@@ -8,14 +8,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
///
internal static class RepositoryCacheKeys
{
- private static readonly Dictionary Keys = new Dictionary();
+ private static readonly Dictionary s_keys = new Dictionary();
public static string GetKey()
{
var type = typeof(T);
- return Keys.TryGetValue(type, out var key) ? key : (Keys[type] = "uRepo_" + type.Name + "_");
+ return s_keys.TryGetValue(type, out var key) ? key : (s_keys[type] = "uRepo_" + type.Name + "_");
}
- public static string GetKey(object id) => GetKey() + id;
+ public static string GetKey(TId id)
+ {
+ if (EqualityComparer.Default.Equals(id, default))
+ {
+ return string.Empty;
+ }
+
+ if (typeof(TId).IsValueType)
+ {
+ return GetKey() + id;
+ }
+
+ return GetKey() + id.ToString().ToUpperInvariant();
+ }
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs
index c9f85c343c0b..9bc0bbeb4702 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs
@@ -83,6 +83,14 @@ private string PasswordConfigJson
protected override IUser PerformGet(int id)
{
+ // This will never resolve to a user, yet this is asked
+ // for all of the time (especially in cases of members).
+ // Don't issue a SQL call for this, we know it will not exist.
+ if (id == default || id < -1)
+ {
+ return null;
+ }
+
var sql = SqlContext.Sql()
.Select()
.From()
@@ -168,7 +176,7 @@ public IDictionary GetUserStates()
}
public Guid CreateLoginSession(int userId, string requestingIpAddress, bool cleanStaleSessions = true)
- {
+ {
var now = DateTime.UtcNow;
var dto = new UserLoginDto
{
diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs
index 5e8bd83c5dc7..8d9388949d63 100644
--- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs
@@ -54,9 +54,9 @@ public override void Refresh(JsonPayload[] payloads)
foreach (var payload in payloads.Where(x => x.Id != default))
{
//By INT Id
- isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id));
+ isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id));
//By GUID Key
- isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key));
+ isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key));
_idkMap.ClearCache(payload.Id);
diff --git a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs
index 0cecba7b7bec..e5f35e09c844 100644
--- a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs
@@ -51,7 +51,7 @@ public override void Refresh(string json)
var macroRepoCache = AppCaches.IsolatedCaches.Get();
if (macroRepoCache)
{
- macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id));
+ macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id));
}
}
diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs
index b0845f2a9a74..a2c1110b8885 100644
--- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs
@@ -62,8 +62,8 @@ public override void Refresh(JsonPayload[] payloads)
// repository cache
// it *was* done for each pathId but really that does not make sense
// only need to do it for the current media
- mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id));
- mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key));
+ mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id));
+ mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key));
// remove those that are in the branch
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove))
diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs
index 48ae40ce3b6b..9483700a1932 100644
--- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs
@@ -21,7 +21,7 @@ public MemberCacheRefresher(AppCaches appCaches, IdkMap idkMap)
_legacyMemberRefresher = new LegacyMemberCacheRefresher(this, appCaches);
}
- public class JsonPayload
+ public class JsonPayload
{
[JsonConstructor]
public JsonPayload(int id, string username)
@@ -87,11 +87,11 @@ private void ClearCache(params JsonPayload[] payloads)
_idkMap.ClearCache(p.Id);
if (memberCache)
{
- memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id));
- memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username));
- }
+ memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id));
+ memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username));
+ }
}
-
+
}
#endregion
diff --git a/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs
index c9c8b47bbf70..8899438a6a89 100644
--- a/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs
@@ -35,7 +35,7 @@ public override void RefreshAll()
public override void Refresh(int id)
{
var cache = AppCaches.IsolatedCaches.Get();
- if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id));
+ if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id));
base.Refresh(id);
}
@@ -48,7 +48,7 @@ public override void Refresh(Guid id)
public override void Remove(int id)
{
var cache = AppCaches.IsolatedCaches.Get();
- if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id));
+ if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id));
base.Remove(id);
}
diff --git a/src/Umbraco.Web/Cache/UserCacheRefresher.cs b/src/Umbraco.Web/Cache/UserCacheRefresher.cs
index ce2cbbf754f7..ed71431fab32 100644
--- a/src/Umbraco.Web/Cache/UserCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/UserCacheRefresher.cs
@@ -43,13 +43,13 @@ public override void Remove(int id)
var userCache = AppCaches.IsolatedCaches.Get();
if (userCache)
{
- userCache.Result.Clear(RepositoryCacheKeys.GetKey(id));
+ userCache.Result.Clear(RepositoryCacheKeys.GetKey(id));
userCache.Result.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id);
userCache.Result.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id);
userCache.Result.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id);
userCache.Result.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id);
}
-
+
base.Remove(id);
}
diff --git a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs
index cfdf8f3669af..7ef5f088d8c2 100644
--- a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs
@@ -58,7 +58,7 @@ public override void Remove(int id)
var userGroupCache = AppCaches.IsolatedCaches.Get();
if (userGroupCache)
{
- userGroupCache.Result.Clear(RepositoryCacheKeys.GetKey(id));
+ userGroupCache.Result.Clear(RepositoryCacheKeys.GetKey(id));
userGroupCache.Result.ClearByKey(UserGroupRepository.GetByAliasCacheKeyPrefix);
}
From dcfdcb11de0a515ca224eb57e010f700455a2e5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Mon, 3 May 2021 11:29:52 +0200
Subject: [PATCH 45/80] remove log
---
.../src/common/services/contenteditinghelper.service.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
index 6ae2becf41fd..bab665579c95 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
@@ -649,7 +649,6 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
formHelper.handleServerValidation(args.err.data.ModelState);
var messageType = 2;//error
- console.log(args)
if (args.action === "save") {
messageType = 4;//warning
}
From ce34165e9dde4187575f47a513ecc2584ad458c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Mon, 3 May 2021 16:18:15 +0200
Subject: [PATCH 46/80] use formCtrl.$invalid instead of running
angularHelper.countAllFormErrors, as this method is way too heavy to run in a
$watch. (#10134)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Niels Lyngsø
---
.../directives/validation/valformmanager.directive.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
index 55878db2e976..f34edf820bb3 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
@@ -25,7 +25,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
function ValFormManagerController($scope) {
//This exposes an API for direct use with this directive
- // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and
+ // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and
// because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is
// an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager
// in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing)
@@ -109,9 +109,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
labels.stayButton = values[3];
});
- //watch the list of validation errors to notify the application of any validation changes
- // TODO: Wouldn't it be easier/faster to watch formCtrl.$invalid ?
- scope.$watch(() => angularHelper.countAllFormErrors(formCtrl),
+
+ // watch the list of validation errors to notify the application of any validation changes
+ scope.$watch(() => formCtrl.$invalid,
function (e) {
notify(scope);
From c2fd28810be3a043a5fcf84abc6fa6439af25caa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Tue, 4 May 2021 10:38:26 +0200
Subject: [PATCH 47/80] show invariant property validation issues in the save
dialog
---
.../components/editor/umb-variant-switcher.less | 13 ++++++++++++-
.../src/views/content/overlays/save.html | 10 ++++++++--
2 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
index 9d2782f184c8..594558da51f3 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
@@ -50,6 +50,10 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
+ .show-validation-type-warning & {
+ background-color: @warningBackground;
+ color: @warningText;
+ }
animation-duration: 1.4s;
animation-iteration-count: infinite;
@@ -233,7 +237,10 @@ button.umb-variant-switcher__toggle {
.umb-variant-switcher__item.--error {
.umb-variant-switcher__name {
- color: @red;
+ color: @formErrorText;
+ .show-validation-type-warning & {
+ color: @formWarningText;
+ }
&::after {
content: '!';
position: relative;
@@ -250,6 +257,10 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
+ .show-validation-type-warning & {
+ background-color: @warningBackground;
+ color: @warningText;
+ }
animation-duration: 1.4s;
animation-iteration-count: infinite;
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html
index d414f30dbf38..36e01991dfa6 100644
--- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html
+++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html
@@ -18,15 +18,18 @@
+
+
+
+
- *
@@ -36,11 +39,14 @@
-
-
+ {{saveVariantSelectorForm.saveVariantSelector.errorMsg}}
+
+ {{saveVariantSelectorForm.saveInvariant.errorMsg}}
+
From c8a98a670cf0157e84d80d5fffc7ddace3ff2a54 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Tue, 4 May 2021 11:06:52 +0200
Subject: [PATCH 48/80] =?UTF-8?q?Review=20AB11194=20=E2=80=94=20Improve=20?=
=?UTF-8?q?media=20selector=20UX=20(#10157)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* set input file accept
* Use PreValue file extensions for limiting the files to be chosen in file input
* Current state for Warren to review
* This should fix up what you need Niels
* update csproj
* use empty string if fileExtensions is undefined
* public interface
* initial work
* local crops
* translations
* translation correction
* fix misspeling
* some progress
* filter media picker
* align media card grid items correctly
* responsive media cropper
* always be able to scale 3 times smallest scale
* making image cropper property editor responsive
* scroll to scale
* adjust slider look
* rearrange parts of mediaentryeditor
* test helper
* styling
* move controls inside umb-image-crop
* seperate umg-cropper-gravity styling
* corrected layout
* more ui refinement
* keep the idea of mandatory out for now.
* remove double ;
* removed testing code
* JSON Property Value Convertor now has an array of property editors to exclude
* Property Value Convertor for Media Picker 3 aka Media Picker with Local Crops
* Experimenting on best approach to retrieve local crop in razor view when iterating over picked media items
* Update ValueConvertor to use ImageCropperValue as part of the model for views as alot of existing CropUrls can then use it
* Update extension methods to take an ImageCropperValue model (localCropData)
* Forgot to update CSProj for new ValueConvertor
* New GetCropUrl @Url.GetCropUrl(crop.Alias, media.LocalCrops) as oppposed to @Url.GetCropUrl(media.LocalCrops, cropAlias:crop.Alias, useCropDimensions: true)
* Remove dupe item in CSProj
* Use a contains as an opposed to Array.IndexOf
* various corrections, SingleMode based on max 1, remove double checkerBackground, enforce validation for Crops, changed error indication
* mediapicker v3
* correct version
* fixing file ext label text color
* clipboard features for MediaPicker v3
* highlight not allowed types
* highlight trashed as an error
* Media Types Video, Sound, Document and Vector Image
* Rename to Audio and VectorGraphics
* Add (SVG) in the name for Vector Graphics
* adding CSV to Documents
* remove this commented code.
* remove this commented code
* number range should not go below 0, at-least as default until we make that configurable.
* use min not ng-min
* description for local crops
* Error/Limits highlighting reactive
* visual adjustments
* Enabling opening filtered folders + corrected select hover states
* Varous fixes to resolve issues with unit tests.
* Refactor MediaType Documents to only contain Article file type
* mark as build-in
* predefined MediaPicker3 DataTypes, renaming v2 to "old"
* set scale bar current value after min and max has been set
* added missing }
* update when focal point is dragged
* adjusted styling for Image Cropper property editor
* correcting comment
* remove todo - message for trashed media items works
* Changed parameter ordering
* Introduced new extension method on MediaWithCrops to get croppings urls in with full path
* Reintroducing Single Item Mode
* use Multiple instead of SingleMode
* renaming and adding multiple to preconfigured datatypes
* Change existing media picker to use the Clipboard type MEDIA, enabling shared functionality.
* clean up unused clipboard parts
* adjusted to new amount
* correcting test
* Fix unit test
* Move MediaWithCrops to separate file and move to Core.Models
* parseContentForPaste
* clean up
* ensure crops is an array.
* actively enable focal points, so we dont set focal points that aren't used.
* only accept files that matches file extensions from Umbraco Settings
* Cleanup
* Add references from MediaPicker3 to media
* corrections from various feedback
* remove comment
* correct wording
* use windowResizeListener
* Show Media Type Selector if there is multiple types that matches the current upload batch
* clean-up and refactoring
* auto pick option
Co-authored-by: Warren Buckley
Co-authored-by: Niels Lyngsø
Co-authored-by: Mads Rasmussen
Co-authored-by: Andy Butland
Co-authored-by: Bjarke Berg
Co-authored-by: Sebastiaan Janssen
Co-authored-by: Elitsa Marinovska
---
.../upload/umbfiledropzone.directive.js | 91 +++++++++++--------
.../services/mediatypehelper.service.js | 43 ++++++---
src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 +
src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 +
.../Umbraco/config/lang/en_us.xml | 1 +
5 files changed, 87 insertions(+), 50 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js
index 7f405eb28c24..79dfee059ee6 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js
@@ -20,7 +20,7 @@ TODO
angular.module("umbraco.directives")
.directive('umbFileDropzone',
- function ($timeout, Upload, localizationService, umbRequestHelper, overlayService) {
+ function ($timeout, Upload, localizationService, umbRequestHelper, overlayService, mediaHelper, mediaTypeHelper) {
return {
restrict: 'E',
replace: true,
@@ -88,21 +88,12 @@ angular.module("umbraco.directives")
});
scope.queue = [];
}
- // One allowed type
- if (scope.acceptedMediatypes && scope.acceptedMediatypes.length === 1) {
- // Standard setup - set alias to auto select to let the server best decide which media type to use
- if (scope.acceptedMediatypes[0].alias === 'Image') {
- scope.contentTypeAlias = "umbracoAutoSelect";
- } else {
- scope.contentTypeAlias = scope.acceptedMediatypes[0].alias;
- }
+ // If we have Accepted Media Types, we will ask to choose Media Type, if Choose Media Type returns false, it only had one choice and therefor no reason to
+ if (scope.acceptedMediatypes && _requestChooseMediaTypeDialog() === false) {
+ scope.contentTypeAlias = "umbracoAutoSelect";
_processQueueItem();
}
- // More than one, open dialog
- if (scope.acceptedMediatypes && scope.acceptedMediatypes.length > 1) {
- _chooseMediaType();
- }
}
}
@@ -146,8 +137,8 @@ angular.module("umbraco.directives")
// set percentage property on file
file.uploadProgress = progressPercentage;
// set uploading status on file
- file.uploadStatus = "uploading";
- }
+ file.uploadStatus = "uploading";
+ }
})
.success(function(data, status, headers, config) {
if (data.notifications && data.notifications.length > 0) {
@@ -195,35 +186,61 @@ angular.module("umbraco.directives")
});
}
- function _chooseMediaType() {
+ function _requestChooseMediaTypeDialog() {
- const dialog = {
- view: "itempicker",
- filter: scope.acceptedMediatypes.length > 15,
- availableItems: scope.acceptedMediatypes,
- submit: function (model) {
- scope.contentTypeAlias = model.selectedItem.alias;
- _processQueueItem();
+ if (scope.acceptedMediatypes.length === 1) {
+ // if only one accepted type, then we wont ask to choose.
+ return false;
+ }
- overlayService.close();
- },
- close: function () {
+ var uploadFileExtensions = scope.queue.map(file => mediaHelper.getFileExtension(file.name));
- scope.queue.map(function (file) {
- file.uploadStatus = "error";
- file.serverErrorMessage = "Cannot upload this file, no mediatype selected";
- scope.rejected.push(file);
- });
- scope.queue = [];
+ var filteredMediaTypes = mediaTypeHelper.getTypeAcceptingFileExtensions(scope.acceptedMediatypes, uploadFileExtensions);
- overlayService.close();
- }
- };
+ var mediaTypesNotFile = filteredMediaTypes.filter(mediaType => mediaType.alias !== "File");
- localizationService.localize("defaultdialogs_selectMediaType").then(value => {
- dialog.title = value;
+ if (mediaTypesNotFile.length <= 1) {
+ // if only one or less accepted types when we have filtered type 'file' out, then we wont ask to choose.
+ return false;
+ }
+
+
+ localizationService.localizeMany(["defaultdialogs_selectMediaType", "mediaType_autoPickMediaType"]).then(function (translations) {
+
+ filteredMediaTypes.push({
+ alias: "umbracoAutoSelect",
+ name: translations[1],
+ icon: "icon-wand"
+ });
+
+ const dialog = {
+ view: "itempicker",
+ filter: filteredMediaTypes.length > 8,
+ availableItems: filteredMediaTypes,
+ submit: function (model) {
+ scope.contentTypeAlias = model.selectedItem.alias;
+ _processQueueItem();
+
+ overlayService.close();
+ },
+ close: function () {
+
+ scope.queue.map(function (file) {
+ file.uploadStatus = "error";
+ file.serverErrorMessage = "No files uploaded, no mediatype selected";
+ scope.rejected.push(file);
+ });
+ scope.queue = [];
+
+ overlayService.close();
+ }
+ };
+
+ dialog.title = translations[0];
overlayService.open(dialog);
});
+
+ return true;// yes, we did open the choose-media dialog, therefor we return true.
}
scope.handleFiles = function(files, event) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js
index a347279fdbf9..f6ac16a9bc65 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js
@@ -23,15 +23,15 @@ function mediaTypeHelper(mediaTypeResource, $q) {
getAllowedImagetypes: function (mediaId){
// TODO: This is horribly inneficient - why make one request per type!?
- //This should make a call to c# to get exactly what it's looking for instead of returning every single media type and doing
+ //This should make a call to c# to get exactly what it's looking for instead of returning every single media type and doing
//some filtering on the client side.
- //This is also called multiple times when it's not needed! Example, when launching the media picker, this will be called twice
+ //This is also called multiple times when it's not needed! Example, when launching the media picker, this will be called twice
//which means we'll be making at least 6 REST calls to fetch each media type
// Get All allowedTypes
return mediaTypeResource.getAllowedTypes(mediaId)
.then(function(types){
-
+
var allowedQ = types.map(function(type){
return mediaTypeResource.getById(type.id);
});
@@ -39,16 +39,8 @@ function mediaTypeHelper(mediaTypeResource, $q) {
// Get full list
return $q.all(allowedQ).then(function(fullTypes){
- // Find all the media types with an Image Cropper property editor
- var filteredTypes = mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper']);
-
- // If there is only one media type with an Image Cropper we will return this one
- if(filteredTypes.length === 1) {
- return filteredTypes;
- // If there is more than one Image cropper, custom media types have been added, and we return all media types with and Image cropper or UploadField
- } else {
- return mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper', 'Umbraco.UploadField']);
- }
+ // Find all the media types with an Image Cropper or Upload Field property editor
+ return mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper', 'Umbraco.UploadField']);
});
});
@@ -68,6 +60,31 @@ function mediaTypeHelper(mediaTypeResource, $q) {
}
});
+ },
+
+ getTypeAcceptingFileExtensions: function (mediaTypes, fileExtensions) {
+ return mediaTypes.filter(mediaType => {
+ var uploadProperty;
+ mediaType.groups.forEach(group => {
+ var foundProperty = group.properties.find(property => property.alias === "umbracoFile");
+ if(foundProperty) {
+ uploadProperty = foundProperty;
+ }
+ });
+ if(uploadProperty) {
+ var acceptedFileExtensions;
+ if(uploadProperty.editor === "Umbraco.ImageCropper") {
+ acceptedFileExtensions = Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes;
+ } else if(uploadProperty.editor === "Umbraco.UploadField") {
+ acceptedFileExtensions = (uploadProperty.config.fileExtensions && uploadProperty.config.fileExtensions.length > 0) ? uploadProperty.config.fileExtensions.map(x => x.value) : null;
+ }
+ if(acceptedFileExtensions && acceptedFileExtensions.length > 0) {
+ return fileExtensions.length === fileExtensions.filter(fileExt => acceptedFileExtensions.includes(fileExt)).length;
+ }
+ return true;
+ }
+ return false;
+ });
}
};
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
index 4abcdf8a40bf..62e3ad6b64b0 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
@@ -343,6 +343,7 @@
Kopiering af medietypen fejledeFlytning af medietypen fejlede
+ Auto vælgKopiering af medlemstypen fejlede
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
index cbb6902d744b..7172e44b3675 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
@@ -362,6 +362,7 @@
Failed to copy media typeFailed to move media type
+ Auto pickFailed to copy member type
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 590a24839347..a159b78b8302 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
@@ -369,6 +369,7 @@
Failed to copy media typeFailed to move media type
+ Auto pickFailed to copy member type
From 7ab09cb4045b92b4832450cb39c7c9e2af686fa4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Tue, 4 May 2021 16:58:31 +0200
Subject: [PATCH 49/80] Ensure BlockObjects have references to a working
current property editor. (#10195)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Niels Lyngsø
---
.../components/editor/umbeditors.directive.js | 6 +-
.../umbBlockListPropertyEditor.component.js | 69 ++++++++++++++++---
2 files changed, 61 insertions(+), 14 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js
index 20fba6eb6ed0..acade190472a 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js
@@ -135,11 +135,11 @@
}
// This directive allows for us to run a custom $compile for the view within the repeater which allows
- // us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the
+ // us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the
// infinite editing. The retain the $scope hiearchy a special $parentScope property is passed in to the model.
function EditorRepeaterDirective($http, $templateCache, $compile, angularHelper) {
- function link(scope, el, attr, ctrl) {
-
+ function link(scope, el) {
+
var editor = scope && scope.$parent ? scope.$parent.model : null;
if (!editor) {
return;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js
index 2d9b13ec7aab..7334fbeadf5e 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js
@@ -55,6 +55,12 @@
vm.supportCopy = clipboardService.isSupported();
vm.clipboardItems = [];
unsubscribe.push(eventsService.on("clipboardService.storageUpdate", updateClipboard));
+ unsubscribe.push($scope.$on("editors.content.splitViewChanged", (event, eventData) => {
+ var compositeId = vm.umbVariantContent.editor.compositeId;
+ if(eventData.editors.some(x => x.compositeId === compositeId)) {
+ updateAllBlockObjects();
+ }
+ }));
vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model.
vm.availableBlockTypes = []; // Available block entries of this property editor.
@@ -66,6 +72,7 @@
});
vm.$onInit = function() {
+
if (vm.umbProperty && !vm.umbVariantContent) {// if we dont have vm.umbProperty, it means we are in the DocumentTypeEditor.
// not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope
// inheritance is (i.e.infinite editing)
@@ -175,6 +182,8 @@
// is invalid for some reason or the data structure has changed.
invalidLayoutItems.push(entry);
}
+ } else {
+ updateBlockObject(entry.$block);
}
});
@@ -197,6 +206,16 @@
}
+ function updateAllBlockObjects() {
+ // Update the blockObjects in our layout.
+ vm.layout.forEach(entry => {
+ // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject.
+ if (entry.$block) {
+ updateBlockObject(entry.$block);
+ }
+ });
+ }
+
function getDefaultViewForBlock(block) {
var defaultViewFolderPath = "views/propertyeditors/blocklist/blocklistentryeditors/";
@@ -237,9 +256,6 @@
if (block === null) return null;
- ensureCultureData(block.content);
- ensureCultureData(block.settings);
-
block.view = (block.config.view ? block.config.view : getDefaultViewForBlock(block));
block.showValidation = block.config.view ? true : false;
@@ -254,20 +270,51 @@
block.setParentForm = function (parentForm) {
this._parentForm = parentForm;
};
- block.activate = activateBlock.bind(null, block);
- block.edit = function () {
+
+ /** decorator methods, to enable switching out methods without loosing references that would have been made in Block Views codes */
+ block.activate = function() {
+ this._activate();
+ };
+ block.edit = function() {
+ this._edit();
+ };
+ block.editSettings = function() {
+ this._editSettings();
+ };
+ block.requestDelete = function() {
+ this._requestDelete();
+ };
+ block.delete = function() {
+ this._delete();
+ };
+ block.copy = function() {
+ this._copy();
+ };
+ updateBlockObject(block);
+
+ return block;
+ }
+
+ /** As the block object now contains references to this instance of a property editor, we need to ensure that the Block Object contains latest references.
+ * This is a bit hacky but the only way to maintain this reference currently.
+ * Notice this is most relevant for invariant properties on variant documents, specially for the scenario where the scope of the reference we stored is destroyed, therefor we need to ensure we always have references to a current running property editor*/
+ function updateBlockObject(block) {
+
+ ensureCultureData(block.content);
+ ensureCultureData(block.settings);
+
+ block._activate = activateBlock.bind(null, block);
+ block._edit = function () {
var blockIndex = vm.layout.indexOf(this.layout);
editBlock(this, false, blockIndex, this._parentForm);
};
- block.editSettings = function () {
+ block._editSettings = function () {
var blockIndex = vm.layout.indexOf(this.layout);
editBlock(this, true, blockIndex, this._parentForm);
};
- block.requestDelete = requestDeleteBlock.bind(null, block);
- block.delete = deleteBlock.bind(null, block);
- block.copy = copyBlock.bind(null, block);
-
- return block;
+ block._requestDelete = requestDeleteBlock.bind(null, block);
+ block._delete = deleteBlock.bind(null, block);
+ block._copy = copyBlock.bind(null, block);
}
function addNewBlock(index, contentElementTypeKey) {
From c1dd0046040d4a5c97ea6bed420709119c4a71ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Wed, 5 May 2021 11:15:23 +0200
Subject: [PATCH 50/80] use warning color for .show-validation-type-warning
---
.../inlineblock/inlineblock.editor.less | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less
index 45a4c0859828..ffadc218663f 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less
@@ -60,8 +60,14 @@
ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & {
> button {
color: @formErrorText;
+ .show-validation-type-warning & {
+ color: @formWarningText;
+ }
span.caret {
border-top-color: @formErrorText;
+ .show-validation-type-warning & {
+ border-top-color: @formWarningText;
+ }
}
}
}
@@ -84,6 +90,9 @@
padding: 2px;
line-height: 10px;
background-color: @formErrorText;
+ .show-validation-type-warning & {
+ background-color: @formWarningText;
+ }
font-weight: 900;
animation-duration: 1.4s;
From 6e16550b849a5bbc987e066ea0936cbd0e555591 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20Lyngs=C3=B8?=
Date: Wed, 5 May 2021 11:18:46 +0200
Subject: [PATCH 51/80] Dont let validation issues prevent saving (#9691)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* skipValidation for content save
* Correcting merge
* Use warning style when saving
* final corrections
* skip client side validation
* remove log
* show invariant property validation issues in the save dialog
* use warning color for .show-validation-type-warning
Co-authored-by: Niels Lyngsø
Co-authored-by: Mads Rasmussen
---
.../components/content/edit.controller.js | 103 ++++++++++--------
.../validation/valformmanager.directive.js | 54 +++++++++
.../services/contenteditinghelper.service.js | 24 +++-
.../src/less/alerts.less | 17 +++
.../editor/umb-variant-switcher.less | 13 ++-
.../src/less/components/overlays.less | 3 +
.../umb-editor-navigation-item.less | 23 ++--
.../src/less/components/umb-list.less | 5 +-
.../less/components/umb-nested-content.less | 6 +-
.../src/less/components/umb-tabs.less | 7 ++
src/Umbraco.Web.UI.Client/src/less/forms.less | 10 ++
.../src/less/variables.less | 6 +-
.../src/views/content/overlays/save.html | 14 ++-
.../inlineblock/inlineblock.editor.less | 9 ++
.../labelblock/labelblock.editor.less | 6 +
.../umb-block-list-property-editor.less | 15 ++-
16 files changed, 239 insertions(+), 76 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
index da93450522fd..bce797d5c819 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
@@ -442,7 +442,6 @@
// This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish
function performSave(args) {
-
//Used to check validility of nested form - coming from Content Apps mostly
//Set them all to be invalid
var fieldsToRollback = checkValidility();
@@ -455,7 +454,8 @@
create: $scope.page.isNew,
action: args.action,
showNotifications: args.showNotifications,
- softRedirect: true
+ softRedirect: true,
+ skipValidation: args.skipValidation
}).then(function (data) {
//success
init();
@@ -467,23 +467,24 @@
eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: true });
- resetNestedFieldValiation(fieldsToRollback);
+ if($scope.contentForm.$invalid !== true) {
+ resetNestedFieldValiation(fieldsToRollback);
+ }
ensureDirtyIsSetIfAnyVariantIsDirty();
return $q.when(data);
},
function (err) {
-
-
syncTreeNode($scope.content, $scope.content.path);
+ if($scope.contentForm.$invalid !== true) {
+ resetNestedFieldValiation(fieldsToRollback);
+ }
if (err && err.status === 400 && err.data) {
// content was saved but is invalid.
eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: false });
}
- resetNestedFieldValiation(fieldsToRollback);
-
return $q.reject(err);
});
}
@@ -735,48 +736,48 @@
clearNotifications($scope.content);
// TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant
if (hasVariants($scope.content)) {
- //before we launch the dialog we want to execute all client side validations first
- if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog" })) {
-
- var dialog = {
- parentScope: $scope,
- view: "views/content/overlays/save.html",
- variants: $scope.content.variants, //set a model property for the dialog
- skipFormValidation: true, //when submitting the overlay form, skip any client side validation
- submitButtonLabelKey: "buttons_save",
- submit: function (model) {
- model.submitButtonState = "busy";
+ var dialog = {
+ parentScope: $scope,
+ view: "views/content/overlays/save.html",
+ variants: $scope.content.variants, //set a model property for the dialog
+ skipFormValidation: true, //when submitting the overlay form, skip any client side validation
+ submitButtonLabelKey: "buttons_save",
+ submit: function (model) {
+ model.submitButtonState = "busy";
+ clearNotifications($scope.content);
+ //we need to return this promise so that the dialog can handle the result and wire up the validation response
+ return performSave({
+ saveMethod: $scope.saveMethod(),
+ action: "save",
+ showNotifications: false,
+ skipValidation: true
+ }).then(function (data) {
+ //show all notifications manually here since we disabled showing them automatically in the save method
+ formHelper.showNotifications(data);
clearNotifications($scope.content);
- //we need to return this promise so that the dialog can handle the result and wire up the validation response
- return performSave({
- saveMethod: $scope.saveMethod(),
- action: "save",
- showNotifications: false
- }).then(function (data) {
- //show all notifications manually here since we disabled showing them automatically in the save method
- formHelper.showNotifications(data);
- clearNotifications($scope.content);
- overlayService.close();
- return $q.when(data);
- },
- function (err) {
- clearDirtyState($scope.content.variants);
- model.submitButtonState = "error";
- //re-map the dialog model since we've re-bound the properties
- dialog.variants = $scope.content.variants;
- handleHttpException(err);
- });
- },
- close: function (oldModel) {
overlayService.close();
- }
- };
+ return $q.when(data);
+ },
+ function (err) {
+ clearDirtyState($scope.content.variants);
+ //model.submitButtonState = "error";
+ // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely.
+ if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) {
+ model.submitButtonState = "success";
+ } else {
+ model.submitButtonState = "error";
+ }
+ //re-map the dialog model since we've re-bound the properties
+ dialog.variants = $scope.content.variants;
+ handleHttpException(err);
+ });
+ },
+ close: function (oldModel) {
+ overlayService.close();
+ }
+ };
- overlayService.open(dialog);
- }
- else {
- showValidationNotification();
- }
+ overlayService.open(dialog);
}
else {
//ensure the flags are set
@@ -784,11 +785,17 @@
$scope.page.saveButtonState = "busy";
return performSave({
saveMethod: $scope.saveMethod(),
- action: "save"
+ action: "save",
+ skipValidation: true
}).then(function () {
$scope.page.saveButtonState = "success";
}, function (err) {
- $scope.page.saveButtonState = "error";
+ // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely.
+ if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) {
+ $scope.page.saveButtonState = "success";
+ } else {
+ $scope.page.saveButtonState = "error";
+ }
handleHttpException(err);
});
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
index f34edf820bb3..d15ad6af518c 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
@@ -15,6 +15,7 @@
function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) {
var SHOW_VALIDATION_CLASS_NAME = "show-validation";
+ var SHOW_VALIDATION_Type_CLASS_NAME = "show-validation-type-";
var SAVING_EVENT_NAME = "formSubmitting";
var SAVED_EVENT_NAME = "formSubmitted";
@@ -44,6 +45,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
this.isShowingValidation = () => $scope.showValidation === true;
+ this.getValidationMessageType = () => $scope.valMsgType;
+
this.notify = notify;
this.isValid = function () {
@@ -94,6 +97,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1);
var subView = ctrls.length > 1 ? ctrls[2] : null;
var labels = {};
+ var valMsgType = 2;// error
var labelKeys = [
"prompt_unsavedChanges",
@@ -109,6 +113,45 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
labels.stayButton = values[3];
});
+ var lastValidationMessageType = null;
+ function setValidationMessageType(type) {
+
+ removeValidationMessageType();
+ scope.valMsgType = type;
+
+ // overall a copy of message types from notifications.service:
+ var postfix = "";
+ switch(type) {
+ case 0:
+ //save
+ break;
+ case 1:
+ //info
+ postfix = "info";
+ break;
+ case 2:
+ //error
+ postfix = "error";
+ break;
+ case 3:
+ //success
+ postfix = "success";
+ break;
+ case 4:
+ //warning
+ postfix = "warning";
+ break;
+ }
+ var cssClass = SHOW_VALIDATION_Type_CLASS_NAME+postfix;
+ element.addClass(cssClass);
+ lastValidationMessageType = cssClass;
+ }
+ function removeValidationMessageType() {
+ if(lastValidationMessageType) {
+ element.removeClass(lastValidationMessageType);
+ lastValidationMessageType = null;
+ }
+ }
// watch the list of validation errors to notify the application of any validation changes
scope.$watch(() => formCtrl.$invalid,
@@ -138,6 +181,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.isShowingValidation())) {
element.addClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = true;
+ var parentValMsgType = parentFormMgr ? parentFormMgr.getValidationMessageType() : 2;
+ setValidationMessageType(parentValMsgType || 2);
notifySubView();
}
@@ -145,8 +190,16 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
//listen for the forms saving event
unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) {
+
+ var messageType = 2;//error
+ switch (args.action) {
+ case "save":
+ messageType = 4;//warning
+ break;
+ }
element.addClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = true;
+ setValidationMessageType(messageType);
notifySubView();
//set the flag so we can check to see if we should display the error.
isSavingNewItem = $routeParams.create;
@@ -156,6 +209,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) {
//remove validation class
element.removeClass(SHOW_VALIDATION_CLASS_NAME);
+ removeValidationMessageType();
scope.showValidation = false;
notifySubView();
}));
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
index 8524b960c68d..bab665579c95 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
@@ -32,7 +32,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
return true;
}
- function showNotificationsForModelsState(ms) {
+ function showNotificationsForModelsState(ms, messageType) {
+ messageType = messageType || 2;
for (const [key, value] of Object.entries(ms)) {
var errorMsg = value[0];
@@ -42,12 +43,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
var idsToErrors = serverValidationManager.parseComplexEditorError(errorMsg, "");
idsToErrors.forEach(x => {
if (x.modelState) {
- showNotificationsForModelsState(x.modelState);
+ showNotificationsForModelsState(x.modelState, messageType);
}
});
}
else if (value[0]) {
- notificationsService.error("Validation", value[0]);
+ //notificationsService.error("Validation", value[0]);
+ console.log({type:messageType, header:"Validation", message:value[0]})
+ notificationsService.showNotification({type:messageType, header:"Validation", message:value[0]})
}
}
}
@@ -93,7 +96,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
//we will use the default one for content if not specified
var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback;
- if (formHelper.submitForm({ scope: args.scope, action: args.action })) {
+ var formSubmitOptions = { scope: args.scope, action: args.action };
+ if(args.skipValidation === true) {
+ formSubmitOptions.skipValidation = true;
+ formSubmitOptions.keepServerValidation = true;
+ }
+ if (formHelper.submitForm(formSubmitOptions)) {
return args.saveMethod(args.content, args.create, fileManager.getFiles(), args.showNotifications)
.then(function (data) {
@@ -124,6 +132,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
showNotifications: args.showNotifications,
softRedirect: args.softRedirect,
err: err,
+ action: args.action,
rebindCallback: function () {
// if the error contains data, we want to map that back as we want to continue editing this save. Especially important when the content is new as the returned data will contain ID etc.
if(err.data) {
@@ -639,9 +648,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
//wire up the server validation errs
formHelper.handleServerValidation(args.err.data.ModelState);
+ var messageType = 2;//error
+ if (args.action === "save") {
+ messageType = 4;//warning
+ }
+
//add model state errors to notifications
if (args.showNotifications) {
- showNotificationsForModelsState(args.err.data.ModelState);
+ showNotificationsForModelsState(args.err.data.ModelState, messageType);
}
if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) {
diff --git a/src/Umbraco.Web.UI.Client/src/less/alerts.less b/src/Umbraco.Web.UI.Client/src/less/alerts.less
index 3539e2106413..94dcef6f25db 100644
--- a/src/Umbraco.Web.UI.Client/src/less/alerts.less
+++ b/src/Umbraco.Web.UI.Client/src/less/alerts.less
@@ -54,6 +54,15 @@
border-color: @errorBorder;
color: @errorText;
}
+
+.alert-warning() {
+ background-color: @warningBackground;
+ border-color: @warningBorder;
+ color: @warningText;
+}
+.alert-warning {
+ .alert-warning()
+}
.alert-danger h4,
.alert-error h4 {
color: @errorText;
@@ -110,6 +119,14 @@
padding: 6px 16px 6px 12px;
margin-bottom: 6px;
+ .show-validation-type-warning & {
+ .alert-warning();
+ font-weight: bold;
+ &.alert-error::after {
+ border-top-color: @warningBackground;
+ }
+ }
+
&::after {
content:'';
position: absolute;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
index 9d2782f184c8..594558da51f3 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
@@ -50,6 +50,10 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
+ .show-validation-type-warning & {
+ background-color: @warningBackground;
+ color: @warningText;
+ }
animation-duration: 1.4s;
animation-iteration-count: infinite;
@@ -233,7 +237,10 @@ button.umb-variant-switcher__toggle {
.umb-variant-switcher__item.--error {
.umb-variant-switcher__name {
- color: @red;
+ color: @formErrorText;
+ .show-validation-type-warning & {
+ color: @formWarningText;
+ }
&::after {
content: '!';
position: relative;
@@ -250,6 +257,10 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
+ .show-validation-type-warning & {
+ background-color: @warningBackground;
+ color: @warningText;
+ }
animation-duration: 1.4s;
animation-iteration-count: infinite;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less
index 035bf02f910c..12cce286d6d7 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less
@@ -267,6 +267,9 @@
.umb-overlay .text-error {
color: @formErrorText;
}
+.umb-overlay .text-warning {
+ color: @formWarningText;
+}
.umb-overlay .text-success {
color: @formSuccessText;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
index 5e9772fb2631..5fd743aaf074 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
@@ -86,6 +86,16 @@
}
}
}
+
+ .show-validation.show-validation-type-warning &.-has-error {
+ color: @yellow-d2;
+ &:hover {
+ color: @yellow-d2 !important;
+ }
+ &::before {
+ background-color: @yellow-d2;
+ }
+ }
}
&__action:active,
@@ -122,14 +132,6 @@
line-height: 16px;
display: block;
- &.-type-alert {
- background-color: @red;
- }
-
- &.-type-warning {
- background-color: @yellow-d2;
- }
-
&:empty {
height: 12px;
min-width: 12px;
@@ -137,6 +139,11 @@
&.--error-badge {
display: none;
font-weight: 900;
+ background-color: @red;
+
+ .show-validation-type-warning & {
+ background-color: @yellow-d2;
+ }
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less
index 57ba73305a42..c281f7f5eafe 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less
@@ -26,7 +26,10 @@ a.umb-list-item:focus {
}
.umb-list-item--error {
- color: @red;
+ color: @formErrorText;
+}
+.umb-list-item--warning {
+ color: @formWarningText;
}
.umb-list-item:hover .umb-list-checkbox,
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 bd787e2329ff..9dd40a4386c9 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
@@ -48,6 +48,10 @@
&.--error {
border-color: @formErrorBorder !important;
}
+
+ .show-validation-type-warning &.--error {
+ border-color: @formWarningBorder !important;
+ }
}
.umb-nested-content__item.ui-sortable-placeholder {
@@ -292,4 +296,4 @@
.umb-textarea, .umb-textstring {
width:100%;
}
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less
index 15b317aa459b..1b249f1c3a62 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less
@@ -86,6 +86,13 @@
background-color: @red !important;
border-color: @errorBorder;
}
+.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button,
+.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:hover,
+.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:focus {
+ color: @white !important;
+ background-color: @yellow-d2 !important;
+ border-color: @warningBorder;
+}
.show-validation .umb-tab--error .umb-tab-button:before {
content: "\e25d";
diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less
index 3782fca695c2..60561f9accfc 100644
--- a/src/Umbraco.Web.UI.Client/src/less/forms.less
+++ b/src/Umbraco.Web.UI.Client/src/less/forms.less
@@ -506,10 +506,20 @@ input[type="checkbox"][readonly] {
.formFieldState(@formErrorText, @formErrorText, @formErrorBackground);
}
+// ValidationError as a warning
+.show-validation.show-validation-type-warning.ng-invalid .control-group.error,
+.show-validation.show-validation-type-warning.ng-invalid .umb-editor-header__name-wrapper {
+ .formFieldState(@formWarningText, @formWarningText, @formWarningBackground);
+}
+
//val-highlight directive styling
.highlight-error {
color: @formErrorText !important;
border-color: @red-l1 !important;
+ .show-validation-type-warning & {
+ color: @formWarningText !important;
+ border-color: @yellow-d2 !important;
+ }
}
// FORM ACTIONS
diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less
index cab0745a427e..9d114b093ee7 100644
--- a/src/Umbraco.Web.UI.Client/src/less/variables.less
+++ b/src/Umbraco.Web.UI.Client/src/less/variables.less
@@ -291,8 +291,8 @@
@btnSuccessBackground: @ui-btn-positive;// updated 2019
@btnSuccessBackgroundHighlight: @ui-btn-positive-hover;// updated 2019
-@btnWarningBackground: @orange;
-@btnWarningBackgroundHighlight: lighten(@orange, 10%);
+@btnWarningBackground: @yellow-d2;
+@btnWarningBackgroundHighlight: lighten(@yellow-d2, 10%);
@btnDangerBackground: @red;
@btnDangerBackgroundHighlight: @red-l1;
@@ -480,7 +480,7 @@
@formWarningBorder: darken(spin(@warningBackground, -10), 3%);
@formErrorText: @errorBackground;
-@formErrorBackground: lighten(@errorBackground, 55%);
+@formErrorBackground: @errorBackground;
@formErrorBorder: @red;
@formSuccessText: @successBackground;
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html
index fa9ab8c43777..36e01991dfa6 100644
--- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html
+++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html
@@ -16,7 +16,9 @@