Member type infinite editing (#8027)

This commit is contained in:
Bjarne Fyrstenborg
2020-07-23 16:42:04 +02:00
committed by GitHub
parent e02b5a74d7
commit 0bd9e3ca99
13 changed files with 230 additions and 115 deletions

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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

View File

@@ -20,7 +20,6 @@
<a ng-attr-href="{{node.extension !== 'svg' ? nodeUrl : undefined}}" ng-click="node.extension === 'svg' && openSVG()" target="_blank">
<i class="icon icon-out"></i>
<span>{{nodeFileName}}</span>
</a>
</li>
</ul>

View File

@@ -28,13 +28,13 @@
{{node.updateDateFormatted}}
</umb-control-group>
<umb-control-group data-element="node-info-document-type" label="@content_membertype">
<umb-control-group data-element="node-info-member-type" label="@content_membertype">
<umb-node-preview style="max-width: 100%; margin-bottom: 0px;"
icon="node.icon"
name="node.contentTypeName"
alias="node.contentTypeAlias"
allow-open="allowChangeMemberType"
on-open="openMemberType(node)">
on-open="openMemberType(memberType)">
</umb-node-preview>
</umb-control-group>

View File

@@ -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() {

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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();
}));

View File

@@ -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<MediaItemDisplay>(foundContent);
return Mapper.Map<MediaItemDisplay>(foundMedia);
}
/// <summary>
@@ -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<MediaItemDisplay>(foundContent);
return Mapper.Map<MediaItemDisplay>(foundMedia);
}
/// <summary>

View File

@@ -7,7 +7,7 @@ using Umbraco.Core.Models.ContentEditing;
namespace Umbraco.Web.Models.ContentEditing
{
/// <summary>
/// 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
/// </summary>
[DataContract(Name = "content", Namespace = "")]
public class MediaItemDisplay : ListViewAwareContentItemDisplayBase<ContentPropertyDisplay>

View File

@@ -19,6 +19,9 @@ namespace Umbraco.Web.Models.ContentEditing
ContentApps = new List<ContentApp>();
}
[DataMember(Name = "contentType")]
public ContentTypeBasic ContentType { get; set; }
[DataMember(Name = "username")]
public string Username { get; set; }

View File

@@ -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;