diff --git a/src/Umbraco.Core/Scoping/ScopeContext.cs b/src/Umbraco.Core/Scoping/ScopeContext.cs index dd26fda85e..4ba1999474 100644 --- a/src/Umbraco.Core/Scoping/ScopeContext.cs +++ b/src/Umbraco.Core/Scoping/ScopeContext.cs @@ -7,27 +7,32 @@ namespace Umbraco.Core.Scoping internal class ScopeContext : IScopeContext, IInstanceIdentifiable { private Dictionary _enlisted; - private bool _exiting; public void ScopeExit(bool completed) { if (_enlisted == null) return; - _exiting = true; + // fixme - can we create infinite loops? + // fixme - what about nested events? will they just be plainly ignored = really bad? List exceptions = null; - foreach (var enlisted in _enlisted.Values.OrderBy(x => x.Priority)) + List orderedEnlisted; + while ((orderedEnlisted = _enlisted.Values.OrderBy(x => x.Priority).ToList()).Count > 0) { - try + _enlisted.Clear(); + foreach (var enlisted in orderedEnlisted) { - enlisted.Execute(completed); - } - catch (Exception e) - { - if (exceptions == null) - exceptions = new List(); - exceptions.Add(e); + try + { + enlisted.Execute(completed); + } + catch (Exception e) + { + if (exceptions == null) + exceptions = new List(); + exceptions.Add(e); + } } } @@ -74,9 +79,6 @@ namespace Umbraco.Core.Scoping public T Enlist(string key, Func creator, Action action = null, int priority = 100) { - if (_exiting) - throw new InvalidOperationException("Cannot enlist now, context is exiting."); - var enlistedObjects = _enlisted ?? (_enlisted = new Dictionary()); if (enlistedObjects.TryGetValue(key, out var enlisted)) diff --git a/src/Umbraco.Tests/Scoping/ScopeTests.cs b/src/Umbraco.Tests/Scoping/ScopeTests.cs index 3f2740291b..6c5e9a74b5 100644 --- a/src/Umbraco.Tests/Scoping/ScopeTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeTests.cs @@ -540,7 +540,7 @@ namespace Umbraco.Tests.Scoping var scopeProvider = ScopeProvider; bool? completed = null; - Exception exception = null; + bool? completed2 = null; Assert.IsNull(scopeProvider.AmbientScope); using (var scope = scopeProvider.CreateScope()) @@ -551,15 +551,7 @@ namespace Umbraco.Tests.Scoping // at that point the scope is gone, but the context is still there var ambientContext = scopeProvider.AmbientContext; - - try - { - ambientContext.Enlist("another", c2 => { }); - } - catch (Exception e) - { - exception = e; - } + ambientContext.Enlist("another", c2 => { completed2 = c2; }); }); if (complete) scope.Complete(); @@ -567,8 +559,8 @@ namespace Umbraco.Tests.Scoping Assert.IsNull(scopeProvider.AmbientScope); Assert.IsNull(scopeProvider.AmbientContext); Assert.IsNotNull(completed); - Assert.IsNotNull(exception); - Assert.IsInstanceOf(exception); + Assert.AreEqual(complete,completed.Value); + Assert.AreEqual(complete, completed2.Value); } [Test] diff --git a/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs b/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs index ba19f41e74..cc83dcb1c9 100644 --- a/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs @@ -29,5 +29,30 @@ namespace Umbraco.Tests.Web.Mvc var output = _htmlHelper.Wrap("div", "hello world", new {style = "color:red;", onclick = "void();"}); Assert.AreEqual("
hello world
", output.ToHtmlString()); } + + [Test] + public void GetRelatedLinkHtml_Simple() + { + var relatedLink = new Umbraco.Web.Models.RelatedLink { + Caption = "Link Caption", + NewWindow = true, + Link = "https://www.google.com/" + }; + var output = _htmlHelper.GetRelatedLinkHtml(relatedLink); + Assert.AreEqual("Link Caption", output.ToHtmlString()); + } + + [Test] + public void GetRelatedLinkHtml_HtmlAttributes() + { + var relatedLink = new Umbraco.Web.Models.RelatedLink + { + Caption = "Link Caption", + NewWindow = true, + Link = "https://www.google.com/" + }; + var output = _htmlHelper.GetRelatedLinkHtml(relatedLink, new { @class = "test-class"}); + Assert.AreEqual("Link Caption", output.ToHtmlString()); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 71fbabe943..b65bf447e7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -97,9 +97,9 @@ } //load in the audit trail if we are currently looking at the INFO tab - if (umbVariantContentCtrl) { + if (umbVariantContentCtrl && umbVariantContentCtrl.editor) { var activeApp = _.find(umbVariantContentCtrl.editor.content.apps, a => a.active); - if (activeApp.alias === "umbInfo") { + if (activeApp && activeApp.alias === "umbInfo") { isInfoTab = true; loadAuditTrail(); loadRedirectUrls(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js index 47d1431e13..56a18a217e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js @@ -86,20 +86,21 @@ angular.module("umbraco.directives") function generateAlias(value) { if (generateAliasTimeout) { - $timeout.cancel(generateAliasTimeout); + $timeout.cancel(generateAliasTimeout); } - if( value !== undefined && value !== "" && value !== null) { + if (value !== undefined && value !== "" && value !== null) { - scope.alias = ""; + scope.alias = ""; scope.placeholderText = scope.labels.busy; generateAliasTimeout = $timeout(function () { updateAlias = true; entityResource.getSafeAlias(value, true).then(function (safeAlias) { if (updateAlias) { - scope.alias = safeAlias.alias; - } + scope.alias = safeAlias.alias; + } + scope.placeholderText = scope.labels.idle; }); }, 500); @@ -108,7 +109,6 @@ angular.module("umbraco.directives") scope.alias = ""; scope.placeholderText = scope.labels.idle; } - } // if alias gets unlocked - stop watching alias @@ -119,17 +119,17 @@ angular.module("umbraco.directives") })); // validate custom entered alias - eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ - - if(scope.alias === "" && bindWatcher === true || scope.alias === null && bindWatcher === true) { - // add watcher - eventBindings.push(scope.$watch('aliasFrom', function(newValue, oldValue) { - if(bindWatcher) { - generateAlias(newValue); - } - })); - } - + eventBindings.push(scope.$watch('alias', function (newValue, oldValue) { + if (scope.alias === "" || scope.alias === null || scope.alias === undefined) { + if (bindWatcher === true) { + // add watcher + eventBindings.push(scope.$watch('aliasFrom', function (newValue, oldValue) { + if (bindWatcher) { + generateAlias(newValue); + } + })); + } + } })); // clean up diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js index bb1dad1dbd..1fe739154a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.codefileResource * @description Loads in data for files that contain code such as js scripts, partial views and partial view macros **/ -function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { +function codefileResource($q, $http, umbDataFormatter, umbRequestHelper, localizationService) { return { @@ -106,13 +106,16 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ deleteByPath: function (type, virtualpath) { + + var promise = localizationService.localize("codefile_deleteItemFailed", [virtualpath]); + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "codeFileApiBaseUrl", "Delete", [{ type: type }, { virtualPath: virtualpath}])), - "Failed to delete item: " + virtualpath); + promise); }, /** @@ -236,13 +239,19 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ - createContainer: function(type, parentId, name) { + createContainer: function (type, parentId, name) { + + // Is the parent ID numeric? + var key = "codefile_createFolderFailedBy" + (isNaN(parseInt(parentId)) ? "Name" : "Id"); + + var promise = localizationService.localize(key, [parentId]); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl( "codeFileApiBaseUrl", "PostCreateContainer", { type: type, parentId: parentId, name: encodeURIComponent(name) })), - 'Failed to create a folder under parent id ' + parentId); + promise); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js index f969864ba1..377bb415fc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.templateResource * @description Loads in data for templates **/ -function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { +function templateResource($q, $http, umbDataFormatter, umbRequestHelper, localizationService) { return { @@ -152,13 +152,16 @@ function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ deleteById: function(id) { + + var promise = localizationService.localize("template_deleteByIdFailed", [id]); + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "templateApiBaseUrl", "DeleteById", [{ id: id }])), - "Failed to delete item " + id); + promise); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js index 06b82d6eab..e023c6d23c 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js @@ -9,7 +9,7 @@ * * @param {navigationService} navigationService A reference to the navigationService */ -function NavigationController($scope, $rootScope, $location, $log, $q, $routeParams, $timeout, treeService, appState, navigationService, keyboardService, historyService, eventsService, angularHelper, languageResource, contentResource) { +function NavigationController($scope, $rootScope, $location, $log, $q, $routeParams, $timeout, $cookies, treeService, appState, navigationService, keyboardService, historyService, eventsService, angularHelper, languageResource, contentResource) { //this is used to trigger the tree to start loading once everything is ready var treeInitPromise = $q.defer(); @@ -344,9 +344,6 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.languages = languages; if ($scope.languages.length > 1) { - var defaultLang = _.find($scope.languages, function (l) { - return l.isDefault; - }); //if there's already one set, check if it exists var currCulture = null; var mainCulture = $location.search().mculture; @@ -356,7 +353,20 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar }); } if (!currCulture) { - $location.search("mculture", defaultLang ? defaultLang.culture : null); + // no culture in the request, let's look for one in the cookie that's set when changing language + var defaultCulture = $cookies.get("UMB_MCULTURE"); + if (!defaultCulture || !_.find($scope.languages, function (l) { + return l.culture.toLowerCase() === defaultCulture.toLowerCase(); + })) { + // no luck either, look for the default language + var defaultLang = _.find($scope.languages, function (l) { + return l.isDefault; + }); + if (defaultLang) { + defaultCulture = defaultLang.culture; + } + } + $location.search("mculture", defaultCulture ? defaultCulture : null); } } @@ -391,6 +401,10 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.selectLanguage = function (language) { $location.search("mculture", language.culture); + // add the selected culture to a cookie so the user will log back into the same culture later on (cookie lifetime = one year) + var expireDate = new Date(); + expireDate.setDate(expireDate.getDate() + 365); + $cookies.put("UMB_MCULTURE", language.culture, {path: "/", expires: expireDate}); // close the language selector $scope.page.languageSelectorIsOpen = false; diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index a28c128706..a65b94444e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -161,6 +161,7 @@ @import "components/umb-file-dropzone.less"; @import "components/umb-node-preview.less"; @import "components/umb-mini-editor.less"; +@import "components/umb-property-file-upload.less"; @import "components/users/umb-user-cards.less"; @import "components/users/umb-user-details.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less index 4803c05f6e..b7c58ad3cf 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less @@ -1,5 +1,5 @@ -.umb-file-dropzone-directive{ +.umb-file-dropzone { // drop zone // tall and small version - animate height 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 df8977a2bf..6ddd9d8d50 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 @@ -3,6 +3,13 @@ position: relative; } +.umb-nested-content-property-container { + position: relative; + &:not(:last-child){ + margin-bottom: 12px; + } +} + .umb-nested-content--not-supported { opacity: 0.3; pointer-events: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less new file mode 100644 index 0000000000..08b1a1b5e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less @@ -0,0 +1,28 @@ +.umb-property-file-upload { + + .umb-upload-button-big { + display: block; + padding: 20px; + opacity: 1; + border: 1px dashed @gray-8; + background: none; + text-align: center; + font-size: 14px; + + &, &:hover { + color: @gray-8; + } + + i.icon { + font-size: 55px; + line-height: 70px + } + + input { + left: 0; + bottom: 0; + height: 100%; + width: 100%; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index e2d4cb708d..3ec8c383da 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -1,3 +1,5 @@ +@checkered-background: url(../img/checkered-background.png); + // // Container styles // -------------------------------------------------- @@ -353,7 +355,7 @@ max-height:100%; margin:auto; display:block; - background-image: url(../img/checkered-background.png); + background-image: @checkered-background; } .umb-sortable-thumbnails li .trashed { @@ -576,12 +578,18 @@ vertical-align: top; } - .gravity-container .viewport { - max-width: 600px; - } + .gravity-container { + border: 1px solid @gray-8; + line-height: 0; - .gravity-container .viewport:hover { - cursor: pointer; + .viewport { + max-width: 600px; + background: @checkered-background; + + &:hover { + cursor: pointer; + } + } } .imagecropper { @@ -594,6 +602,10 @@ float: left; max-width: 100%; } + + .viewport img { + background: @checkered-background; + } } .imagecropper .umb-cropper__container { @@ -687,7 +699,7 @@ // // folder-browser // -------------------------------------------------- -.umb-folderbrowser .add-link{ +.umb-folderbrowser .add-link { display: inline-block; height: 120px; width: 120px; @@ -696,17 +708,6 @@ line-height: 120px } -.umb-upload-button-big:hover{color: @gray-8;} - -.umb-upload-button-big {display: block} -.umb-upload-button-big input { - left: 0; - bottom: 0; - height: 100%; - width: 100%; -} - - // // File upload // -------------------------------------------------- @@ -724,6 +725,10 @@ list-style: none; vertical-align: middle; margin-bottom: 0; + + img { + background: @checkered-background; + } } .umb-fileupload label { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 2591789a36..360c6b2bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -24,7 +24,7 @@ - .{{item.extension}} + .{{item.extension}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html index 9d2997625e..c99655b7fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index f205dffa57..dea532b7ea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -1,4 +1,5 @@ -
+
+
0 && userData.startContentIds.indexOf(-1) == -1; + }); + + function nodeSelectHandler(args) { + + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + + } + + function nodeExpandedHandler(args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } + + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.results = []; + } + + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + }; + + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); + } + + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + + function openMiniListView(node) { + $scope.miniListView = node; + } relationResource.getByChildId($scope.source.id, "relateParentDocumentOnDelete").then(function (data) { $scope.loading = false; if (!data.length) { - localizationService.localizeMany(["recycleBin_itemCannotBeRestored", "recycleBin_noRestoreRelation"]) - .then(function(values) { - $scope.success = false; - $scope.error = { - errorMsg: values[0], - data: { - Message: values[1] - } - } - }); + $scope.moving = true; return; } @@ -30,40 +95,32 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController" $scope.target = { id: -1, name: "Root" }; } else { - $scope.loading = true; + $scope.loading = true; + entityResource.getById($scope.relation.parentId, "Document").then(function (data) { $scope.loading = false; - $scope.target = data; - // make sure the target item isn't in the recycle bin - if($scope.target.path.indexOf("-20") !== -1) { - localizationService.localizeMany(["recycleBin_itemCannotBeRestored", "recycleBin_restoreUnderRecycled"]) - .then(function (values) { - $scope.success = false; - $scope.error = { - errorMsg: values[0], - data: { - Message: values[1].replace('%0%', $scope.target.name) - } - } - }); - $scope.success = false; - } + $scope.target = data; + // make sure the target item isn't in the recycle bin + if ($scope.target.path.indexOf("-20") !== -1) { + $scope.moving = true; + $scope.target = null; + } }, function (err) { - $scope.success = false; - $scope.error = err; $scope.loading = false; + $scope.error = err; }); } }, function (err) { - $scope.success = false; - $scope.error = err; + $scope.loading = false; + $scope.error = err; }); $scope.restore = function () { $scope.loading = true; - // this code was copied from `content.move.controller.js` + + // this code was copied from `content.move.controller.js` contentResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { @@ -88,9 +145,8 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController" }); }, function (err) { - $scope.success = false; - $scope.error = err; $scope.loading = false; + $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/restore.html b/src/Umbraco.Web.UI.Client/src/views/content/restore.html index 83a69effb6..ead6ed91ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/restore.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/restore.html @@ -1,34 +1,93 @@
-
- +
+ + + - - +
+
+
{{error.errorMsg}}
+
{{error.data.Message}}
+
+
-

- Restore {{source.name}} under {{target.name}}? -

+
+
+ {{source.name}} + was restored under + was moved underneath + {{target.name}} +
+ +
-
-
-
{{error.errorMsg}}
-
{{error.data.Message}}
-
-
+
-
-
- {{source.name}} was moved underneath {{target.name}} -
- -
+

+ Restore {{source.name}} under {{target.name}}? +

- +
+ +
+
+
+
Cannot automatically restore this item
+
There is no location where this item can be automatically restored. You can move the item manually using the tree below.
+
+
+ +
+ + + +
+ + + + +
+ + +
+
+ + + + +
+ +
- + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html b/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html index 1165776be6..3cca62b2b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html @@ -57,7 +57,7 @@
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.controller.js index b25ac3c7b8..1e615ad0d4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.controller.js @@ -12,6 +12,9 @@ function PartialViewsDeleteController($scope, codefileResource, treeService, nav //mark it for deletion (used in the UI) $scope.currentNode.loading = true; + + // Reset the error message + $scope.error = null; codefileResource.deleteByPath('partialViews', $scope.currentNode.id) .then(function() { @@ -21,6 +24,9 @@ function PartialViewsDeleteController($scope, codefileResource, treeService, nav //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); + }, function (err) { + $scope.currentNode.loading = false; + $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html index 0f75e8514e..c0fdf2c77f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html @@ -1,6 +1,13 @@
+
+
+
{{error.errorMsg}}
+
{{error.data.message}}
+
+
+

Are you sure you want to delete {{currentNode.name}} ?

diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 6e67d2d251..79e00fa453 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -81,25 +81,6 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop : undefined; }); - $scope.editIconTitle = ''; - $scope.moveIconTitle = ''; - $scope.deleteIconTitle = ''; - - // localize the edit icon title - localizationService.localize('general_edit').then(function (value) { - $scope.editIconTitle = value; - }); - - // localize the delete icon title - localizationService.localize('general_delete').then(function (value) { - $scope.deleteIconTitle = value; - }); - - // localize the move icon title - localizationService.localize('actions_move').then(function (value) { - $scope.moveIconTitle = value; - }); - $scope.nodes = []; $scope.currentNode = undefined; $scope.realCurrentNode = undefined; @@ -116,6 +97,11 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.showIcons = $scope.model.config.showIcons || true; $scope.wideMode = $scope.model.config.hideLabel == "1"; + $scope.labels = {}; + localizationService.localizeMany(["grid_insertControl"]).then(function(data) { + $scope.labels.docTypePickerTitle = data[0]; + }); + // helper to force the current form into the dirty state $scope.setDirty = function () { if ($scope.propertyForm) { @@ -138,7 +124,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop } $scope.overlayMenu = { - title: localizationService.localize('grid_insertControl'), + title: $scope.labels.docTypePickerTitle, show: false, style: {}, filter: $scope.scaffolds.length > 15 ? true : false, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html index 83076b54a0..0cf67022c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -1,5 +1,5 @@ 
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index 1ff6666907..572021aebd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -12,13 +12,13 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index 688ac7693f..a61930f877 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -1,9 +1,12 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.TagsController", - function ($scope) { + function ($scope, angularHelper) { $scope.valueChanged = function(value) { $scope.model.value = value; + // the model value seems to be a reference to the same array, so we need + // to set the form as dirty explicitly when the content of the array changes + angularHelper.getCurrentForm($scope).$setDirty(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/delete.controller.js index 8995cb1a31..d019a44a10 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/delete.controller.js @@ -12,6 +12,10 @@ function TemplatesDeleteController($scope, templateResource , treeService, navig //mark it for deletion (used in the UI) $scope.currentNode.loading = true; + + // Reset the error message + $scope.error = null; + templateResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; @@ -21,6 +25,9 @@ function TemplatesDeleteController($scope, templateResource , treeService, navig //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); + }, function (err) { + $scope.currentNode.loading = false; + $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/delete.html b/src/Umbraco.Web.UI.Client/src/views/templates/delete.html index 34648aa43e..c98677f764 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/delete.html @@ -1,11 +1,18 @@
+
+
+
{{error.errorMsg}}
+
{{error.data.message}}
+
+
+

