From 0bd9e3ca9955844389556ce512fa0aab181703fb Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 23 Jul 2020 16:42:04 +0200 Subject: [PATCH] Member type infinite editing (#8027) --- .../media/umbmedianodeinfo.directive.js | 4 +- .../member/umbmembernodeinfo.directive.js | 13 ++ .../src/common/services/editor.service.js | 17 -- .../components/media/umb-media-node-info.html | 1 - .../member/umb-member-node-info.html | 4 +- .../src/views/media/media.edit.controller.js | 9 +- .../src/views/mediatypes/edit.controller.js | 11 +- .../views/member/member.edit.controller.js | 217 ++++++++++++------ .../src/views/membertypes/edit.controller.js | 51 +++- src/Umbraco.Web/Editors/MediaController.cs | 12 +- .../Models/ContentEditing/MediaItemDisplay.cs | 2 +- .../Models/ContentEditing/MemberDisplay.cs | 3 + .../Models/Mapping/MemberMapDefinition.cs | 1 + 13 files changed, 230 insertions(+), 115 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index dfa1afc247..6f34cfc0a1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $q) { + function MediaNodeInfoDirective($timeout, $location, $q, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource) { function link(scope, element, attrs, ctrl) { @@ -37,7 +37,7 @@ }); }); - // get document type details + // get media type details scope.mediaType = scope.node.contentType; // set the media link initially diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/member/umbmembernodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/member/umbmembernodeinfo.directive.js index 3b6a2c069a..8dd6d56139 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/member/umbmembernodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/member/umbmembernodeinfo.directive.js @@ -11,6 +11,19 @@ scope.allowChangeMemberType = false; function onInit() { + + userService.getCurrentUser().then(function (user) { + // only allow change of member type if user has access to the settings sections + Utilities.forEach(user.sections, function (section) { + if (section.alias === "settings") { + scope.allowChangeMemberType = true; + } + }); + }); + + // get member type details + scope.memberType = scope.node.contentType; + // make sure dates are formatted to the user's locale formatDatesToLocal(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 538bd41ce0..0f4f04c6bf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -669,23 +669,6 @@ When building a custom infinite editor view you can use the same components as a open(editor); } - /** - * @ngdoc method - * @name umbraco.services.editorService#memberTypeEditor - * @methodOf umbraco.services.editorService - * - * @description - * Opens the member type editor in infinite editing, the submit callback returns the saved member type - * @param {Object} editor rendering options - * @param {Callback} editor.submit Submits the editor - * @param {Callback} editor.close Closes the editor - * @returns {Object} editor object - */ - function memberTypeEditor(editor) { - editor.view = "views/membertypes/edit.html"; - open(editor); - } - /** * @ngdoc method * @name umbraco.services.editorService#queryBuilder diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index a606aa5588..4ebfa89b41 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -20,7 +20,6 @@ {{nodeFileName}} - diff --git a/src/Umbraco.Web.UI.Client/src/views/components/member/umb-member-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/member/umb-member-node-info.html index 62b2052771..0dceeb4c26 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/member/umb-member-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/member/umb-member-node-info.html @@ -28,13 +28,13 @@ {{node.updateDateFormatted}} - + + on-open="openMemberType(memberType)"> 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 a7ce67fc0b..59faff82aa 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 @@ -6,10 +6,10 @@ * @description * The controller for the media editor */ -function mediaEditController($scope, $routeParams, $q, appState, mediaResource, +function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, - editorState, umbRequestHelper, $http, eventsService, $location) { + editorState, umbRequestHelper, eventsService) { var evts = []; var nodeId = null; @@ -104,12 +104,10 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, content.apps[0].active = true; $scope.appChanged(content.apps[0]); } - editorState.set($scope.content); bindEvents(); - } function bindEvents() { @@ -260,7 +258,6 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, $scope.page.loading = false; $q.resolve($scope.content); - }); } @@ -282,7 +279,7 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, $scope.showBack = function () { return !infiniteMode && !!$scope.page.listViewPath; - } + }; /** Callback for when user clicks the back-icon */ $scope.onBack = function() { diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js index f0745e7f82..d683bfa112 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js @@ -9,10 +9,10 @@ (function () { "use strict"; - function MediaTypesEditController($scope, $routeParams, mediaTypeResource, - dataTypeResource, editorState, contentEditingHelper, formHelper, - navigationService, iconHelper, contentTypeHelper, notificationsService, - $q, localizationService, overlayHelper, eventsService, angularHelper) { + function MediaTypesEditController($scope, $routeParams, $q, + mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, + navigationService, iconHelper, contentTypeHelper, notificationsService, + localizationService, overlayHelper, eventsService, angularHelper) { var vm = this; var evts = []; @@ -248,6 +248,7 @@ }); if (create) { + vm.page.loading = true; //we are creating so get an empty data type item @@ -425,7 +426,7 @@ } function close() { - if(infiniteMode && $scope.model.close) { + if (infiniteMode && $scope.model.close) { $scope.model.close(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index 3ec76deb8e..9d756861ca 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -6,9 +6,15 @@ * @description * The controller for the member editor */ -function MemberEditController($scope, $routeParams, $location, appState, memberResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, $http) { +function MemberEditController($scope, $routeParams, $location, $http, $q, appState, memberResource, + entityResource, navigationService, notificationsService, localizationService, + serverValidationManager, contentEditingHelper, fileManager, formHelper, + editorState, umbRequestHelper, eventsService) { + + var evts = []; var infiniteMode = $scope.model && $scope.model.infiniteMode; + var id = infiniteMode ? $scope.model.id : $routeParams.id; var create = infiniteMode ? $scope.model.create : $routeParams.create; var listName = infiniteMode ? $scope.model.listname : $routeParams.listName; @@ -66,53 +72,11 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR } else { - //so, we usually refernce all editors with the Int ID, but with members we have - //a different pattern, adding a route-redirect here to handle this just in case. - //(isNumber doesnt work here since its seen as a string) - //The reason this might be an INT is due to the routing used for the member list view - //but this is now configured to use the key, so this is just a fail safe - - if (id && id.length < 9) { - - entityResource.getById(id, "Member").then(function(entity) { - $location.path("/member/member/edit/" + entity.key); + $scope.page.loading = true; + loadMember() + .then(function () { + $scope.page.loading = false; }); - } - else { - - //we are editing so get the content item from the server - memberResource.getByKey(id) - .then(function(data) { - - $scope.content = data; - - init(); - - if (!infiniteMode) { - var path = buildTreePath(data); - - //sync the tree (only for ui purposes) - navigationService.syncTree({ tree: "member", path: path.split(",") }); - } - //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(data.treeNodeUrl), - 'Failed to retrieve data for child node ' + data.key).then(function (node) { - $scope.page.menu.currentNode = node; - }); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.notifyAndClearAllSubscriptions(); - - $scope.page.loading = false; - - }); - } - } function init() { @@ -157,6 +121,51 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR 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.memberType.saved", function (name, args) { + // if this member item uses the updated member type we need to reload the member item + if (args && args.memberType && args.memberType.key.replace(/-/g, '') === $scope.content.contentType.key) { + $scope.page.loading = true; + loadMember().then(function () { + $scope.page.loading = false; + }); + } + })); + } + + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { + + if (infiniteMode) { + return; + } + + if (!$scope.content.isChildOfListView) { + navigationService.syncTree({ tree: "member", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + } + else if (initialLoad === true) { + + //it's a child item, just sync the ui node to the parent + navigationService.syncTree({ tree: "member", 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 + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise( + $http.get(content.treeNodeUrl), + 'Failed to retrieve data for child node ' + content.id).then(function (node) { + $scope.page.menu.currentNode = node; + }); + } } /** Just shows a simple notification that there are client side validation issues to be fixed */ @@ -191,23 +200,34 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR formHelper.resetForm({ scope: $scope }); - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - //specify a custom id to redirect to since we want to use the GUID - redirectId: data.key, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); + // close the editor if it's infinite mode + // submit function manages rebinding changes + if (infiniteMode && $scope.model.submit) { + $scope.model.memberNode = $scope.content; + $scope.model.submit($scope.model); + } else { + // if not infinite mode, rebind changed props etc + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + //specify a custom id to redirect to since we want to use the GUID + redirectId: data.key, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); - editorState.set($scope.content); - $scope.page.saveButtonState = "success"; + editorState.set($scope.content); - var path = buildTreePath(data); + var path = buildTreePath(data); - //sync the tree (only for ui purposes) - navigationService.syncTree({ tree: "member", path: path.split(","), forceReload: true }); + navigationService.syncTree({ tree: "member", path: path.split(",") }); + //syncTreeNode($scope.content, data.path); - }, function (err) { + $scope.page.saveButtonState = "success"; + + init(); + } + + }, function(err) { contentEditingHelper.handleSaveError({ err: err, @@ -225,6 +245,69 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR }; + function loadMember() { + + var deferred = $q.defer(); + + //so, we usually reference all editors with the Int ID, but with members we have + //a different pattern, adding a route-redirect here to handle this just in case. + //(isNumber doesnt work here since its seen as a string) + //The reason this might be an INT is due to the routing used for the member list view + //but this is now configured to use the key, so this is just a fail safe + + if (id && id.length < 9) { + + entityResource.getById(id, "Member").then(function (entity) { + $location.path("/member/member/edit/" + entity.key); + + deferred.resolve($scope.content); + }, function () { + deferred.reject(); + }); + } + else { + + //we are editing so get the content item from the server + memberResource.getByKey(id) + .then(function (data) { + + $scope.content = data; + + if (!infiniteMode) { + var path = buildTreePath(data); + + navigationService.syncTree({ tree: "member", path: path.split(","), forceReload: true }); + //syncTreeNode($scope.content, data.path, true); + } + + //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(data.treeNodeUrl), + 'Failed to retrieve data for child node ' + data.key).then(function (node) { + $scope.page.menu.currentNode = node; + }); + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.notifyAndClearAllSubscriptions(); + + init(); + + $scope.page.loading = false; + + deferred.resolve($scope.content); + + }, function () { + deferred.reject(); + }); + } + + return deferred.promise; + } + $scope.appChanged = function (app) { $scope.app = app; @@ -235,8 +318,8 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR } $scope.showBack = function () { - return !!listName; - } + return !infiniteMode && !!listName; + }; /** Callback for when user clicks the back-icon */ $scope.onBack = function () { @@ -247,11 +330,17 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR } }; - $scope.export = function() { + $scope.export = function () { var memberKey = $scope.content.key; memberResource.exportMemberData(memberKey); - } + }; + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); } angular.module("umbraco").controller("Umbraco.Editors.Member.EditController", MemberEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js index b2e515e187..13042d60b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js @@ -9,13 +9,16 @@ (function () { "use strict"; - function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService, overlayHelper, contentTypeHelper, angularHelper, eventsService) { + function MemberTypesEditController($scope, $routeParams, $q, + memberTypeResource, dataTypeResource, editorState, iconHelper, + navigationService, contentEditingHelper, notificationsService, localizationService, + overlayHelper, contentTypeHelper, angularHelper, eventsService) { var evts = []; var vm = this; var infiniteMode = $scope.model && $scope.model.infiniteMode; - var memberTypeId = infiniteMode ? $scope.model.id : $routeParams.id; - var create = infiniteMode ? $scope.model.create : $routeParams.create; + var memberTypeId = $routeParams.id; + var create = $routeParams.create; vm.save = save; vm.close = close; @@ -30,7 +33,20 @@ vm.page.loading = false; vm.page.saveButtonState = "init"; vm.labels = {}; - vm.saveButtonKey = infiniteMode ? "buttons_saveAndClose" : "buttons_save"; + vm.saveButtonKey = "buttons_save"; + vm.generateModelsKey = "buttons_saveAndGenerateModels"; + + onInit(); + + function onInit() { + // get init values from model when in infinite mode + if (infiniteMode) { + memberTypeId = $scope.model.id; + create = $scope.model.create; + vm.saveButtonKey = "buttons_saveAndClose"; + vm.generateModelsKey = "buttons_generateModelsAndClose"; + } + } var labelKeys = [ "general_design", @@ -154,7 +170,7 @@ }); if (create) { - + vm.page.loading = true; //we are creating so get an empty data type item @@ -166,13 +182,17 @@ }); } else { + loadMemberType(); + } + + function loadMemberType() { vm.page.loading = true; memberTypeResource.getById(memberTypeId).then(function (dt) { init(dt); - if(!infiniteMode) { + if (!infiniteMode) { syncTreeNode(vm.contentType, dt.path, true); } @@ -180,7 +200,10 @@ }); } + /* ---------- SAVE ---------- */ + function save() { + // only save if there is no overlays open if(overlayHelper.getNumberOfOverlays() === 0) { @@ -227,10 +250,15 @@ } }).then(function (data) { //success + if(!infiniteMode) { syncTreeNode(vm.contentType, data.path); } + // emit event + var args = { memberType: vm.contentType }; + eventsService.emit("editors.memberType.saved", args); + vm.page.saveButtonState = "success"; if(infiniteMode && $scope.model.submit) { @@ -238,6 +266,7 @@ } deferred.resolve(data); + }, function (err) { //error if (err) { @@ -282,7 +311,6 @@ editorState.set(contentType); vm.contentType = contentType; - } function convertLegacyIcons(contentType) { @@ -298,7 +326,6 @@ // set icon back on contentType contentType.icon = contentTypeArray[0].icon; - } function getDataTypeDetails(property) { @@ -315,19 +342,21 @@ /** Syncs the content type to it's tree node - this occurs on first load and after saving */ function syncTreeNode(dt, path, initialLoad) { - navigationService.syncTree({ tree: "membertypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { vm.currentNode = syncArgs.node; }); - } function close() { - if(infiniteMode && $scope.model.close) { + if (infiniteMode && $scope.model.close) { $scope.model.close(); } } + evts.push(eventsService.on("app.refreshEditor", function (name, error) { + loadMemberType(); + })); + evts.push(eventsService.on("editors.groupsBuilder.changed", function(name, args) { angularHelper.getCurrentForm($scope).$setDirty(); })); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index e45593961b..dfe6939552 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -126,15 +126,15 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForMedia("id")] public MediaItemDisplay GetById(int id) { - var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + var foundMedia = GetObjectFromRequest(() => Services.MediaService.GetById(id)); - if (foundContent == null) + if (foundMedia == null) { HandleContentNotFound(id); //HandleContentNotFound will throw an exception return null; } - return Mapper.Map(foundContent); + return Mapper.Map(foundMedia); } /// @@ -146,15 +146,15 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForMedia("id")] public MediaItemDisplay GetById(Guid id) { - var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + var foundMedia = GetObjectFromRequest(() => Services.MediaService.GetById(id)); - if (foundContent == null) + if (foundMedia == null) { HandleContentNotFound(id); //HandleContentNotFound will throw an exception return null; } - return Mapper.Map(foundContent); + return Mapper.Map(foundMedia); } /// diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs index 0118645b60..a5d538c6ac 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models.ContentEditing; namespace Umbraco.Web.Models.ContentEditing { /// - /// A model representing a content item to be displayed in the back office + /// A model representing a media item to be displayed in the back office /// [DataContract(Name = "content", Namespace = "")] public class MediaItemDisplay : ListViewAwareContentItemDisplayBase diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs index 3857731671..b3a606dad1 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs @@ -19,6 +19,9 @@ namespace Umbraco.Web.Models.ContentEditing ContentApps = new List(); } + [DataMember(Name = "contentType")] + public ContentTypeBasic ContentType { get; set; } + [DataMember(Name = "username")] public string Username { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs index 851f452244..f86e7eb1fc 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs @@ -77,6 +77,7 @@ namespace Umbraco.Web.Models.Mapping private void Map(IMember source, MemberDisplay target, MapperContext context) { target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentType = _commonMapper.GetContentType(source, context); target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name;