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 3a874f83c6..69cde0ba7d 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 @@ -26,26 +26,26 @@ $scope.allowOpen = true; $scope.app = null; - function init() { - + function init(firstLoad) { + var content = $scope.content; - + // we need to check wether 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.) - _.forEach(content.apps, function(app) { + _.forEach(content.apps, function (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) { - _.forEach(content.apps, function(app) { + _.forEach(content.apps, function (app) { if (app.alias === $scope.app.alias) { isAppPresent = true; app.active = true; @@ -53,7 +53,7 @@ } }); } - + } // if we still dont have a app, lets show the first one: @@ -61,7 +61,23 @@ content.apps[0].active = true; $scope.appChanged(content.apps[0]); } - + + if (firstLoad) { + + //Check if the editor state is holding a reference to a new item and merge it's notifications. + //We need to do this when creating new content because when it's created, we redirect to a new route and re-load the content from the server + //but we need to maintain it's 'notifications' data so we can re-bind it in the UI. + //The only time that editorState will contain something here is if it was preserved and that is only explicitly done when creating new content and redirecting. + + var currState = editorState.getCurrent(); + if (currState && currState.udi === content.udi) { + content.notifications = currState.notifications; + for (var i = 0; i < content.variants.length; i++) { + content.variants[i].notifications = currState.variants[i].notifications; + } + } + } + editorState.set(content); //We fetch all ancestors of the node to generate the footer breadcrumb navigation @@ -118,10 +134,10 @@ function isContentCultureVariant() { return $scope.content.variants.length > 1; } - + function reload() { $scope.page.loading = true; - loadContent().then(function() { + loadContent().then(function () { $scope.page.loading = false; }); } @@ -134,7 +150,7 @@ evts.push(eventsService.on("editors.documentType.saved", function (name, args) { // if this content item uses the updated doc type we need to reload the content item - if(args && args.documentType && $scope.content.documentType.id === args.documentType.id) { + if (args && args.documentType && $scope.content.documentType.id === args.documentType.id) { reload(); } })); @@ -144,7 +160,7 @@ /** * This does the content loading and initializes everything, called on first load */ - function loadContent() { + function loadContent(firstLoad) { //we are editing so get the content item from the server return $scope.getMethod()($scope.contentId) @@ -158,7 +174,7 @@ "/content/content/edit/" + data.parentId; } - init(); + init(firstLoad); syncTreeNode($scope.content, $scope.content.path, true); @@ -219,7 +235,7 @@ $scope.page.showPreviewButton = true; } - + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ function syncTreeNode(content, path, initialLoad) { @@ -328,7 +344,7 @@ $scope.contentForm.$dirty = false; for (var i = 0; i < $scope.content.variants.length; i++) { - if($scope.content.variants[i].isDirty){ + if ($scope.content.variants[i].isDirty) { $scope.contentForm.$dirty = true; return; } @@ -337,7 +353,7 @@ // 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(); @@ -364,11 +380,6 @@ function (err) { syncTreeNode($scope.content, $scope.content.path); - //error - if (err) { - editorState.set($scope.content); - } - resetNestedFieldValiation(fieldsToRollback); return $q.reject(err); @@ -421,9 +432,9 @@ //need to show a notification else it's not clear there was an error. localizationService.localizeMany([ - "speechBubbles_validationFailedHeader", - "speechBubbles_validationFailedMessage" - ] + "speechBubbles_validationFailedHeader", + "speechBubbles_validationFailedMessage" + ] ).then(function (data) { notificationsService.error(data[0], data[1]); }); @@ -453,7 +464,7 @@ $scope.page.loading = true; - loadContent().then(function () { + loadContent(true).then(function () { $scope.page.loading = false; }); } @@ -510,7 +521,7 @@ 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_saveToPublish", - submit: function(model) { + 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 @@ -518,14 +529,14 @@ saveMethod: contentResource.sendToPublish, action: "sendToPublish", 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) { + }).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 @@ -534,7 +545,7 @@ return $q.when(err); }); }, - close: function() { + close: function () { overlayService.close(); } }; @@ -570,7 +581,7 @@ 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_saveAndPublish", - submit: function(model) { + 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 @@ -578,14 +589,14 @@ saveMethod: contentResource.publish, action: "publish", 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) { + }).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 @@ -594,7 +605,7 @@ return $q.when(err); }); }, - close: function() { + close: function () { overlayService.close(); } }; @@ -634,7 +645,7 @@ 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) { + 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 @@ -642,14 +653,14 @@ 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) { + }).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 @@ -658,7 +669,7 @@ return $q.when(err); }); }, - close: function(oldModel) { + close: function (oldModel) { overlayService.close(); } }; @@ -685,7 +696,7 @@ }; - $scope.schedule = function() { + $scope.schedule = function () { clearNotifications($scope.content); //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "schedule" })) { @@ -755,7 +766,7 @@ } }; - $scope.publishDescendants = function() { + $scope.publishDescendants = function () { clearNotifications($scope.content); //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publishDescendants" })) { @@ -883,13 +894,13 @@ * @param {any} app */ $scope.appChanged = function (app) { - + $scope.app = app; - + $scope.$broadcast("editors.apps.appChanged", { app: app }); - + createButtons($scope.content); - + }; /** @@ -907,11 +918,11 @@ $scope.infiniteModel.close($scope.infiniteModel); } }; - + /** * Call back when user click the back-icon */ - $scope.onBack = function() { + $scope.onBack = function () { if ($scope.infiniteModel && $scope.infiniteModel.close) { $scope.infiniteModel.close($scope.infiniteModel); } else { 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 1b2be5f635..f54accbba0 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 @@ -5,7 +5,7 @@ * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by * all editors to share logic and reduce the amount of replicated code among editors. **/ -function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, navigationService, localizationService, serverValidationManager, formHelper) { +function contentEditingHelper(fileManager, $q, $location, $routeParams, editorState, notificationsService, navigationService, localizationService, serverValidationManager, formHelper) { function isValidIdentifier(id) { @@ -35,8 +35,6 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica return { /** Used by the content editor and mini content editor to perform saving operations */ - // TODO: Make this a more helpful/reusable method for other form operations! we can simplify this form most forms - // = this is already done in the formhelper service contentEditorPerformSave: function (args) { if (!angular.isObject(args)) { throw "args must be an object"; @@ -62,9 +60,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //we will use the default one for content if not specified var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; - if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, action: args.action })) { - - args.scope.busy = true; + if (formHelper.submitForm({ scope: args.scope, action: args.action })) { return args.saveMethod(args.content, $routeParams.create, fileManager.getFiles(), args.showNotifications) .then(function (data) { @@ -80,7 +76,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } }); - args.scope.busy = false; + //update editor state to what is current + editorState.set(args.content); + return $q.resolve(data); }, function (err) { @@ -93,7 +91,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } }); - args.scope.busy = false; + //update editor state to what is current + editorState.set(args.content); + return $q.reject(err); }); } @@ -265,7 +265,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica // if publishing is allowed also allow schedule publish // 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")) { + if (_.contains(args.content.allowedActions, "U")) { buttons.subButtons.push(createButtonDefinition("SCHEDULE")); buttons.subButtons.push(createButtonDefinition("PUBLISH_DESCENDANTS")); } @@ -274,7 +274,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica // so long as it's already published and if the user has access to publish // and the user has access to unpublish (may have been removed via Event) if (!args.create) { - var hasPublishedVariant = args.content.variants.filter(function(variant) { return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); }).length > 0; + var hasPublishedVariant = args.content.variants.filter(function (variant) { return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); }).length > 0; if (hasPublishedVariant && _.contains(args.content.allowedActions, "U") && _.contains(args.content.allowedActions, "Z")) { buttons.subButtons.push(createButtonDefinition("Z")); } @@ -444,7 +444,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica var shouldIgnore = function (propName) { return _.some([ "variants", - + "tabs", "properties", "apps", @@ -600,16 +600,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } + // we need to detect what properties have changed and re-bind them with the server data. + if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + args.rebindCallback(); + } + if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) { - //we are not redirecting because this is not new content, it is existing content. In this case - // we need to detect what properties have changed and re-bind them with the server data. Then we need - // to re-bind any server validation errors after the digest takes place. - - if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { - args.rebindCallback(); - } - - //notify all validators (don't clear the server validations though since we need to maintain their state because of + // we are not redirecting because this is not new content, it is existing content. In this case + // notify all validators (don't clear the server validations though since we need to maintain their state because of // how the variant switcher works in content). server validation state is always cleared when an editor first loads // and in theory when an editor is destroyed. serverValidationManager.notify(); @@ -680,6 +678,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //clear the query strings navigationService.clearSearch(["cculture"]); + //preserve the editor state when we are redirecting to new content + editorState.preserve(); + //change to new path $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); //don't add a browser history for this diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js index 5e42af9c5e..acfef46e75 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js @@ -5,13 +5,16 @@ * * @description * Tracks the parent object for complex editors by exposing it as - * an object reference via editorState.current.entity + * an object reference via editorState.current.getCurrent(). + * The state is cleared on each successful route. * * it is possible to modify this object, so should be used with care */ -angular.module('umbraco.services').factory("editorState", function() { +angular.module('umbraco.services').factory("editorState", function ($rootScope) { var current = null; + var preserveBetweenRoute = false; + var state = { /** @@ -40,7 +43,7 @@ angular.module('umbraco.services').factory("editorState", function() { * Since the editorstate entity is read-only, you cannot set it to null * only through the reset() method */ - reset: function() { + reset: function () { current = null; }, @@ -59,8 +62,25 @@ angular.module('umbraco.services').factory("editorState", function() { * editorState.current can not be overwritten, you should only read values from it * since modifying individual properties should be handled by the property editors */ - getCurrent: function() { + getCurrent: function () { return current; + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#preserve + * @methodOf umbraco.services.editorState + * @function + * + * @description + * When called it will flag the state to be preserved after the next route. + * Normally the editorState is cleared on each successful route but in some cases it should be preserved so calling this will preserve it. + * + * editorState.current can not be overwritten, you should only read values from it + * since modifying individual properties should be handled by the property editors + */ + preserve: function () { + preserveBetweenRoute = true; } }; @@ -76,5 +96,18 @@ angular.module('umbraco.services').factory("editorState", function() { } }); + //execute on each successful route (this is only bound once per application since a service is a singleton) + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + + if (!preserveBetweenRoute) { + //reset the editorState on each successful route chage + state.reset(); + } + + //always reset this + preserveBetweenRoute = false; + + }); + return state; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js index 6d319ad90a..8fe6761c94 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js @@ -8,11 +8,12 @@ * that need to attach files. * When a route changes successfully, we ensure that the collection is cleared. */ -function fileManager() { +function fileManager($rootScope) { var fileCollection = []; - return { + + var mgr = { /** * @ngdoc function * @name umbraco.services.fileManager#addFiles @@ -24,7 +25,7 @@ function fileManager() { * for the files collection that effectively clears the files for the specified editor. */ setFiles: function (args) { - + //propertyAlias, files if (!angular.isString(args.propertyAlias)) { throw "args.propertyAlias must be a non empty string"; @@ -52,7 +53,7 @@ function fileManager() { fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, metaData: metaData }); } }, - + /** * @ngdoc function * @name umbraco.services.fileManager#getFiles @@ -62,10 +63,10 @@ function fileManager() { * @description * Returns all of the files attached to the file manager */ - getFiles: function() { + getFiles: function () { return fileCollection; }, - + /** * @ngdoc function * @name umbraco.services.fileManager#clearFiles @@ -78,7 +79,17 @@ function fileManager() { clearFiles: function () { fileCollection = []; } -}; + }; + + //execute on each successful route (this is only bound once per application since a service is a singleton) + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + //reset the file manager on each route change, the file collection is only relavent + // when working in an editor and submitting data to the server. + //This ensures that memory remains clear of any files and that the editors don't have to manually clear the files. + mgr.clearFiles(); + }); + + return mgr; } angular.module('umbraco.services').factory('fileManager', fileManager); diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index eaa2fe7b31..2e3aeb7e8d 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,6 +1,6 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', 'tourService', 'dashboardResource', - function (userService, $q, $log, $rootScope, $route, $location, urlHelper, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService, tourService, dashboardResource) { +app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', + function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so @@ -91,13 +91,6 @@ app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlH $rootScope.locationTitle = "Umbraco - " + $location.$$host; } - //reset the editorState on each successful route chage - editorState.reset(); - - //reset the file manager on each route change, the file collection is only relavent - // when working in an editor and submitting data to the server. - //This ensures that memory remains clear of any files and that the editors don't have to manually clear the files. - fileManager.clearFiles(); }); /** When the route change is rejected - based on checkAuth - we'll prevent the rejected route from executing including diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index 560d398a2c..3450de5dc5 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -59,7 +59,7 @@ namespace Umbraco.Web.Models.Mapping variants.Remove(defaultLang); //Sort the remaining languages a-z - variants = variants.OrderBy(x => x.Name).ToList(); + variants = variants.OrderBy(x => x.Language.IsoCode).ToList(); //Insert the default language as the first item variants.Insert(0, defaultLang);