Are you sure you want to delete {{currentNode.name}} ?

- +
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 383dc855fd..7a370ba90e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -180,6 +180,11 @@ Overførsel af egenskaber kunne ikke fuldføres, da en eller flere egenskaber er indstillet til at blive overført mere end én gang. Kun andre dokumenttyper, der er gyldige på denne placering, vises. + + Oprettelse af mappen under parent med ID %0% fejlede + Oprettelse af mappen under parent med navnet %0% fejlede + Sletning af filen/mappen fejlede: %0% + Udgivet Om siden @@ -276,6 +281,7 @@ Hvor ønsker du at oprette den nye %0% Opret under Vælg den dokumenttype, du vil oprette en indholdsskabelon til + Angiv et navn for mappen Vælg en type og skriv en titel "dokument typer".]]> "media typer".]]> @@ -1062,6 +1068,7 @@ Mange hilsner fra Umbraco robotten Editor + Sletning af skabelonen med ID %0% fejlede Rediger skabelon Sektioner Indsæt indholdsområde diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 69f106a775..386d3af518 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -189,6 +189,11 @@ Could not complete property mapping as one or more properties have more than one mapping defined. Only alternate types valid for the current location are displayed. + + Failed to create a folder under parent with ID %0% + Failed to create a folder under parent with name %0% + Failed to delete item: %0% + Is Published About this page @@ -291,6 +296,7 @@ Where do you want to create the new %0% Create an item under Select the document type you want to make a content template for + Enter a folder name Choose a type and a title "document types".]]> "media types".]]> @@ -1248,6 +1254,7 @@ To manage your website, simply open the Umbraco back office and start adding con This Content Type uses as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template Add icon @@ -1347,6 +1354,7 @@ To manage your website, simply open the Umbraco back office and start adding con Editor + Failed to delete template with ID %0% Edit template Sections Insert content area @@ -1968,8 +1976,8 @@ To manage your website, simply open the Umbraco back office and start adding con Trashed content with Id: {0} related to original parent content with Id: {1} Trashed media with Id: {0} related to original parent media item with Id: {1} Cannot automatically restore this item - There is no 'restore' relation found for this node. Use the Move menu item to move it manually. - The item you want to restore it under ('%0%') is in the recycle bin. Use the Move menu item to move the item manually. + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under Direction 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 eaa1c6c39e..5de373f571 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -196,6 +196,11 @@ Could not complete property mapping as one or more properties have more than one mapping defined. Only alternate types valid for the current location are displayed. + + Failed to create a folder under parent with ID %0% + Failed to create a folder under parent with name %0% + Failed to delete item: %0% + Is Published About this page @@ -316,6 +321,7 @@ Where do you want to create the new %0% Create an item under Select the document type you want to make a content template for + Enter a folder name Choose a type and a title "document types".]]> "media types".]]> @@ -1272,6 +1278,7 @@ To manage your website, simply open the Umbraco back office and start adding con This Content Type uses as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template Add icon @@ -1387,6 +1394,7 @@ To manage your website, simply open the Umbraco back office and start adding con How the text will look like in the rich text editor. + Failed to delete template with ID %0% Edit template Sections Insert content area @@ -2024,8 +2032,8 @@ To manage your website, simply open the Umbraco back office and start adding con Trashed content with Id: {0} related to original parent content with Id: {1} Trashed media with Id: {0} related to original parent media item with Id: {1} Cannot automatically restore this item - There is no 'restore' relation found for this node. Use the Move menu item to move it manually. - The item you want to restore it under ('%0%') is in the recycle bin. Use the Move menu item to move the item manually. + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under Direction diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs index 81b133b9ef..d522e54de6 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs @@ -142,8 +142,8 @@ namespace Umbraco.Web.Cache () => ContentService.Saved -= ContentService_Saved); Bind(() => ContentService.Copied += ContentService_Copied, // needed for permissions () => ContentService.Copied -= ContentService_Copied); - Bind(() => ContentService.TreeChanged += ContentService_Changed,// handles all content changes - () => ContentService.TreeChanged -= ContentService_Changed); + Bind(() => ContentService.TreeChanged += ContentService_TreeChanged,// handles all content changes + () => ContentService.TreeChanged -= ContentService_TreeChanged); // TreeChanged should also deal with this //Bind(() => ContentService.SavedBlueprint += ContentService_SavedBlueprint, @@ -206,7 +206,7 @@ namespace Umbraco.Web.Cache { } - private void ContentService_Changed(IContentService sender, TreeChange.EventArgs args) + private void ContentService_TreeChanged(IContentService sender, TreeChange.EventArgs args) { _distributedCache.RefreshContentCache(args.Changes.ToArray()); } diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs index 2b2bf337de..985c42bbbf 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs @@ -53,6 +53,16 @@ namespace Umbraco.Web.Editors.Filters if (currentUser.IsAdmin()) return Attempt.Succeed(); + var existingGroups = _userService.GetUserGroupsByAlias(groupAliases); + + if(!existingGroups.Any()) + { + // We're dealing with new groups, + // so authorization should be given to any user with access to Users section + if (currentUser.AllowedSections.Contains(Constants.Applications.Users)) + return Attempt.Succeed(); + } + var userGroups = currentUser.Groups.Select(x => x.Alias).ToArray(); var missingAccess = groupAliases.Except(userGroups).ToArray(); return missingAccess.Length == 0 diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index 677890bcc4..5069d99cfe 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -31,6 +31,7 @@ namespace Umbraco.Web.Editors //authorize that the user has access to save this user group var authHelper = new UserGroupEditorAuthorizationHelper( Services.UserService, Services.ContentService, Services.MediaService, Services.EntityService); + var isAuthorized = authHelper.AuthorizeGroupAccess(Security.CurrentUser, userGroupSave.Alias); if (isAuthorized == false) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result)); @@ -51,6 +52,14 @@ namespace Umbraco.Web.Editors if (isAuthorized == false) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result)); + //current user needs to be added to a new group if not an admin (possibly only if no other users are added?) to avoid a 401 + if(!Security.CurrentUser.IsAdmin() && (userGroupSave.Id == null || Convert.ToInt32(userGroupSave.Id) >= 0)/* && !userGroupSave.Users.Any() */) + { + var userIds = userGroupSave.Users.ToList(); + userIds.Add(Security.CurrentUser.Id); + userGroupSave.Users = userIds; + } + //save the group Services.UserService.Save(userGroupSave.PersistedUserGroup, userGroupSave.Users.ToArray()); diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 2e82fe252d..9c30f74ea7 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; +using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.Security; using Current = Umbraco.Web.Composing.Current; @@ -831,5 +832,37 @@ namespace Umbraco.Web #endregion + #region RelatedLink + + /// + /// Renders an anchor element for a RelatedLink instance. + /// Format: <a href="relatedLink.Link" target="_blank/_self">relatedLink.Caption</a> + /// + /// The HTML helper instance that this method extends. + /// The RelatedLink instance + /// An anchor element + public static MvcHtmlString GetRelatedLinkHtml(this HtmlHelper htmlHelper, RelatedLink relatedLink) + { + return htmlHelper.GetRelatedLinkHtml(relatedLink, null); + } + + /// + /// Renders an anchor element for a RelatedLink instance, accepting htmlAttributes. + /// Format: <a href="relatedLink.Link" target="_blank/_self" htmlAttributes>relatedLink.Caption</a> + /// + /// The HTML helper instance that this method extends. + /// The RelatedLink instance + /// An object that contains the HTML attributes to set for the element. + /// + public static MvcHtmlString GetRelatedLinkHtml(this HtmlHelper htmlHelper, RelatedLink relatedLink, object htmlAttributes) + { + var tagBuilder = new TagBuilder("a"); + tagBuilder.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + tagBuilder.MergeAttribute("href", relatedLink.Link); + tagBuilder.MergeAttribute("target", relatedLink.NewWindow ? "_blank" : "_self"); + tagBuilder.InnerHtml = HttpUtility.HtmlEncode(relatedLink.Caption); + return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.Normal)); + } + #endregion } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 3aa33f0e2f..0647e1c806 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Web.Hosting; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; @@ -15,7 +14,6 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; @@ -26,7 +24,6 @@ using Umbraco.Core.Services.Implement; using Umbraco.Web.Cache; using Umbraco.Web.Install; using Umbraco.Web.PublishedCache.NuCache.DataSource; -using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Routing; namespace Umbraco.Web.PublishedCache.NuCache