diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js index 4ed1d68e59..bb16edf761 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js @@ -10,7 +10,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link

Markup example

     
-        
+        
         
             // Content here
         
@@ -21,7 +21,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link
 
     
         // the title-key property needs an areaAlias_keyAlias from the language files
-        
+        
         
             // Content here
         
@@ -35,8 +35,10 @@ Use this directive to construct a title. Recommended to use it inside an {@link
     
  • {@link umbraco.directives.directive:umbBoxContent umbBoxContent}
  • -@param {string} title (attrbute): Custom title text. -@param {string} title-key (attrbute): the key alias of the language xml files. +@param {string=} title (attrbute): Custom title text. +@param {string=} titleKey (attrbute): The translation key from the language xml files. +@param {string=} description (attrbute): Custom description text. +@param {string=} descriptionKey (attrbute): The translation key from the language xml files. **/ @@ -52,7 +54,9 @@ Use this directive to construct a title. Recommended to use it inside an {@link templateUrl: 'views/components/html/umb-box/umb-box-header.html', scope: { titleKey: "@?", - title: "@?" + title: "@?", + descriptionKey: "@?", + description: "@?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less index cfbd929a3a..492a257b38 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less @@ -15,6 +15,13 @@ font-weight: bold; } +.umb-box-header-description { + font-size: 13px; + color: @gray-3; + line-height: 1.6em; + margin-top: 1px; +} + .umb-box-content { padding: 20px; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html index 6feffe9d3d..8c59061788 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html @@ -1,6 +1,10 @@
    -
    +
    {{title}}
    +
    + + {{description}} +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index f1427e3266..455f2177c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -23,7 +23,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index a9b17998d5..89b1334baf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -66,11 +66,7 @@ vm.user = user; makeBreadcrumbs(vm.user); setUserDisplayState(); - - // format dates to local - if(vm.user.lastLoginDate) { - vm.user.formattedLastLogin = getLocalDate(vm.user.lastLoginDate, "MMMM Do YYYY, HH:mm"); - } + formatDatesToLocal(vm.user); vm.emailIsUsername = user.email === user.username; @@ -89,18 +85,20 @@ } function getLocalDate(date, format) { - var dateVal; - var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; - var localOffset = new Date().getTimezoneOffset(); - var serverTimeNeedsOffsetting = (-serverOffset !== localOffset); + if(date) { + var dateVal; + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + var localOffset = new Date().getTimezoneOffset(); + var serverTimeNeedsOffsetting = (-serverOffset !== localOffset); - if(serverTimeNeedsOffsetting) { - dateVal = dateHelper.convertToLocalMomentTime(date, serverOffset); - } else { - dateVal = moment(date, "YYYY-MM-DD HH:mm:ss"); + if(serverTimeNeedsOffsetting) { + dateVal = dateHelper.convertToLocalMomentTime(date, serverOffset); + } else { + dateVal = moment(date, "YYYY-MM-DD HH:mm:ss"); + } + + return dateVal.format(format); } - - return dateVal.format(format); } function toggleChangePassword() { @@ -128,6 +126,7 @@ vm.user = saved; setUserDisplayState(); + formatDatesToLocal(vm.user); vm.changePasswordModel.isChanging = false; vm.page.saveButtonState = "success"; @@ -380,6 +379,14 @@ vm.user.userDisplayState = usersHelper.getUserStateFromValue(vm.user.userState); } + function formatDatesToLocal(user) { + user.formattedLastLogin = getLocalDate(user.lastLoginDate, "MMMM Do YYYY, HH:mm"); + user.formattedLastLockoutDate = getLocalDate(user.lastLockoutDate, "MMMM Do YYYY, HH:mm"); + user.formattedCreateDate = getLocalDate(user.createDate, "MMMM Do YYYY, HH:mm"); + user.formattedUpdateDate = getLocalDate(user.updateDate, "MMMM Do YYYY, HH:mm"); + user.formattedLastPasswordChangeDate = getLocalDate(user.lastPasswordChangeDate, "MMMM Do YYYY, HH:mm"); + } + init(); } angular.module("umbraco").controller("Umbraco.Editors.Users.UserController", UserEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 88ae4bf46e..a55b1bf83d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -1,9 +1,5 @@
    - - -
    @@ -17,6 +13,10 @@ + + +
    @@ -73,7 +73,7 @@ - + @@ -158,12 +158,48 @@ + + + + + + + + + + + + + + + + + + + + +
    +
    @@ -209,27 +245,8 @@
    -
    -
    - Status:
    -
    - - {{vm.user.userDisplayState.name}} - -
    -
    - -
    -
    - Last login: -
    -
    - {{ vm.user.formattedLastLogin }} - {{ vm.user.name }} has not logged in yet -
    -
    - -
    + +
    + +
    +
    + Status: +
    +
    + + {{vm.user.userDisplayState.name}} + +
    +
    + +
    +
    + Last login: +
    +
    + {{ vm.user.formattedLastLogin }} + {{ vm.user.name | umbWordLimit:1 }} has not logged in yet +
    +
    + +
    +
    + Failed login attempts: +
    +
    + {{ vm.user.failedPasswordAttempts }} +
    +
    + +
    +
    + Last lockout date: +
    +
    + + {{ vm.user.name | umbWordLimit:1 }} hasn't been locked out + + {{ vm.user.formattedLastLockoutDate }} +
    +
    + +
    +
    + Password is last changed: +
    +
    + + The password hasn't been changed + + {{ vm.user.formattedLastPasswordChangeDate }} +
    +
    + +
    +
    + User is created: +
    +
    + {{ vm.user.formattedCreateDate }} +
    +
    + +
    +
    + User is last updated: +
    +
    + {{ vm.user.formattedUpdateDate }} +
    +
    +
    @@ -332,7 +422,8 @@ type="button" action="vm.goToPage(vm.breadcrumbs[0])" label="Return to list" - label-key="buttons_returnToList"> + label-key="buttons_returnToList" + disabled="vm.loading"> + label-key="buttons_save" + disabled="vm.loading"> 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 e312368270..ed0edf8b6d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1586,11 +1586,16 @@ To manage your website, simply open the Umbraco back office and start adding con Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access Administrator Category field + User created Change Your Password Change photo New password + hasn't been locked out + The password hasn't been changed Confirm new password You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button Content Channel @@ -1601,13 +1606,16 @@ To manage your website, simply open the Umbraco back office and start adding con Document Type Editor Excerpt field + Failed login attempts Go to user profile Add groups to assign access and permissions Invite another user Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Language Set the language you will see in menus and dialogs + Last lockout date Last login + Password last changed Login Media start node Limit the media library to a specific start node @@ -1643,6 +1651,7 @@ To manage your website, simply open the Umbraco back office and start adding con Limit the content tree to a specific start node Content start nodes Limit the content tree to specific start nodes + User last updated has been created The new user has successfully been created. To log in to Umbraco use the password below. Name diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index 4cff43e3b8..8a79344c8e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Runtime.Serialization; @@ -38,6 +39,40 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "resetPasswordValue")] [ReadOnly(true)] public string ResetPasswordValue { get; set; } - + + /// + /// A readonly value showing the user's current calculated start content ids + /// + [DataMember(Name = "calculatedStartContentIds")] + [ReadOnly(true)] + public IEnumerable CalculatedStartContentIds { get; set; } + + /// + /// A readonly value showing the user's current calculated start media ids + /// + [DataMember(Name = "calculatedStartMediaIds")] + [ReadOnly(true)] + public IEnumerable CalculatedStartMediaIds { get; set; } + + [DataMember(Name = "failedPasswordAttempts")] + [ReadOnly(true)] + public int FailedPasswordAttempts { get; set; } + + [DataMember(Name = "lastLockoutDate")] + [ReadOnly(true)] + public DateTime LastLockoutDate { get; set; } + + [DataMember(Name = "lastPasswordChangeDate")] + [ReadOnly(true)] + public DateTime LastPasswordChangeDate { get; set; } + + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + [DataMember(Name = "updateDate")] + [ReadOnly(true)] + public DateTime UpdateDate { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 02f400cdf9..bd25c5da07 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -228,13 +228,45 @@ namespace Umbraco.Web.Models.Mapping display.AssignedPermissions = allAssignedPermissions; }); + //Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. config.CreateMap() .ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(applicationContext.Services.UserService, applicationContext.ApplicationCache.RuntimeCache))) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(detail => detail.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?) user.LastLoginDate)) .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) - .ForMember(detail => detail.StartContentIds, opt => opt.UseValue(Enumerable.Empty())) - .ForMember(detail => detail.StartMediaIds, opt => opt.UseValue(Enumerable.Empty())) + .ForMember( + detail => detail.CalculatedStartContentIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.CalculateContentStartNodeIds(applicationContext.Services.EntityService), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "content/contentRoot"))) + .ForMember( + detail => detail.CalculatedStartMediaIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.CalculateMediaStartNodeIds(applicationContext.Services.EntityService), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "media/mediaRoot"))) + .ForMember( + detail => detail.StartContentIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.StartContentIds.ToArray(), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "content/contentRoot"))) + .ForMember( + detail => detail.StartMediaIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.StartMediaIds.ToArray(), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Media, + "media/mediaRoot"))) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember( detail => detail.AvailableCultures, @@ -252,40 +284,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.ResetPasswordValue, opt => opt.Ignore()) .ForMember(detail => detail.Alias, opt => opt.Ignore()) .ForMember(detail => detail.Trashed, opt => opt.Ignore()) - .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()) - .AfterMap((user, display) => - { - //Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - - var startContentIds = user.StartContentIds.ToArray(); - if (startContentIds.Length > 0) - { - //TODO: Update GetAll to be able to pass in a parameter like on the normal Get to NOT load in the entire object! - var startNodes = new List(); - if (startContentIds.Contains(-1)) - { - startNodes.Add(RootNode(applicationContext.Services.TextService.Localize("content/contentRoot"))); - } - var contentItems = applicationContext.Services.EntityService.GetAll(UmbracoObjectTypes.Document, startContentIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(contentItems)); - display.StartContentIds = startNodes; - - - } - var startMediaIds = user.StartMediaIds.ToArray(); - if (startMediaIds.Length > 0) - { - var startNodes = new List(); - if (startContentIds.Contains(-1)) - { - startNodes.Add(RootNode(applicationContext.Services.TextService.Localize("media/mediaRoot"))); - } - var mediaItems = applicationContext.Services.EntityService.GetAll(UmbracoObjectTypes.Media, startMediaIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); - display.StartMediaIds = startNodes; - } - }); + .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()); config.CreateMap() //Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost @@ -323,7 +322,7 @@ namespace Umbraco.Web.Models.Mapping opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().GenerateHash())) .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()) .AfterMap((user, detail) => - { + { //we need to map the legacy UserType //the best we can do here is to return the user's first user group as a IUserType object //but we should attempt to return any group that is the built in ones first @@ -342,7 +341,7 @@ namespace Umbraco.Web.Models.Mapping detail.UserType = foundBuiltIn.Alias; } else - { + { //otherwise return the first detail.UserType = groups[0].Alias; } @@ -367,6 +366,24 @@ namespace Umbraco.Web.Models.Mapping } + private IEnumerable GetStartNodeValues(int[] startNodeIds, + ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType, + string localizedKey) + { + if (startNodeIds.Length > 0) + { + var startNodes = new List(); + if (startNodeIds.Contains(-1)) + { + startNodes.Add(RootNode(textService.Localize(localizedKey))); + } + var mediaItems = entityService.GetAll(objectType, startNodeIds); + startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); + return startNodes; + } + return Enumerable.Empty(); + } + private void MapUserGroupBasic(ServiceContext services, dynamic group, UserGroupBasic display) { var allSections = services.SectionService.GetSections();