From 2184c9e22e2b99f7b29fbc25d0742adabf950a24 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Sat, 12 Jul 2025 19:55:58 +1000 Subject: [PATCH] add comments for users [#145] --- Disco.Data/Disco.Data.csproj | 7 + .../202507110430252_DBv27.Designer.cs | 27 ++ .../Migrations/202507110430252_DBv27.cs | 58 +++ .../Migrations/202507110430252_DBv27.resx | 123 +++++ Disco.Data/Repository/DiscoDataContext.cs | 2 + Disco.Models/Disco.Models.csproj | 2 + Disco.Models/Repository/Device/Device.cs | 5 +- .../Repository/Device/DeviceComment.cs | 25 + Disco.Models/Repository/User/User.cs | 4 +- Disco.Models/Repository/User/UserComment.cs | 25 + Disco.Services/Authorization/Claims.cs | 32 ++ .../ClaimGroups/User/UserActionsClaims.cs | 7 + .../Roles/ClaimGroups/User/UserClaims.cs | 3 + Disco.Services/Users/UserUpdatesHub.cs | 92 ++-- .../Areas/API/Controllers/JobController.cs | 4 +- .../Areas/API/Controllers/UserController.cs | 91 +++- .../Areas/API/Models/Shared/CommentModel.cs | 31 +- Disco.Web/ClientSource/Style/User.css | 139 +++++- Disco.Web/ClientSource/Style/User.less | 164 ++++++- Disco.Web/ClientSource/Style/User.min.css | 2 +- Disco.Web/Controllers/UserController.cs | 5 +- Disco.Web/Disco.Web.csproj | 17 +- .../T4MVC/API.UserController.generated.cs | 118 +++++ Disco.Web/Extensions/T4MVC/T4MVCExtensions.cs | 48 +- .../T4MVC/UserController.generated.cs | 6 +- Disco.Web/Views/User/Show.cshtml | 83 +++- Disco.Web/Views/User/Show.generated.cs | 178 +++++-- .../Views/User/UserParts/Comments.cshtml | 212 ++++++++ .../User/UserParts/Comments.generated.cs | 451 ++++++++++++++++++ .../User/UserParts/_CommentsAndJobs.cshtml | 41 ++ .../UserParts/_CommentsAndJobs.generated.cs | 185 +++++++ Disco.Web/Views/User/UserParts/_Jobs.cshtml | 12 - .../Views/User/UserParts/_Jobs.generated.cs | 90 ---- .../Views/User/UserParts/_Resources.cshtml | 58 +-- .../User/UserParts/_Resources.generated.cs | 352 +++++--------- 35 files changed, 2201 insertions(+), 498 deletions(-) create mode 100644 Disco.Data/Migrations/202507110430252_DBv27.Designer.cs create mode 100644 Disco.Data/Migrations/202507110430252_DBv27.cs create mode 100644 Disco.Data/Migrations/202507110430252_DBv27.resx create mode 100644 Disco.Models/Repository/Device/DeviceComment.cs create mode 100644 Disco.Models/Repository/User/UserComment.cs create mode 100644 Disco.Web/Views/User/UserParts/Comments.cshtml create mode 100644 Disco.Web/Views/User/UserParts/Comments.generated.cs create mode 100644 Disco.Web/Views/User/UserParts/_CommentsAndJobs.cshtml create mode 100644 Disco.Web/Views/User/UserParts/_CommentsAndJobs.generated.cs delete mode 100644 Disco.Web/Views/User/UserParts/_Jobs.cshtml delete mode 100644 Disco.Web/Views/User/UserParts/_Jobs.generated.cs diff --git a/Disco.Data/Disco.Data.csproj b/Disco.Data/Disco.Data.csproj index fcea0928..d9ec35b5 100644 --- a/Disco.Data/Disco.Data.csproj +++ b/Disco.Data/Disco.Data.csproj @@ -193,6 +193,10 @@ 202503140520548_DBv26.cs + + + 202507110430252_DBv27.cs + @@ -286,6 +290,9 @@ 202503140520548_DBv26.cs + + 202507110430252_DBv27.cs + ResXFileCodeGenerator Resources.Designer.cs diff --git a/Disco.Data/Migrations/202507110430252_DBv27.Designer.cs b/Disco.Data/Migrations/202507110430252_DBv27.Designer.cs new file mode 100644 index 00000000..7f935896 --- /dev/null +++ b/Disco.Data/Migrations/202507110430252_DBv27.Designer.cs @@ -0,0 +1,27 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + public sealed partial class DBv27 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(DBv27)); + + string IMigrationMetadata.Id + { + get { return "202507110430252_DBv27"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/Disco.Data/Migrations/202507110430252_DBv27.cs b/Disco.Data/Migrations/202507110430252_DBv27.cs new file mode 100644 index 00000000..9020744e --- /dev/null +++ b/Disco.Data/Migrations/202507110430252_DBv27.cs @@ -0,0 +1,58 @@ +namespace Disco.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class DBv27 : DbMigration + { + public override void Up() + { + CreateTable( + "dbo.UserComments", + c => new + { + Id = c.Int(nullable: false, identity: true), + UserId = c.String(maxLength: 50), + TechUserId = c.String(nullable: false, maxLength: 50), + Timestamp = c.DateTime(nullable: false), + Comments = c.String(nullable: false), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("dbo.Users", t => t.TechUserId) + .ForeignKey("dbo.Users", t => t.UserId) + .Index(t => t.TechUserId) + .Index(t => t.UserId); + + CreateTable( + "dbo.DeviceComments", + c => new + { + Id = c.Int(nullable: false, identity: true), + DeviceSerialNumber = c.String(maxLength: 60), + TechUserId = c.String(nullable: false, maxLength: 50), + Timestamp = c.DateTime(nullable: false), + Comments = c.String(nullable: false), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("dbo.Users", t => t.TechUserId) + .ForeignKey("dbo.Devices", t => t.DeviceSerialNumber) + .Index(t => t.TechUserId) + .Index(t => t.DeviceSerialNumber); + + } + + public override void Down() + { + DropIndex("dbo.DeviceComments", new[] { "DeviceSerialNumber" }); + DropIndex("dbo.DeviceComments", new[] { "TechUserId" }); + DropIndex("dbo.UserComments", new[] { "UserId" }); + DropIndex("dbo.UserComments", new[] { "TechUserId" }); + DropForeignKey("dbo.DeviceComments", "DeviceSerialNumber", "dbo.Devices"); + DropForeignKey("dbo.DeviceComments", "TechUserId", "dbo.Users"); + DropForeignKey("dbo.UserComments", "UserId", "dbo.Users"); + DropForeignKey("dbo.UserComments", "TechUserId", "dbo.Users"); + DropTable("dbo.DeviceComments"); + DropTable("dbo.UserComments"); + } + } +} diff --git a/Disco.Data/Migrations/202507110430252_DBv27.resx b/Disco.Data/Migrations/202507110430252_DBv27.resx new file mode 100644 index 00000000..963abc57 --- /dev/null +++ b/Disco.Data/Migrations/202507110430252_DBv27.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + \ No newline at end of file diff --git a/Disco.Data/Repository/DiscoDataContext.cs b/Disco.Data/Repository/DiscoDataContext.cs index 119019d1..ce77cf6a 100644 --- a/Disco.Data/Repository/DiscoDataContext.cs +++ b/Disco.Data/Repository/DiscoDataContext.cs @@ -19,6 +19,7 @@ namespace Disco.Data.Repository public virtual DbSet DocumentTemplates { get; set; } public virtual DbSet Users { get; set; } + public virtual DbSet UserComments { get; set; } public virtual DbSet UserDetails { get; set; } public virtual DbSet UserAttachments { get; set; } public virtual DbSet UserFlags { get; set; } @@ -28,6 +29,7 @@ namespace Disco.Data.Repository public virtual DbSet DeviceUserAssignments { get; set; } public virtual DbSet Devices { get; set; } + public virtual DbSet DeviceComments { get; set; } public virtual DbSet DeviceDetails { get; set; } public virtual DbSet DeviceModels { get; set; } public virtual DbSet DeviceProfiles { get; set; } diff --git a/Disco.Models/Disco.Models.csproj b/Disco.Models/Disco.Models.csproj index 392fab94..bcfee045 100644 --- a/Disco.Models/Disco.Models.csproj +++ b/Disco.Models/Disco.Models.csproj @@ -58,6 +58,8 @@ + + diff --git a/Disco.Models/Repository/Device/Device.cs b/Disco.Models/Repository/Device/Device.cs index 691680c9..03502c4c 100644 --- a/Disco.Models/Repository/Device/Device.cs +++ b/Disco.Models/Repository/Device/Device.cs @@ -48,9 +48,12 @@ namespace Disco.Models.Repository public virtual IList DeviceAttachments { get; set; } public virtual IList DeviceCertificates { get; set; } - [InverseProperty("DeviceSerialNumber")] + [InverseProperty(nameof(Job.Device))] public virtual IList Jobs { get; set; } public virtual IList DeviceFlagAssignments { get; set; } + + [InverseProperty(nameof(DeviceComment.Device))] + public virtual IList DeviceComments { get; set; } /// /// A list of the current device assignments, ordered by the most recent assignment date. diff --git a/Disco.Models/Repository/Device/DeviceComment.cs b/Disco.Models/Repository/Device/DeviceComment.cs new file mode 100644 index 00000000..35ec6d43 --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceComment.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class DeviceComment + { + [Key] + public int Id { get; set; } + public string DeviceSerialNumber { get; set; } + + [Required] + public string TechUserId { get; set; } + public DateTime Timestamp { get; set; } + [Required] + public string Comments { get; set; } + + [ForeignKey(nameof(DeviceSerialNumber))] + public Device Device { get; set; } + + [ForeignKey(nameof(TechUserId))] + public User TechUser { get; set; } + } +} diff --git a/Disco.Models/Repository/User/User.cs b/Disco.Models/Repository/User/User.cs index a4c1c8c3..a168bfff 100644 --- a/Disco.Models/Repository/User/User.cs +++ b/Disco.Models/Repository/User/User.cs @@ -25,9 +25,11 @@ namespace Disco.Models.Repository public virtual IList UserDetails { get; set; } public virtual IList UserAttachments { get; set; } public virtual IList DeviceUserAssignments { get; set; } - [InverseProperty("UserId")] + [InverseProperty(nameof(Job.User))] public virtual IList Jobs { get; set; } public virtual IList UserFlagAssignments { get; set; } + [InverseProperty(nameof(UserComment.User))] + public virtual IList UserComments { get; set; } [NotMapped, Obsolete("Should be using Combined Domain\\User format - UserId")] public string Id diff --git a/Disco.Models/Repository/User/UserComment.cs b/Disco.Models/Repository/User/UserComment.cs new file mode 100644 index 00000000..30716e02 --- /dev/null +++ b/Disco.Models/Repository/User/UserComment.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class UserComment + { + [Key] + public int Id { get; set; } + public string UserId { get; set; } + + [Required] + public string TechUserId { get; set; } + public DateTime Timestamp { get; set; } + [Required] + public string Comments { get; set; } + + [ForeignKey(nameof(UserId))] + public User User { get; set; } + + [ForeignKey(nameof(TechUserId))] + public User TechUser { get; set; } + } +} diff --git a/Disco.Services/Authorization/Claims.cs b/Disco.Services/Authorization/Claims.cs index ff8d3ade..b302c59b 100644 --- a/Disco.Services/Authorization/Claims.cs +++ b/Disco.Services/Authorization/Claims.cs @@ -216,14 +216,18 @@ namespace Disco.Services.Authorization { "Device.Show", new Tuple, Action, string, string, bool>(c => c.Device.Show, (c, v) => c.Device.Show = v, "Show Devices", "Can show devices", false) }, { "Device.ShowJobs", new Tuple, Action, string, string, bool>(c => c.Device.ShowJobs, (c, v) => c.Device.ShowJobs = v, "Show Devices Jobs", "Can show jobs associated with devices", false) }, { "User.Actions.AddAttachments", new Tuple, Action, string, string, bool>(c => c.User.Actions.AddAttachments, (c, v) => c.User.Actions.AddAttachments = v, "Add Attachments", "Can add attachments to users", false) }, + { "User.Actions.AddComments", new Tuple, Action, string, string, bool>(c => c.User.Actions.AddComments, (c, v) => c.User.Actions.AddComments = v, "Add Comments", "Can add user comments", false) }, { "User.Actions.AddFlags", new Tuple, Action, string, string, bool>(c => c.User.Actions.AddFlags, (c, v) => c.User.Actions.AddFlags = v, "Add User Flags", "Can add user flags", false) }, { "User.Actions.EditFlags", new Tuple, Action, string, string, bool>(c => c.User.Actions.EditFlags, (c, v) => c.User.Actions.EditFlags = v, "Edit User Flags", "Can edit user flags", false) }, { "User.Actions.GenerateDocuments", new Tuple, Action, string, string, bool>(c => c.User.Actions.GenerateDocuments, (c, v) => c.User.Actions.GenerateDocuments = v, "Generate Documents", "Can generate documents for users", false) }, { "User.Actions.RemoveAnyAttachments", new Tuple, Action, string, string, bool>(c => c.User.Actions.RemoveAnyAttachments, (c, v) => c.User.Actions.RemoveAnyAttachments = v, "Remove Any Attachments", "Can remove any attachments from users", false) }, + { "User.Actions.RemoveAnyComments", new Tuple, Action, string, string, bool>(c => c.User.Actions.RemoveAnyComments, (c, v) => c.User.Actions.RemoveAnyComments = v, "Remove Any Comments", "Can remove any user comments", false) }, { "User.Actions.RemoveOwnAttachments", new Tuple, Action, string, string, bool>(c => c.User.Actions.RemoveOwnAttachments, (c, v) => c.User.Actions.RemoveOwnAttachments = v, "Remove Own Attachments", "Can remove own attachments from users", false) }, + { "User.Actions.RemoveOwnComments", new Tuple, Action, string, string, bool>(c => c.User.Actions.RemoveOwnComments, (c, v) => c.User.Actions.RemoveOwnComments = v, "Remove Own Comments", "Can remove own user comments", false) }, { "User.Actions.RemoveFlags", new Tuple, Action, string, string, bool>(c => c.User.Actions.RemoveFlags, (c, v) => c.User.Actions.RemoveFlags = v, "Remove User Flags", "Can remove user flags", false) }, { "User.Search", new Tuple, Action, string, string, bool>(c => c.User.Search, (c, v) => c.User.Search = v, "Search Users", "Can search users", false) }, { "User.ShowAttachments", new Tuple, Action, string, string, bool>(c => c.User.ShowAttachments, (c, v) => c.User.ShowAttachments = v, "Show Attachments", "Can show user attachments", false) }, + { "User.ShowComments", new Tuple, Action, string, string, bool>(c => c.User.ShowComments, (c, v) => c.User.ShowComments = v, "Show Comments", "Can show user comments", false) }, { "User.ShowAssignmentHistory", new Tuple, Action, string, string, bool>(c => c.User.ShowAssignmentHistory, (c, v) => c.User.ShowAssignmentHistory = v, "Show Device Assignment History", "Can show the device assignment history for users", false) }, { "User.ShowAssignments", new Tuple, Action, string, string, bool>(c => c.User.ShowAssignments, (c, v) => c.User.ShowAssignments = v, "Show Device Assignments", "Can show the current device assignments users", false) }, { "User.Show", new Tuple, Action, string, string, bool>(c => c.User.Show, (c, v) => c.User.Show = v, "Show Users", "Can show users", false) }, @@ -488,15 +492,19 @@ namespace Disco.Services.Authorization new ClaimNavigatorItem("User", "User", "Permissions related to Users", false, new List() { new ClaimNavigatorItem("User.Actions", "Actions", "Permissions related to User Actions", false, new List() { new ClaimNavigatorItem("User.Actions.AddAttachments", false), + new ClaimNavigatorItem("User.Actions.AddComments", false), new ClaimNavigatorItem("User.Actions.AddFlags", false), new ClaimNavigatorItem("User.Actions.EditFlags", false), new ClaimNavigatorItem("User.Actions.GenerateDocuments", false), new ClaimNavigatorItem("User.Actions.RemoveAnyAttachments", false), + new ClaimNavigatorItem("User.Actions.RemoveAnyComments", false), new ClaimNavigatorItem("User.Actions.RemoveOwnAttachments", false), + new ClaimNavigatorItem("User.Actions.RemoveOwnComments", false), new ClaimNavigatorItem("User.Actions.RemoveFlags", false) }), new ClaimNavigatorItem("User.Search", false), new ClaimNavigatorItem("User.ShowAttachments", false), + new ClaimNavigatorItem("User.ShowComments", false), new ClaimNavigatorItem("User.ShowAssignmentHistory", false), new ClaimNavigatorItem("User.ShowAssignments", false), new ClaimNavigatorItem("User.Show", false), @@ -764,14 +772,18 @@ namespace Disco.Services.Authorization c.Device.Show = true; c.Device.ShowJobs = true; c.User.Actions.AddAttachments = true; + c.User.Actions.AddComments = true; c.User.Actions.AddFlags = true; c.User.Actions.EditFlags = true; c.User.Actions.GenerateDocuments = true; c.User.Actions.RemoveAnyAttachments = true; + c.User.Actions.RemoveAnyComments = true; c.User.Actions.RemoveOwnAttachments = true; + c.User.Actions.RemoveOwnComments = true; c.User.Actions.RemoveFlags = true; c.User.Search = true; c.User.ShowAttachments = true; + c.User.ShowComments = true; c.User.ShowAssignmentHistory = true; c.User.ShowAssignments = true; c.User.Show = true; @@ -1961,6 +1973,11 @@ namespace Disco.Services.Authorization /// public const string AddAttachments = "User.Actions.AddAttachments"; + /// Add Comments + /// Can add user comments + /// + public const string AddComments = "User.Actions.AddComments"; + /// Add User Flags /// Can add user flags /// @@ -1981,11 +1998,21 @@ namespace Disco.Services.Authorization /// public const string RemoveAnyAttachments = "User.Actions.RemoveAnyAttachments"; + /// Remove Any Comments + /// Can remove any user comments + /// + public const string RemoveAnyComments = "User.Actions.RemoveAnyComments"; + /// Remove Own Attachments /// Can remove own attachments from users /// public const string RemoveOwnAttachments = "User.Actions.RemoveOwnAttachments"; + /// Remove Own Comments + /// Can remove own user comments + /// + public const string RemoveOwnComments = "User.Actions.RemoveOwnComments"; + /// Remove User Flags /// Can remove user flags /// @@ -2002,6 +2029,11 @@ namespace Disco.Services.Authorization /// public const string ShowAttachments = "User.ShowAttachments"; + /// Show Comments + /// Can show user comments + /// + public const string ShowComments = "User.ShowComments"; + /// Show Device Assignment History /// Can show the device assignment history for users /// diff --git a/Disco.Services/Authorization/Roles/ClaimGroups/User/UserActionsClaims.cs b/Disco.Services/Authorization/Roles/ClaimGroups/User/UserActionsClaims.cs index 76ed7684..a4eb0e6f 100644 --- a/Disco.Services/Authorization/Roles/ClaimGroups/User/UserActionsClaims.cs +++ b/Disco.Services/Authorization/Roles/ClaimGroups/User/UserActionsClaims.cs @@ -3,6 +3,13 @@ [ClaimDetails("Actions", "Permissions related to User Actions")] public class UserActionsClaims : BaseRoleClaimGroup { + [ClaimDetails("Add Comments", "Can add user comments")] + public bool AddComments { get; set; } + [ClaimDetails("Remove Any Comments", "Can remove any user comments")] + public bool RemoveAnyComments { get; set; } + [ClaimDetails("Remove Own Comments", "Can remove own user comments")] + public bool RemoveOwnComments { get; set; } + [ClaimDetails("Add Attachments", "Can add attachments to users")] public bool AddAttachments { get; set; } [ClaimDetails("Remove Any Attachments", "Can remove any attachments from users")] diff --git a/Disco.Services/Authorization/Roles/ClaimGroups/User/UserClaims.cs b/Disco.Services/Authorization/Roles/ClaimGroups/User/UserClaims.cs index b37fbdab..c29a05f1 100644 --- a/Disco.Services/Authorization/Roles/ClaimGroups/User/UserClaims.cs +++ b/Disco.Services/Authorization/Roles/ClaimGroups/User/UserClaims.cs @@ -17,6 +17,9 @@ [ClaimDetails("Show Users Details", "Can show users contact and personal details")] public bool ShowDetails { get; set; } + [ClaimDetails("Show Comments", "Can show user comments")] + public bool ShowComments { get; set; } + [ClaimDetails("Show Attachments", "Can show user attachments")] public bool ShowAttachments { get; set; } diff --git a/Disco.Services/Users/UserUpdatesHub.cs b/Disco.Services/Users/UserUpdatesHub.cs index b6cb563f..22831221 100644 --- a/Disco.Services/Users/UserUpdatesHub.cs +++ b/Disco.Services/Users/UserUpdatesHub.cs @@ -1,44 +1,75 @@ using Disco.Data.Repository.Monitor; +using Disco.Models.Repository; using Disco.Services.Authorization; using Disco.Services.Web.Signalling; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; using System; using System.Linq; -using System.Threading.Tasks; using System.Reactive.Linq; -using Disco.Models.Repository; -using Disco.Data.Repository; +using System.Threading.Tasks; namespace Disco.Services.Users { - [HubName("userUpdates"), DiscoHubAuthorizeAll(Claims.User.Show, Claims.User.ShowAttachments)] + [HubName("userUpdates"), DiscoHubAuthorize(Claims.User.Show)] public class UserUpdatesHub : Hub { - private const string UserPrefix = "User_"; public static IHubContext HubContext { get; private set; } - private static IDisposable RepositoryBeforeSubscription; - private static IDisposable RepositoryAfterSubscription; + private static readonly IDisposable repositoryBeforeSubscription; + private static readonly IDisposable repositoryAfterSubscription; static UserUpdatesHub() { HubContext = GlobalHost.ConnectionManager.GetHubContext(); // Subscribe to Repository Monitor for Changes - RepositoryBeforeSubscription = RepositoryMonitor.StreamBeforeCommit - .Where(e => e.EntityType == typeof(UserAttachment) && e.EventType == RepositoryMonitorEventType.Deleted) - .Subscribe(RepositoryEventBefore); - RepositoryAfterSubscription = RepositoryMonitor.StreamAfterCommit - .Where(e => e.EntityType == typeof(UserAttachment) && e.EventType == RepositoryMonitorEventType.Added) - .Subscribe(RepositoryAfterEvent); + repositoryBeforeSubscription = RepositoryMonitor.StreamBeforeCommit + .Where(e => + e.EventType == RepositoryMonitorEventType.Deleted && ( + e.EntityType == typeof(UserComment) || + e.EntityType == typeof(UserAttachment) + ) + ).Subscribe(RepositoryEventBefore); + repositoryAfterSubscription = RepositoryMonitor.StreamAfterCommit + .Where(e => + e.EventType == RepositoryMonitorEventType.Added && ( + e.EntityType == typeof(UserComment) || + e.EntityType == typeof(UserAttachment) + ) + ).Subscribe(RepositoryAfterEvent); } - private static string GroupName(string UserId) + private static bool TryAttachmentGroupName(RepositoryMonitorEvent e, out string groupName) { - return UserPrefix + UserId; + var userId = e.GetPreviousPropertyValue(nameof(UserAttachment.UserId)); + if (userId == null) + { + groupName = null; + return false; + } + groupName = AttachmentGroupName(userId); + return true; } + private static string AttachmentGroupName(string UserId) + => $"User_Attachment_{UserId.ToLowerInvariant()}"; + + private static bool TryCommentGroupName(RepositoryMonitorEvent e, out string groupName) + { + var userId = e.GetPreviousPropertyValue(nameof(UserComment.UserId)); + if (userId == null) + { + groupName = null; + return false; + } + groupName = CommentGroupName(userId); + return true; + } + + private static string CommentGroupName(string UserId) + => $"User_Comment_{UserId.ToLowerInvariant()}"; + public override Task OnConnected() { var userId = Context.QueryString["UserId"]; @@ -46,7 +77,12 @@ namespace Disco.Services.Users if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException("UserId"); - Groups.Add(Context.ConnectionId, GroupName(userId)); + var authorization = UserService.GetAuthorization(Context.User.Identity.Name); + + if (authorization.Has(Claims.User.ShowComments)) + Groups.Add(Context.ConnectionId, CommentGroupName(userId)); + if (authorization.Has(Claims.User.ShowAttachments)) + Groups.Add(Context.ConnectionId, AttachmentGroupName(userId)); return base.OnConnected(); } @@ -55,16 +91,10 @@ namespace Disco.Services.Users { if (e.EventType == RepositoryMonitorEventType.Deleted) { - if (e.EntityType == typeof(UserAttachment)) - { - var repositoryAttachment = (UserAttachment)e.Entity; - string attachmentUserId; - - using (DiscoDataContext Database = new DiscoDataContext()) - attachmentUserId = Database.UserAttachments.Where(a => a.Id == repositoryAttachment.Id).Select(a => a.UserId).First(); - - HubContext.Clients.Group(GroupName(attachmentUserId)).removeAttachment(repositoryAttachment.Id); - } + if (e.Entity is UserComment comment && TryCommentGroupName(e, out var commentGroupName)) + HubContext.Clients.Group(commentGroupName).commentRemoved(comment.Id); + else if (e.Entity is UserAttachment attachment && TryAttachmentGroupName(e, out var attachmentGroupName)) + HubContext.Clients.Group(attachmentGroupName).attachmentRemoved(attachment.Id); } } @@ -72,12 +102,10 @@ namespace Disco.Services.Users { if (e.EventType == RepositoryMonitorEventType.Added) { - if (e.EntityType == typeof(UserAttachment)) - { - var a = (UserAttachment)e.Entity; - - HubContext.Clients.Group(GroupName(a.UserId)).addAttachment(a.Id); - } + if (e.Entity is UserComment comment) + HubContext.Clients.Group(CommentGroupName(comment.UserId)).commentAdded(comment.Id); + else if (e.Entity is UserAttachment attachment) + HubContext.Clients.Group(AttachmentGroupName(attachment.UserId)).attachmentAdded(attachment.Id); } } } diff --git a/Disco.Web/Areas/API/Controllers/JobController.cs b/Disco.Web/Areas/API/Controllers/JobController.cs index f3586261..f78c4d2a 100644 --- a/Disco.Web/Areas/API/Controllers/JobController.cs +++ b/Disco.Web/Areas/API/Controllers/JobController.cs @@ -1830,7 +1830,7 @@ namespace Disco.Web.Areas.API.Controllers if (job == null) return BadRequest("Invalid Job Number"); - var results = job.JobLogs.OrderByDescending(m => m.Timestamp).Select(jl => Models.Shared.CommentModel.FromJobLog(jl)).ToList(); + var results = job.JobLogs.OrderByDescending(m => m.Timestamp).Select(jl => Models.Shared.CommentModel.FromEntity(jl)).ToList(); return Json(results); } @@ -1846,7 +1846,7 @@ namespace Disco.Web.Areas.API.Controllers if (jobLog == null) return BadRequest("Invalid JobLog Id"); - var c = Models.Shared.CommentModel.FromJobLog(jobLog); + var c = Models.Shared.CommentModel.FromEntity(jobLog); return Json(c); } diff --git a/Disco.Web/Areas/API/Controllers/UserController.cs b/Disco.Web/Areas/API/Controllers/UserController.cs index 969c8045..475a88d5 100644 --- a/Disco.Web/Areas/API/Controllers/UserController.cs +++ b/Disco.Web/Areas/API/Controllers/UserController.cs @@ -1,4 +1,5 @@ -using Disco.Services; +using Disco.Models.Repository; +using Disco.Services; using Disco.Services.Authorization; using Disco.Services.Interop; using Disco.Services.Interop.ActiveDirectory; @@ -8,7 +9,6 @@ using Disco.Services.Users; using Disco.Services.Web; using System; using System.Data.Entity; -using System.DirectoryServices.ActiveDirectory; using System.Linq; using System.Threading.Tasks; using System.Web.Mvc; @@ -17,6 +17,91 @@ namespace Disco.Web.Areas.API.Controllers { public partial class UserController : AuthorizedDatabaseController { + #region User Comments + + [DiscoAuthorize(Claims.User.ShowComments)] + [HttpPost, ValidateAntiForgeryToken] + public virtual ActionResult Comments(string id, string domain) + { + if (string.IsNullOrEmpty(id)) + throw new ArgumentNullException(nameof(id)); + + var userId = ActiveDirectory.ParseDomainAccountId(id, domain); + + var user = Database.Users + .Include(u => u.UserComments.Select(l => l.TechUser)) + .Where(u => u.UserId == userId).FirstOrDefault(); + if (user == null) + return BadRequest("Invalid User Id"); + + var results = user.UserComments.OrderByDescending(c => c.Timestamp).Select(c => Models.Shared.CommentModel.FromEntity(c)).ToList(); + return Json(results); + } + + [DiscoAuthorize(Claims.User.ShowComments)] + [HttpPost, ValidateAntiForgeryToken] + public virtual ActionResult Comment(int id) + { + var entity = Database.UserComments + .Include(c => c.TechUser) + .FirstOrDefault(c => c.Id == id); + + if (entity == null) + return BadRequest("Invalid User Comment Id"); + + var comment = Models.Shared.CommentModel.FromEntity(entity); + return Json(comment); + } + + [DiscoAuthorize(Claims.User.Actions.AddComments)] + [HttpPost, ValidateAntiForgeryToken] + public virtual ActionResult CommentAdd(string id, string domain, string comment = null) + { + if (string.IsNullOrEmpty(id)) + throw new ArgumentNullException(nameof(id)); + + var userId = ActiveDirectory.ParseDomainAccountId(id, domain); + + if (string.IsNullOrWhiteSpace(comment)) + return BadRequest("Comment is required"); + + var user = Database.Users.Find(userId); + if (user == null) + return BadRequest("Invalid User Id"); + + var entity = new UserComment() + { + UserId = user.UserId, + TechUserId = CurrentUser.UserId, + Timestamp = DateTime.Now, + Comments = comment + }; + Database.UserComments.Add(entity); + Database.SaveChanges(); + + return Json(entity.Id); + } + + [DiscoAuthorizeAny(Claims.User.Actions.RemoveAnyComments, Claims.User.Actions.RemoveOwnComments)] + [HttpPost, ValidateAntiForgeryToken] + public virtual ActionResult CommentRemove(int id) + { + var entity = Database.UserComments.Find(id); + if (entity != null) + { + if (entity.TechUserId.Equals(CurrentUser.UserId, StringComparison.OrdinalIgnoreCase)) + Authorization.RequireAny(Claims.User.Actions.RemoveAnyComments, Claims.User.Actions.RemoveOwnComments); + else + Authorization.Require(Claims.User.Actions.RemoveAnyComments); + + Database.UserComments.Remove(entity); + Database.SaveChanges(); + } + // Doesn't Exist/Already Deleted - OK + return Ok(); + } + #endregion + #region User Attachments [DiscoAuthorize(Claims.User.ShowAttachments)] @@ -74,7 +159,7 @@ namespace Disco.Web.Areas.API.Controllers if (string.IsNullOrWhiteSpace(comments)) comments = null; - var ua = new Disco.Models.Repository.UserAttachment() + var ua = new UserAttachment() { UserId = u.UserId, TechUserId = CurrentUser.UserId, diff --git a/Disco.Web/Areas/API/Models/Shared/CommentModel.cs b/Disco.Web/Areas/API/Models/Shared/CommentModel.cs index 613aa3cc..ee858d8b 100644 --- a/Disco.Web/Areas/API/Models/Shared/CommentModel.cs +++ b/Disco.Web/Areas/API/Models/Shared/CommentModel.cs @@ -16,18 +16,33 @@ namespace Disco.Web.Areas.API.Models.Shared public long TimestampUnixEpoc => Timestamp.ToUnixEpoc(); public string TimestampFull => Timestamp.ToFullDateTime(); - public static CommentModel FromJobLog(JobLog jl) + public static CommentModel FromEntity(JobLog log) { return new CommentModel { - Id = jl.Id, + Id = log.Id, TargetType = AttachmentTypes.Job, - TargetId = jl.JobId.ToString(), - AuthorId = jl.TechUserId, - Author = jl.TechUser.ToString(), - Timestamp = jl.Timestamp, - Comments = jl.Comments, - HtmlComments = jl.Comments.ToHtmlComment().ToString() + TargetId = log.JobId.ToString(), + AuthorId = log.TechUserId, + Author = log.TechUser.ToString(), + Timestamp = log.Timestamp, + Comments = log.Comments, + HtmlComments = log.Comments.ToHtmlComment().ToString() + }; + } + + public static CommentModel FromEntity(UserComment comment) + { + return new CommentModel + { + Id = comment.Id, + TargetType = AttachmentTypes.User, + TargetId = comment.UserId, + AuthorId = comment.TechUserId, + Author = comment.TechUser.ToString(), + Timestamp = comment.Timestamp, + Comments = comment.Comments, + HtmlComments = comment.Comments.ToHtmlComment().ToString() }; } diff --git a/Disco.Web/ClientSource/Style/User.css b/Disco.Web/ClientSource/Style/User.css index 1105413c..6ccf2d97 100644 --- a/Disco.Web/ClientSource/Style/User.css +++ b/Disco.Web/ClientSource/Style/User.css @@ -203,9 +203,144 @@ border-top: none; background-color: #eee; } +#UserDetailTab-CommentsAndJobs { + display: grid; + grid-template-columns: auto; +} +#UserDetailTab-CommentsAndJobs.canShowComments.canShowJobs { + grid-template-columns: 375px auto; +} +#UserDetailTab-CommentsAndJobs.canShowComments.canShowJobs > #UserDetailTab-Comments { + grid-column: 1; +} +#UserDetailTab-CommentsAndJobs.canShowComments.canShowJobs > #UserDetailTab-JobsContainer { + grid-column: 2; +} +#UserDetailTab-CommentsAndJobs.cannotShowComments div.jobTable { + border: 1px solid #ccc; +} +#Comments { + box-sizing: border-box; + height: 100%; + min-height: 373px; + padding-bottom: 51px; + border: 1px solid #ccc; + background-color: #fff; + position: relative; +} +#Comments div.commentInput { + border-top: 1px solid #ccc; + box-sizing: border-box; + width: 100%; + height: 51px; + padding: 5px; + position: absolute; + bottom: 0; + display: grid; + grid-template-columns: auto 40px; +} +#Comments div.commentInput textarea.commentInput { + grid-column: 1; + border: 0; + padding: 0; + margin: 0; + width: 100%; + height: 40px; + min-height: 40px; + overflow: auto; + resize: none; +} +#Comments div.commentInput button { + grid-column: 2; + appearance: none; + font-size: 1.5em; + display: block; + border: 1px solid #fff; + background-color: #fff; +} +#Comments div.commentInput button:not([disabled]):hover, +#Comments div.commentInput button:not([disabled]):focus { + color: #335A87; + background-color: #ededed; + border: 1px solid #ccc; +} +#Comments div.commentInput button[disabled] { + color: rgba(51, 51, 51, 0.2); + cursor: default; +} +#Comments div.commentOutput { + height: 100%; + overflow: auto; + background-color: #fafafa; + color: #000; +} +#Comments div.commentOutput > div { + padding: 3px; + margin: 4px 6px; + border-bottom: 1px solid #ccc; +} +#Comments div.commentOutput > div span.author { + color: #444; + display: block; + font-weight: 600; + font-size: 0.95em; + float: left; +} +#Comments div.commentOutput > div span.timestamp { + display: block; + float: right; + font-size: 0.9em; + font-style: italic; +} +#Comments div.commentOutput > div div.comment { + clear: both; + display: block; + margin-left: 4px; +} +#Comments div.commentOutput > div div.comment p { + line-height: 1.2em; + padding-bottom: 0.2em; +} +#Comments div.commentOutput > div div.comment h1, +#Comments div.commentOutput > div div.comment h2, +#Comments div.commentOutput > div div.comment h3, +#Comments div.commentOutput > div div.comment h4, +#Comments div.commentOutput > div div.comment h5 { + font-family: "Segoe UI", Arial, Verdana, Tahoma, sans-serif; + font-weight: 600; + font-size: 14px; + margin: 2px 0 !important; +} +#Comments div.commentOutput > div div.comment hr { + margin-top: 0.2em; +} +#Comments div.commentOutput > div div.comment code { + font-size: 0.9em; +} +#Comments div.commentOutput > div:hover span.remove { + opacity: 0.5; +} +#Comments div.commentOutput > div span.remove { + font-size: 1.2em; + color: #e51400; + margin-left: 6px; + cursor: pointer; + opacity: 0; +} +#Comments div.commentOutput > div span.remove:hover { + opacity: 1; +} +#Comments div.commentOutput > div:last-child { + border-bottom: none; +} +#Comments.cannotAddComments { + padding-bottom: 0; +} #UserDetailTab-JobsContainer div.jobTable { - margin: -1px; - border: 1px solid #ddd; + min-height: 320px; + border-top: 1px solid #ccc; + border-right: 1px solid #ccc; + border-bottom: 1px solid #ccc; } #UserDetailTab-JobsContainer .dataTables_wrapper .dataTables_filter { margin-top: -24px; diff --git a/Disco.Web/ClientSource/Style/User.less b/Disco.Web/ClientSource/Style/User.less index 74b03c5e..05385ae7 100644 --- a/Disco.Web/ClientSource/Style/User.less +++ b/Disco.Web/ClientSource/Style/User.less @@ -170,10 +170,170 @@ } } +#UserDetailTab-CommentsAndJobs { + display: grid; + grid-template-columns: auto; + + &.canShowComments.canShowJobs { + grid-template-columns: 375px auto; + + & > #UserDetailTab-Comments { + grid-column: 1; + } + + & > #UserDetailTab-JobsContainer { + grid-column: 2; + } + } + + &.cannotShowComments div.jobTable { + border: 1px solid @SubtleBorderColour; + } +} + +#Comments { + box-sizing: border-box; + height: 100%; + min-height: 373px; + padding-bottom: 51px; + border: 1px solid @SubtleBorderColour; + background-color: @white; + position: relative; + + div.commentInput { + border-top: 1px solid @SubtleBorderColour; + box-sizing: border-box; + width: 100%; + height: 51px; + padding: 5px; + position: absolute; + bottom: 0; + display: grid; + grid-template-columns: auto 40px; + + textarea.commentInput { + grid-column: 1; + border: 0; + padding: 0; + margin: 0; + width: 100%; + height: 40px; + min-height: 40px; + overflow: auto; + resize: none; + } + + button { + grid-column: 2; + appearance: none; + font-size: 1.5em; + display: block; + border: 1px solid @white; + background-color: @white; + + &:not([disabled]) { + &:hover, &:focus { + color: @HyperLinkColour; + background-color: @SubtleColour; + border: 1px solid @SubtleBorderColour; + } + } + + &[disabled] { + color: fade(@HeaderBackgroundColour, 20%); + cursor: default; + } + } + } + + div.commentOutput { + height: 100%; + overflow: auto; + background-color: @BackgroundColourLight; + color: @black; + + & > div { + padding: 3px; + margin: 4px 6px; + border-bottom: 1px solid @SubtleBorderColour; + + span.author { + color: #444; + display: block; + font-weight: @FontWeightBodyBold; + font-size: 0.95em; + float: left; + } + + span.timestamp { + display: block; + float: right; + font-size: 0.90em; + font-style: italic; + } + + div.comment { + clear: both; + display: block; + margin-left: 4px; + + p { + line-height: 1.2em; + padding-bottom: .2em; + } + + h1, h2, h3, h4, h5 { + font-family: @FontFamilyBody; + font-weight: 600; + font-size: 14px; + margin: 2px 0 !important; + } + + hr { + margin-top: .2em; + } + + code { + font-size: .9em; + } + } + + &:hover { + span.remove { + opacity: .5; + } + } + + span.remove { + font-size: 1.2em; + color: @StatusRemove; + margin-left: 6px; + cursor: pointer; + opacity: 0; + + &:hover { + opacity: 1; + } + } + + &:last-child { + border-bottom: none; + } + } + } + + &.cannotAddComments { + padding-bottom: 0; + } +} + #UserDetailTab-JobsContainer { + div.jobTable { - margin: -1px; - border: 1px solid #ddd; + min-height: 320px; + border-top: 1px solid @SubtleBorderColour; + border-right: 1px solid @SubtleBorderColour; + border-bottom: 1px solid @SubtleBorderColour; } .dataTables_wrapper { diff --git a/Disco.Web/ClientSource/Style/User.min.css b/Disco.Web/ClientSource/Style/User.min.css index 92e6cb44..d627515e 100644 --- a/Disco.Web/ClientSource/Style/User.min.css +++ b/Disco.Web/ClientSource/Style/User.min.css @@ -1 +1 @@ -.tableData{border:solid 1px #f4f4f4;border-collapse:collapse;}.tableData>tbody>tr>td{border:solid 1px #f4f4f4;background-color:#fff;}.tableData>tbody>tr:nth-child(odd)>td{background-color:hsl(0,0%,98.5%);}.tableData>thead>tr>th,.tableData>tbody>tr>th{background-color:#f4f4f4;border:solid 1px #f4f4f4;}.tableData>tbody>tr:hover>td{background-color:hsl(0,0%,97.5%);}.tableData>tfoot>tr>th,.tableData>tfoot>tr>td{background-color:#f4f4f4;}.tableDataDark{border:solid 1px #d8d8d8;border-collapse:collapse;}.tableDataDark td{border:solid 1px #d8d8d8;background-color:#fff;}.tableDataDark th{background-color:#eee;border:solid 1px #d8d8d8;}.tableDataContainer{background-color:#fff;}.tableDataVertical{border:solid 1px #f4f4f4;border-collapse:collapse;}.tableDataVertical>tbody>tr:nth-child(odd){background-color:#f4f4f4;margin:0;padding:0;}.tableDataVertical>tbody>tr>th.name{width:170px;text-align:right;}.tableDataVertical table.sub>tbody>tr:not(:first-child)>th,.tableDataVertical table.sub>tbody>tr:not(:first-child)>td{border-top:1px dashed #aaa;}.tableDataVertical table.sub>tbody>tr>th{font-weight:normal;text-align:right;}.tableDataVertical table.sub>tbody>tr>th.name{text-align:right;}.icon16{display:inline-block;height:16px;width:16px;margin-left:2px;cursor:pointer;}.subtleUntilHover{-moz-opacity:.3;opacity:.3;}.subtleUntilHover:hover{-moz-opacity:1;opacity:1;}#layout_PageHeading #User_Show_Flags{display:inline-block;float:right;font-size:.6em;}#layout_PageHeading #User_Show_Flags>i{cursor:default;}#layout_PageHeading #User_Show_Flags>i>.details{display:none;}#User_Show #User_Show_Subjects{table-layout:fixed;}#User_Show #User_Show_Subjects>tbody>tr>td{padding-top:0;height:100%;}#User_Show #User_Show_Subjects>tbody>tr>td>div div.status{margin-top:2px;padding-top:2px;border-top:1px dashed #ddd;}#User_Show #User_Show_Subjects>tbody>tr>td>div input.discreet{margin-left:-2px;}#User_Show #User_Show_Subjects>tbody>tr>td:not(:last-child){border-right:1px dashed #aaa;}#User_Show #User_Show_Subjects #User_Show_Details{width:330px;}#User_Show #User_Show_Subjects #User_Show_Details.hasPhoto{width:450px;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_Details_Photo_Container{float:left;padding-right:4px;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_Details_Photo{max-height:192px;width:auto;}#User_Show #User_Show_Subjects #User_Show_Details table.verticalHeadings{width:auto;}#User_Show #User_Show_Subjects #User_Show_Details table.verticalHeadings>tbody>tr>td:first-child{width:104px;font-weight:600;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_Details_Identity_Id{font-weight:600;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_GenerateDocument_Container{padding-top:4px;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_Details_Actions{margin-top:4px;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment{border-bottom:1px dashed #ddd;padding:4px;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment td:first-child{width:90px;font-weight:600;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment img.User_Show_AssignedDevices_CurrentAssignment_Image{float:left;width:64px;height:64px;margin-right:6px;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment div.User_Show_AssignedDevices_CurrentAssignment_Details{float:left;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment .User_Show_Assigned_Devices_CurrentAssignment_Flags{font-size:16px;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment .User_Show_Assigned_Devices_CurrentAssignment_Flags>i{cursor:default;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment .User_Show_Assigned_Devices_CurrentAssignment_Flags>i>.details{display:none;}#User_Show #User_Show_Subjects #User_Show_Subjects_Actions>td{padding-top:4px;}#UserDetailTabs{margin-top:10px;border-radius:0;background-image:none;background-color:#fff;border:0;padding:0;}#UserDetailTabs #UserDetailTabItems{border-radius:0;border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;border-bottom:0;padding:2px 0 0 4px;background-image:none;background-color:#eee;display:table;}#UserDetailTabs #UserDetailTabItems>li{top:0;border-radius:0;margin:0 5px 0 0;padding:0;line-height:normal;margin-right:4px;}#UserDetailTabs #UserDetailTabItems>li>a{padding:5px 8px;}#UserDetailTabs div.ui-tabs-panel{border-radius:0;padding:4px;border-right:1px solid #ddd;border-bottom:1px solid #ddd;border-left:1px solid #ddd;border-top:0;background-color:#eee;}#UserDetailTab-JobsContainer div.jobTable{margin:-1px;border:1px solid #ddd;}#UserDetailTab-JobsContainer .dataTables_wrapper .dataTables_filter{margin-top:-24px;-moz-opacity:1;opacity:1;}#UserDetailTab-JobsContainer .dataTables_wrapper .dataTables_length{margin-top:-24px;-moz-opacity:1;opacity:1;}#UserDetailTab-JobsContainer .dataTables_wrapper .dataTables_showStatusClosed{right:220px;margin-top:-24px;}#User_Show_Details_Actions_AddFlag_Dialog{height:400px;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker{position:absolute;width:250px;height:300px;overflow-y:auto;background-color:#fcfcfc;border:1px solid #ccc;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>input{box-sizing:border-box;width:100%;border:0;border-bottom:1px solid #ddd;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>div{background-color:#fff;border-bottom:1px solid #ddd;padding:6px 0 6px 6px;cursor:pointer;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>div:hover{background-color:#f4f4f4;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>div.selected,#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>div.selected:hover{background-color:#eee;}#User_Show_Details_Actions_AddFlag_Dialog .details{display:none;position:absolute;left:280px;top:1em;}#User_Show_Details_Actions_AddFlag_Dialog .details h4{margin-bottom:4px;}#User_Show_Details_Actions_AddFlag_Dialog .details textarea{min-width:280px;height:200px;}#UserDetailTab-Flags #userFlags{border:solid 1px #d8d8d8;border-collapse:collapse;table-layout:fixed;}#UserDetailTab-Flags #userFlags td{border:solid 1px #d8d8d8;background-color:#fff;}#UserDetailTab-Flags #userFlags th{background-color:#eee;border:solid 1px #d8d8d8;}#UserDetailTab-Flags #userFlags i.fa-edit{position:absolute;top:0;right:0;margin-top:4px;font-size:1.1em;cursor:pointer;display:none;color:#335a87;}#UserDetailTab-Flags #userFlags i.fa-edit:hover{color:#5e8cc2;}#UserDetailTab-Flags #userFlags td:hover i.fa-edit{display:inline-block;}#UserDetailTab-Flags #userFlags th.name{width:200px;}#UserDetailTab-Flags #userFlags tr.removed td{background-color:#f4f4f4;}#UserDetailTab-Flags #userFlags td.name{vertical-align:middle;}#UserDetailTab-Flags #userFlags td.name .fa-stack{line-height:1.6em;}#UserDetailTab-Flags #userFlags td.added,#UserDetailTab-Flags #userFlags td.removed{vertical-align:middle;}#UserDetailTab-Flags #userFlags td.added .expressionResult,#UserDetailTab-Flags #userFlags td.removed .expressionResult{margin-top:4px;font-style:italic;}#UserDetailTab-Flags #userFlags td.comments{vertical-align:middle;}#UserDetailTab-Flags #userFlags td.comments .editable{position:relative;}#UserDetailTab-Flags #userFlags td.comments .commentsRaw{display:none;}#UserDetailTab-Flags #userFlags td.removed.na{vertical-align:middle;text-align:center;}#UserDetailTab-Flags>.none{text-align:center;padding:30px 0;font-style:italic;background-color:#fff;}#User_Show_Flags_Actions_EditComments_Dialog h4{margin-bottom:4px;}#User_Show_Flags_Actions_EditComments_Dialog_Comments{width:280px;}#UserDetailTab-Authorization #UserDetailTab-AuthorizationContainer{background-color:#fff;border:1px solid #ccc;}#UserDetailTab-Authorization #UserDetailTab-Authorization_ClaimsTree_Container{width:50%;float:left;padding:6px 10px 6px 4px;}#UserDetailTab-Authorization #UserDetailTab-Authorization_ClaimsTree_Container>span.smallMessage:last-child{display:block;text-align:right;}#UserDetailTab-Authorization #UserDetailTab-Authorization_Membership{width:40%;float:right;padding:6px 10px;border-left:1px dashed #ccc;border-bottom:1px dashed #ccc;}#UserDetailTab-Authorization #UserDetailTab-Authorization_Membership #UserDetailTab-Authorization_Membership_Roles{margin-bottom:10px;}#UserDetailTab-Authorization #UserDetailTab-Authorization_Membership #UserDetailTab-Authorization_Membership_Groups_Container>span.smallMessage:last-child{display:block;text-align:right;}#UserDetailTab-Authorization #UserDetailTab-Authorization_NoAccess{width:50%;float:left;padding:6px 10px;}#UserDetailTab-Authorization #UserDetailTab-Authorization_NoAccess h3{margin-bottom:10px;}#userShowResources #AttachmentsContainer{padding:0;}#userShowResources #Attachments{position:relative;border:1px solid #ccc;background-color:#fff;}#userShowResources #Attachments div.attachmentOutput{position:relative;height:320px;overflow:auto;}#userShowResources #Attachments div.attachmentOutput>a{display:block;float:left;height:48px;width:218px;padding:2px;margin:2px;font-size:.9em;border:1px solid #fff;color:#000;text-decoration:none;}#userShowResources #Attachments div.attachmentOutput>a span.comments,#userShowResources #Attachments div.attachmentOutput>a span.author,#userShowResources #Attachments div.attachmentOutput>a span.timestamp{display:block;float:left;width:168px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;height:16px;}#userShowResources #Attachments div.attachmentOutput>a span.author{color:#888;width:150px;}#userShowResources #Attachments div.attachmentOutput>a span.timestamp{color:#888;font-style:italic;}#userShowResources #Attachments div.attachmentOutput>a span.icon{display:block;float:left;height:48px;width:48px;margin-right:2px;}#userShowResources #Attachments div.attachmentOutput>a span.icon img{height:48px;width:48px;}#userShowResources #Attachments div.attachmentOutput>a span.icon img.loading{display:none;}#userShowResources #Attachments div.attachmentOutput>a:hover{background-color:#ededed;border:1px solid #ccc;}#userShowResources #Attachments div.attachmentOutput>a:hover span.remove{opacity:.5;}#userShowResources #Attachments div.attachmentOutput>a span.remove{font-size:1.4em;color:#e51400;margin-left:5px;cursor:pointer;opacity:0;}#userShowResources #Attachments div.attachmentOutput>a span.remove:hover{opacity:1;}#userShowResources #Attachments.cannotAddAttachments div.attachmentOutput{height:162px;}#userShowResources #Attachments div.attachmentInput{border-top:1px solid #ccc;height:40px;background-color:#fff;padding:3px;}#userShowResources #Attachments div.attachmentInput span.action{display:block;margin:0 4px 0 0;font-size:1.5em;cursor:pointer;float:right;padding:.5em;}#userShowResources #Attachments div.attachmentInput span.action:not(.fa-spin){color:#333;border:1px solid #fff;}#userShowResources #Attachments div.attachmentInput span.action:not(.fa-spin):hover{color:#335a87;background-color:#ededed;border:1px solid #ccc;}#userShowResources #Attachments div.attachmentInput span.action:not(.fa-spin).disabled{color:rgba(51,51,51,.2);cursor:default;}#userShowResources #Attachments div.attachmentInput span.action:not(.fa-spin).disabled:hover{color:rgba(51,51,51,.2);background-color:inherit;border:1px solid #fff;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments{margin-top:6px;background-color:#fff;line-height:1.3;border:1px solid #ddd;max-height:300px;overflow-y:auto;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment{display:block;padding:4px;cursor:pointer;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment:not(:last-child){border-bottom:1px dashed #ddd;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment:hover{background-color:#f4f4f4;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment tr:first-child td{width:68px;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment td:first-child{width:90px;font-weight:600;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment img.CreateJob_Assignment_Image{width:64px;height:64px;} \ No newline at end of file +.tableData{border:solid 1px #f4f4f4;border-collapse:collapse;}.tableData>tbody>tr>td{border:solid 1px #f4f4f4;background-color:#fff;}.tableData>tbody>tr:nth-child(odd)>td{background-color:hsl(0,0%,98.5%);}.tableData>thead>tr>th,.tableData>tbody>tr>th{background-color:#f4f4f4;border:solid 1px #f4f4f4;}.tableData>tbody>tr:hover>td{background-color:hsl(0,0%,97.5%);}.tableData>tfoot>tr>th,.tableData>tfoot>tr>td{background-color:#f4f4f4;}.tableDataDark{border:solid 1px #d8d8d8;border-collapse:collapse;}.tableDataDark td{border:solid 1px #d8d8d8;background-color:#fff;}.tableDataDark th{background-color:#eee;border:solid 1px #d8d8d8;}.tableDataContainer{background-color:#fff;}.tableDataVertical{border:solid 1px #f4f4f4;border-collapse:collapse;}.tableDataVertical>tbody>tr:nth-child(odd){background-color:#f4f4f4;margin:0;padding:0;}.tableDataVertical>tbody>tr>th.name{width:170px;text-align:right;}.tableDataVertical table.sub>tbody>tr:not(:first-child)>th,.tableDataVertical table.sub>tbody>tr:not(:first-child)>td{border-top:1px dashed #aaa;}.tableDataVertical table.sub>tbody>tr>th{font-weight:normal;text-align:right;}.tableDataVertical table.sub>tbody>tr>th.name{text-align:right;}.icon16{display:inline-block;height:16px;width:16px;margin-left:2px;cursor:pointer;}.subtleUntilHover{-moz-opacity:.3;opacity:.3;}.subtleUntilHover:hover{-moz-opacity:1;opacity:1;}#layout_PageHeading #User_Show_Flags{display:inline-block;float:right;font-size:.6em;}#layout_PageHeading #User_Show_Flags>i{cursor:default;}#layout_PageHeading #User_Show_Flags>i>.details{display:none;}#User_Show #User_Show_Subjects{table-layout:fixed;}#User_Show #User_Show_Subjects>tbody>tr>td{padding-top:0;height:100%;}#User_Show #User_Show_Subjects>tbody>tr>td>div div.status{margin-top:2px;padding-top:2px;border-top:1px dashed #ddd;}#User_Show #User_Show_Subjects>tbody>tr>td>div input.discreet{margin-left:-2px;}#User_Show #User_Show_Subjects>tbody>tr>td:not(:last-child){border-right:1px dashed #aaa;}#User_Show #User_Show_Subjects #User_Show_Details{width:330px;}#User_Show #User_Show_Subjects #User_Show_Details.hasPhoto{width:450px;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_Details_Photo_Container{float:left;padding-right:4px;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_Details_Photo{max-height:192px;width:auto;}#User_Show #User_Show_Subjects #User_Show_Details table.verticalHeadings{width:auto;}#User_Show #User_Show_Subjects #User_Show_Details table.verticalHeadings>tbody>tr>td:first-child{width:104px;font-weight:600;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_Details_Identity_Id{font-weight:600;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_GenerateDocument_Container{padding-top:4px;}#User_Show #User_Show_Subjects #User_Show_Details #User_Show_Details_Actions{margin-top:4px;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment{border-bottom:1px dashed #ddd;padding:4px;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment td:first-child{width:90px;font-weight:600;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment img.User_Show_AssignedDevices_CurrentAssignment_Image{float:left;width:64px;height:64px;margin-right:6px;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment div.User_Show_AssignedDevices_CurrentAssignment_Details{float:left;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment .User_Show_Assigned_Devices_CurrentAssignment_Flags{font-size:16px;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment .User_Show_Assigned_Devices_CurrentAssignment_Flags>i{cursor:default;}#User_Show #User_Show_Subjects #User_Show_AssignedDevices .User_Show_AssignedDevices_CurrentAssignment .User_Show_Assigned_Devices_CurrentAssignment_Flags>i>.details{display:none;}#User_Show #User_Show_Subjects #User_Show_Subjects_Actions>td{padding-top:4px;}#UserDetailTabs{margin-top:10px;border-radius:0;background-image:none;background-color:#fff;border:0;padding:0;}#UserDetailTabs #UserDetailTabItems{border-radius:0;border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;border-bottom:0;padding:2px 0 0 4px;background-image:none;background-color:#eee;display:table;}#UserDetailTabs #UserDetailTabItems>li{top:0;border-radius:0;margin:0 5px 0 0;padding:0;line-height:normal;margin-right:4px;}#UserDetailTabs #UserDetailTabItems>li>a{padding:5px 8px;}#UserDetailTabs div.ui-tabs-panel{border-radius:0;padding:4px;border-right:1px solid #ddd;border-bottom:1px solid #ddd;border-left:1px solid #ddd;border-top:0;background-color:#eee;}#UserDetailTab-CommentsAndJobs{display:grid;grid-template-columns:auto;}#UserDetailTab-CommentsAndJobs.canShowComments.canShowJobs{grid-template-columns:375px auto;}#UserDetailTab-CommentsAndJobs.canShowComments.canShowJobs>#UserDetailTab-Comments{grid-column:1;}#UserDetailTab-CommentsAndJobs.canShowComments.canShowJobs>#UserDetailTab-JobsContainer{grid-column:2;}#UserDetailTab-CommentsAndJobs.cannotShowComments div.jobTable{border:1px solid #ccc;}#Comments{box-sizing:border-box;height:100%;min-height:373px;padding-bottom:51px;border:1px solid #ccc;background-color:#fff;position:relative;}#Comments div.commentInput{border-top:1px solid #ccc;box-sizing:border-box;width:100%;height:51px;padding:5px;position:absolute;bottom:0;display:grid;grid-template-columns:auto 40px;}#Comments div.commentInput textarea.commentInput{grid-column:1;border:0;padding:0;margin:0;width:100%;height:40px;min-height:40px;overflow:auto;resize:none;}#Comments div.commentInput button{grid-column:2;appearance:none;font-size:1.5em;display:block;border:1px solid #fff;background-color:#fff;}#Comments div.commentInput button:not([disabled]):hover,#Comments div.commentInput button:not([disabled]):focus{color:#335a87;background-color:#ededed;border:1px solid #ccc;}#Comments div.commentInput button[disabled]{color:rgba(51,51,51,.2);cursor:default;}#Comments div.commentOutput{height:100%;overflow:auto;background-color:#fafafa;color:#000;}#Comments div.commentOutput>div{padding:3px;margin:4px 6px;border-bottom:1px solid #ccc;}#Comments div.commentOutput>div span.author{color:#444;display:block;font-weight:600;font-size:.95em;float:left;}#Comments div.commentOutput>div span.timestamp{display:block;float:right;font-size:.9em;font-style:italic;}#Comments div.commentOutput>div div.comment{clear:both;display:block;margin-left:4px;}#Comments div.commentOutput>div div.comment p{line-height:1.2em;padding-bottom:.2em;}#Comments div.commentOutput>div div.comment h1,#Comments div.commentOutput>div div.comment h2,#Comments div.commentOutput>div div.comment h3,#Comments div.commentOutput>div div.comment h4,#Comments div.commentOutput>div div.comment h5{font-family:"Segoe UI",Arial,Verdana,Tahoma,sans-serif;font-weight:600;font-size:14px;margin:2px 0!important;}#Comments div.commentOutput>div div.comment hr{margin-top:.2em;}#Comments div.commentOutput>div div.comment code{font-size:.9em;}#Comments div.commentOutput>div:hover span.remove{opacity:.5;}#Comments div.commentOutput>div span.remove{font-size:1.2em;color:#e51400;margin-left:6px;cursor:pointer;opacity:0;}#Comments div.commentOutput>div span.remove:hover{opacity:1;}#Comments div.commentOutput>div:last-child{border-bottom:0;}#Comments.cannotAddComments{padding-bottom:0;}#UserDetailTab-JobsContainer div.jobTable{min-height:320px;border-top:1px solid #ccc;border-right:1px solid #ccc;border-bottom:1px solid #ccc;}#UserDetailTab-JobsContainer .dataTables_wrapper .dataTables_filter{margin-top:-24px;-moz-opacity:1;opacity:1;}#UserDetailTab-JobsContainer .dataTables_wrapper .dataTables_length{margin-top:-24px;-moz-opacity:1;opacity:1;}#UserDetailTab-JobsContainer .dataTables_wrapper .dataTables_showStatusClosed{right:220px;margin-top:-24px;}#User_Show_Details_Actions_AddFlag_Dialog{height:400px;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker{position:absolute;width:250px;height:300px;overflow-y:auto;background-color:#fcfcfc;border:1px solid #ccc;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>input{box-sizing:border-box;width:100%;border:0;border-bottom:1px solid #ddd;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>div{background-color:#fff;border-bottom:1px solid #ddd;padding:6px 0 6px 6px;cursor:pointer;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>div:hover{background-color:#f4f4f4;}#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>div.selected,#User_Show_Details_Actions_AddFlag_Dialog .flagPicker>div.selected:hover{background-color:#eee;}#User_Show_Details_Actions_AddFlag_Dialog .details{display:none;position:absolute;left:280px;top:1em;}#User_Show_Details_Actions_AddFlag_Dialog .details h4{margin-bottom:4px;}#User_Show_Details_Actions_AddFlag_Dialog .details textarea{min-width:280px;height:200px;}#UserDetailTab-Flags #userFlags{border:solid 1px #d8d8d8;border-collapse:collapse;table-layout:fixed;}#UserDetailTab-Flags #userFlags td{border:solid 1px #d8d8d8;background-color:#fff;}#UserDetailTab-Flags #userFlags th{background-color:#eee;border:solid 1px #d8d8d8;}#UserDetailTab-Flags #userFlags i.fa-edit{position:absolute;top:0;right:0;margin-top:4px;font-size:1.1em;cursor:pointer;display:none;color:#335a87;}#UserDetailTab-Flags #userFlags i.fa-edit:hover{color:#5e8cc2;}#UserDetailTab-Flags #userFlags td:hover i.fa-edit{display:inline-block;}#UserDetailTab-Flags #userFlags th.name{width:200px;}#UserDetailTab-Flags #userFlags tr.removed td{background-color:#f4f4f4;}#UserDetailTab-Flags #userFlags td.name{vertical-align:middle;}#UserDetailTab-Flags #userFlags td.name .fa-stack{line-height:1.6em;}#UserDetailTab-Flags #userFlags td.added,#UserDetailTab-Flags #userFlags td.removed{vertical-align:middle;}#UserDetailTab-Flags #userFlags td.added .expressionResult,#UserDetailTab-Flags #userFlags td.removed .expressionResult{margin-top:4px;font-style:italic;}#UserDetailTab-Flags #userFlags td.comments{vertical-align:middle;}#UserDetailTab-Flags #userFlags td.comments .editable{position:relative;}#UserDetailTab-Flags #userFlags td.comments .commentsRaw{display:none;}#UserDetailTab-Flags #userFlags td.removed.na{vertical-align:middle;text-align:center;}#UserDetailTab-Flags>.none{text-align:center;padding:30px 0;font-style:italic;background-color:#fff;}#User_Show_Flags_Actions_EditComments_Dialog h4{margin-bottom:4px;}#User_Show_Flags_Actions_EditComments_Dialog_Comments{width:280px;}#UserDetailTab-Authorization #UserDetailTab-AuthorizationContainer{background-color:#fff;border:1px solid #ccc;}#UserDetailTab-Authorization #UserDetailTab-Authorization_ClaimsTree_Container{width:50%;float:left;padding:6px 10px 6px 4px;}#UserDetailTab-Authorization #UserDetailTab-Authorization_ClaimsTree_Container>span.smallMessage:last-child{display:block;text-align:right;}#UserDetailTab-Authorization #UserDetailTab-Authorization_Membership{width:40%;float:right;padding:6px 10px;border-left:1px dashed #ccc;border-bottom:1px dashed #ccc;}#UserDetailTab-Authorization #UserDetailTab-Authorization_Membership #UserDetailTab-Authorization_Membership_Roles{margin-bottom:10px;}#UserDetailTab-Authorization #UserDetailTab-Authorization_Membership #UserDetailTab-Authorization_Membership_Groups_Container>span.smallMessage:last-child{display:block;text-align:right;}#UserDetailTab-Authorization #UserDetailTab-Authorization_NoAccess{width:50%;float:left;padding:6px 10px;}#UserDetailTab-Authorization #UserDetailTab-Authorization_NoAccess h3{margin-bottom:10px;}#userShowResources #AttachmentsContainer{padding:0;}#userShowResources #Attachments{position:relative;border:1px solid #ccc;background-color:#fff;}#userShowResources #Attachments div.attachmentOutput{position:relative;height:320px;overflow:auto;}#userShowResources #Attachments div.attachmentOutput>a{display:block;float:left;height:48px;width:218px;padding:2px;margin:2px;font-size:.9em;border:1px solid #fff;color:#000;text-decoration:none;}#userShowResources #Attachments div.attachmentOutput>a span.comments,#userShowResources #Attachments div.attachmentOutput>a span.author,#userShowResources #Attachments div.attachmentOutput>a span.timestamp{display:block;float:left;width:168px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;height:16px;}#userShowResources #Attachments div.attachmentOutput>a span.author{color:#888;width:150px;}#userShowResources #Attachments div.attachmentOutput>a span.timestamp{color:#888;font-style:italic;}#userShowResources #Attachments div.attachmentOutput>a span.icon{display:block;float:left;height:48px;width:48px;margin-right:2px;}#userShowResources #Attachments div.attachmentOutput>a span.icon img{height:48px;width:48px;}#userShowResources #Attachments div.attachmentOutput>a span.icon img.loading{display:none;}#userShowResources #Attachments div.attachmentOutput>a:hover{background-color:#ededed;border:1px solid #ccc;}#userShowResources #Attachments div.attachmentOutput>a:hover span.remove{opacity:.5;}#userShowResources #Attachments div.attachmentOutput>a span.remove{font-size:1.4em;color:#e51400;margin-left:5px;cursor:pointer;opacity:0;}#userShowResources #Attachments div.attachmentOutput>a span.remove:hover{opacity:1;}#userShowResources #Attachments.cannotAddAttachments div.attachmentOutput{height:162px;}#userShowResources #Attachments div.attachmentInput{border-top:1px solid #ccc;height:40px;background-color:#fff;padding:3px;}#userShowResources #Attachments div.attachmentInput span.action{display:block;margin:0 4px 0 0;font-size:1.5em;cursor:pointer;float:right;padding:.5em;}#userShowResources #Attachments div.attachmentInput span.action:not(.fa-spin){color:#333;border:1px solid #fff;}#userShowResources #Attachments div.attachmentInput span.action:not(.fa-spin):hover{color:#335a87;background-color:#ededed;border:1px solid #ccc;}#userShowResources #Attachments div.attachmentInput span.action:not(.fa-spin).disabled{color:rgba(51,51,51,.2);cursor:default;}#userShowResources #Attachments div.attachmentInput span.action:not(.fa-spin).disabled:hover{color:rgba(51,51,51,.2);background-color:inherit;border:1px solid #fff;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments{margin-top:6px;background-color:#fff;line-height:1.3;border:1px solid #ddd;max-height:300px;overflow-y:auto;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment{display:block;padding:4px;cursor:pointer;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment:not(:last-child){border-bottom:1px dashed #ddd;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment:hover{background-color:#f4f4f4;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment tr:first-child td{width:68px;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment td:first-child{width:90px;font-weight:600;}#User_Show_Details_Actions_CreateJob_Dialog #CreateJob_Assignments li.CreateJob_Assignment img.CreateJob_Assignment_Image{width:64px;height:64px;} \ No newline at end of file diff --git a/Disco.Web/Controllers/UserController.cs b/Disco.Web/Controllers/UserController.cs index c1f56475..1e479e42 100644 --- a/Disco.Web/Controllers/UserController.cs +++ b/Disco.Web/Controllers/UserController.cs @@ -24,7 +24,7 @@ namespace Disco.Web.Controllers var m = new Models.User.IndexModel(); // UI Extensions - UIExtensions.ExecuteExtensions(this.ControllerContext, m); + UIExtensions.ExecuteExtensions(ControllerContext, m); return View(); } @@ -64,6 +64,7 @@ namespace Disco.Web.Controllers .Include(u => u.UserFlagAssignments.Select(ufa => ufa.AddedUser)) .Include(u => u.UserFlagAssignments.Select(ufa => ufa.RemovedUser)) .Include(u => u.UserDetails) + .Include(u => u.UserComments.Select(c => c.TechUser)) .FirstOrDefault(um => um.UserId == id); if (m.User == null) @@ -121,7 +122,7 @@ namespace Disco.Web.Controllers } // UI Extensions - UIExtensions.ExecuteExtensions(this.ControllerContext, m); + UIExtensions.ExecuteExtensions(ControllerContext, m); return View(m); } diff --git a/Disco.Web/Disco.Web.csproj b/Disco.Web/Disco.Web.csproj index 6d10b456..69d601ed 100644 --- a/Disco.Web/Disco.Web.csproj +++ b/Disco.Web/Disco.Web.csproj @@ -1331,10 +1331,15 @@ True _AssignmentHistory.cshtml - + + _CommentsAndJobs.cshtml + True + True + + + Comments.cshtml True True - _Jobs.cshtml _Resources.cshtml @@ -2566,9 +2571,13 @@ RazorGenerator _AssignmentHistory.generated.cs - + RazorGenerator - _Jobs.generated.cs + _CommentsAndJobs.generated.cs + + + RazorGenerator + Comments.generated.cs RazorGenerator diff --git a/Disco.Web/Extensions/T4MVC/API.UserController.generated.cs b/Disco.Web/Extensions/T4MVC/API.UserController.generated.cs index df8643b7..cd41d1d9 100644 --- a/Disco.Web/Extensions/T4MVC/API.UserController.generated.cs +++ b/Disco.Web/Extensions/T4MVC/API.UserController.generated.cs @@ -59,6 +59,30 @@ namespace Disco.Web.Areas.API.Controllers return RedirectToActionPermanent(taskResult.Result); } + [NonAction] + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public virtual System.Web.Mvc.ActionResult Comments() + { + return new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.Comments); + } + [NonAction] + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public virtual System.Web.Mvc.ActionResult Comment() + { + return new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.Comment); + } + [NonAction] + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public virtual System.Web.Mvc.ActionResult CommentAdd() + { + return new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.CommentAdd); + } + [NonAction] + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public virtual System.Web.Mvc.ActionResult CommentRemove() + { + return new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.CommentRemove); + } [NonAction] [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] public virtual System.Web.Mvc.ActionResult AttachmentDownload() @@ -136,6 +160,10 @@ namespace Disco.Web.Areas.API.Controllers [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] public class ActionNamesClass { + public readonly string Comments = "Comments"; + public readonly string Comment = "Comment"; + public readonly string CommentAdd = "CommentAdd"; + public readonly string CommentRemove = "CommentRemove"; public readonly string AttachmentDownload = "AttachmentDownload"; public readonly string AttachmentThumbnail = "AttachmentThumbnail"; public readonly string AttachmentUpload = "AttachmentUpload"; @@ -151,6 +179,10 @@ namespace Disco.Web.Areas.API.Controllers [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] public class ActionNameConstants { + public const string Comments = "Comments"; + public const string Comment = "Comment"; + public const string CommentAdd = "CommentAdd"; + public const string CommentRemove = "CommentRemove"; public const string AttachmentDownload = "AttachmentDownload"; public const string AttachmentThumbnail = "AttachmentThumbnail"; public const string AttachmentUpload = "AttachmentUpload"; @@ -164,6 +196,41 @@ namespace Disco.Web.Areas.API.Controllers } + static readonly ActionParamsClass_Comments s_params_Comments = new ActionParamsClass_Comments(); + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public ActionParamsClass_Comments CommentsParams { get { return s_params_Comments; } } + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public class ActionParamsClass_Comments + { + public readonly string id = "id"; + public readonly string domain = "domain"; + } + static readonly ActionParamsClass_Comment s_params_Comment = new ActionParamsClass_Comment(); + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public ActionParamsClass_Comment CommentParams { get { return s_params_Comment; } } + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public class ActionParamsClass_Comment + { + public readonly string id = "id"; + } + static readonly ActionParamsClass_CommentAdd s_params_CommentAdd = new ActionParamsClass_CommentAdd(); + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public ActionParamsClass_CommentAdd CommentAddParams { get { return s_params_CommentAdd; } } + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public class ActionParamsClass_CommentAdd + { + public readonly string id = "id"; + public readonly string domain = "domain"; + public readonly string comment = "comment"; + } + static readonly ActionParamsClass_CommentRemove s_params_CommentRemove = new ActionParamsClass_CommentRemove(); + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public ActionParamsClass_CommentRemove CommentRemoveParams { get { return s_params_CommentRemove; } } + [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] + public class ActionParamsClass_CommentRemove + { + public readonly string id = "id"; + } static readonly ActionParamsClass_AttachmentDownload s_params_AttachmentDownload = new ActionParamsClass_AttachmentDownload(); [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode] public ActionParamsClass_AttachmentDownload AttachmentDownloadParams { get { return s_params_AttachmentDownload; } } @@ -271,6 +338,57 @@ namespace Disco.Web.Areas.API.Controllers { public T4MVC_UserController() : base(Dummy.Instance) { } + [NonAction] + partial void CommentsOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, string id, string domain); + + [NonAction] + public override System.Web.Mvc.ActionResult Comments(string id, string domain) + { + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.Comments); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "id", id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "domain", domain); + CommentsOverride(callInfo, id, domain); + return callInfo; + } + + [NonAction] + partial void CommentOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id); + + [NonAction] + public override System.Web.Mvc.ActionResult Comment(int id) + { + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.Comment); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "id", id); + CommentOverride(callInfo, id); + return callInfo; + } + + [NonAction] + partial void CommentAddOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, string id, string domain, string comment); + + [NonAction] + public override System.Web.Mvc.ActionResult CommentAdd(string id, string domain, string comment) + { + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.CommentAdd); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "id", id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "domain", domain); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "comment", comment); + CommentAddOverride(callInfo, id, domain, comment); + return callInfo; + } + + [NonAction] + partial void CommentRemoveOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id); + + [NonAction] + public override System.Web.Mvc.ActionResult CommentRemove(int id) + { + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.CommentRemove); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "id", id); + CommentRemoveOverride(callInfo, id); + return callInfo; + } + [NonAction] partial void AttachmentDownloadOverride(T4MVC_System_Web_Mvc_ActionResult callInfo, int id); diff --git a/Disco.Web/Extensions/T4MVC/T4MVCExtensions.cs b/Disco.Web/Extensions/T4MVC/T4MVCExtensions.cs index 33908123..784612c5 100644 --- a/Disco.Web/Extensions/T4MVC/T4MVCExtensions.cs +++ b/Disco.Web/Extensions/T4MVC/T4MVCExtensions.cs @@ -1,6 +1,5 @@ using Disco.Services.Interop.ActiveDirectory; using System; -using System.Threading.Tasks; using System.Web.Mvc; namespace Disco.Web.Controllers @@ -38,13 +37,34 @@ namespace Disco.Web.Areas.API.Controllers { public partial class UserController { + [NonAction] + public virtual ActionResult Comments(string id) + { + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, nameof(Comments)); + + Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); + + return callInfo; + } + + [NonAction] + public virtual ActionResult CommentAdd(string id, string comment = null) + { + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, nameof(CommentAdd)); + + Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, nameof(comment), comment); + + return callInfo; + } + [NonAction] public virtual ActionResult AttachmentUpload(string id, string Comments) { - var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.AttachmentUpload); + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, nameof(AttachmentUpload)); - Disco.Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); - ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "Comments", Comments); + Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, nameof(Comments), Comments); return callInfo; } @@ -53,9 +73,9 @@ namespace Disco.Web.Areas.API.Controllers [NonAction] public virtual ActionResult Attachments(string id) { - var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.Attachments); + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, nameof(Attachments)); - Disco.Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); + Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); return callInfo; } @@ -63,9 +83,9 @@ namespace Disco.Web.Areas.API.Controllers [NonAction] public virtual ActionResult AttachmentOnlineUploadSession(string id) { - var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.AttachmentOnlineUploadSession); + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, nameof(AttachmentOnlineUploadSession)); - Disco.Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); + Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); return callInfo; } @@ -73,10 +93,10 @@ namespace Disco.Web.Areas.API.Controllers [NonAction] public virtual ActionResult GeneratePdf(string id, string DocumentTemplateId) { - var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.GeneratePdf); + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, nameof(GeneratePdf)); - Disco.Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); - ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "DocumentTemplateId", DocumentTemplateId); + Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, nameof(DocumentTemplateId), DocumentTemplateId); return callInfo; } @@ -84,10 +104,10 @@ namespace Disco.Web.Areas.API.Controllers [NonAction] public virtual ActionResult GeneratePdfPackage(string id, string DocumentTemplatePackageId) { - var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.GeneratePdfPackage); + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, nameof(GeneratePdfPackage)); - Disco.Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); - ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, "DocumentTemplatePackageId", DocumentTemplatePackageId); + Web.Controllers.UserController.T4MVCAddUserIdRouteValues(callInfo, id); + ModelUnbinderHelpers.AddRouteValues(callInfo.RouteValueDictionary, nameof(DocumentTemplatePackageId), DocumentTemplatePackageId); return callInfo; } diff --git a/Disco.Web/Extensions/T4MVC/UserController.generated.cs b/Disco.Web/Extensions/T4MVC/UserController.generated.cs index c7abc4ac..98a00b80 100644 --- a/Disco.Web/Extensions/T4MVC/UserController.generated.cs +++ b/Disco.Web/Extensions/T4MVC/UserController.generated.cs @@ -132,17 +132,19 @@ namespace Disco.Web.Controllers { public readonly string _AssignmentHistory = "_AssignmentHistory"; public readonly string _Authorization = "_Authorization"; + public readonly string _CommentsAndJobs = "_CommentsAndJobs"; public readonly string _Flags = "_Flags"; - public readonly string _Jobs = "_Jobs"; public readonly string _Resources = "_Resources"; public readonly string _Subject = "_Subject"; + public readonly string Comments = "Comments"; } public readonly string _AssignmentHistory = "~/Views/User/UserParts/_AssignmentHistory.cshtml"; public readonly string _Authorization = "~/Views/User/UserParts/_Authorization.cshtml"; + public readonly string _CommentsAndJobs = "~/Views/User/UserParts/_CommentsAndJobs.cshtml"; public readonly string _Flags = "~/Views/User/UserParts/_Flags.cshtml"; - public readonly string _Jobs = "~/Views/User/UserParts/_Jobs.cshtml"; public readonly string _Resources = "~/Views/User/UserParts/_Resources.cshtml"; public readonly string _Subject = "~/Views/User/UserParts/_Subject.cshtml"; + public readonly string Comments = "~/Views/User/UserParts/Comments.cshtml"; } } } diff --git a/Disco.Web/Views/User/Show.cshtml b/Disco.Web/Views/User/Show.cshtml index 8ca0a7c0..58a25f07 100644 --- a/Disco.Web/Views/User/Show.cshtml +++ b/Disco.Web/Views/User/Show.cshtml @@ -4,8 +4,15 @@ Authorization.Require(Claims.User.Show); ViewBag.Title = Html.ToBreadcrumb("Users", MVC.User.Index(), string.Format("User: {0} ({1})", Model.User.DisplayName, Model.User.FriendlyId())); + + var requiresLive = Authorization.HasAny(Claims.User.ShowComments, Claims.User.ShowAttachments); + + if (requiresLive) + { + Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR"); + } } -
+
@if (Authorization.Has(Claims.User.ShowFlagAssignments)) {
@@ -87,9 +94,9 @@
    - @if (Authorization.Has(Claims.User.ShowJobs)) + @if (Authorization.HasAny(Claims.User.ShowComments, Claims.User.ShowJobs)) { - @Html.Partial(MVC.User.Views.UserParts._Jobs, Model) + @Html.Partial(MVC.User.Views.UserParts._CommentsAndJobs, Model) } @if (Authorization.Has(Claims.User.ShowAssignmentHistory)) { @@ -108,4 +115,74 @@ @Html.Partial(MVC.User.Views.UserParts._Authorization, Model) }
    + @if (requiresLive) + { + + }
    diff --git a/Disco.Web/Views/User/Show.generated.cs b/Disco.Web/Views/User/Show.generated.cs index e725355e..9ff035d9 100644 --- a/Disco.Web/Views/User/Show.generated.cs +++ b/Disco.Web/Views/User/Show.generated.cs @@ -56,6 +56,13 @@ namespace Disco.Web.Views.User ViewBag.Title = Html.ToBreadcrumb("Users", MVC.User.Index(), string.Format("User: {0} ({1})", Model.User.DisplayName, Model.User.FriendlyId())); + var requiresLive = Authorization.HasAny(Claims.User.ShowComments, Claims.User.ShowAttachments); + + if (requiresLive) + { + Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR"); + } + #line default #line hidden @@ -63,16 +70,27 @@ WriteLiteral("\r\n
    \r\n
    \r\n"); +WriteLiteral(" \r\n"); + + + #line 118 "..\..\Views\User\Show.cshtml" + + + #line default + #line hidden + + #line 118 "..\..\Views\User\Show.cshtml" + if (requiresLive) + { + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + #line 187 "..\..\Views\User\Show.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n"); } } diff --git a/Disco.Web/Views/User/UserParts/Comments.cshtml b/Disco.Web/Views/User/UserParts/Comments.cshtml new file mode 100644 index 00000000..bb06816c --- /dev/null +++ b/Disco.Web/Views/User/UserParts/Comments.cshtml @@ -0,0 +1,212 @@ +@model Disco.Web.Models.User.ShowModel +@{ + Authorization.Require(Claims.User.ShowComments); + var canAddComments = Authorization.Has(Claims.User.Actions.AddComments); + var canRemoveAnyComments = Authorization.Has(Claims.User.Actions.RemoveAnyComments); + var canRemoveOwnComments = Authorization.Has(Claims.User.Actions.RemoveOwnComments); +} +
    + @Html.AntiForgeryToken() + @if (canAddComments) + { +
    + + +
    + } +
    + @foreach (var c in Model.User.UserComments.OrderBy(m => m.Timestamp)) + { +
    + @c.TechUser.ToStringFriendly()@if (canRemoveAnyComments || (canRemoveOwnComments && c.TechUserId.Equals(CurrentUser.UserId, StringComparison.OrdinalIgnoreCase))) + {}@c.Timestamp.ToFullDateTime() +
    @c.Comments.ToHtmlComment()
    +
    + } +
    +
    + +@if (canAddComments) +{ + +} +@if (canRemoveAnyComments || canRemoveOwnComments) +{ + +} \ No newline at end of file diff --git a/Disco.Web/Views/User/UserParts/Comments.generated.cs b/Disco.Web/Views/User/UserParts/Comments.generated.cs new file mode 100644 index 00000000..95f37c3f --- /dev/null +++ b/Disco.Web/Views/User/UserParts/Comments.generated.cs @@ -0,0 +1,451 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.User.UserParts +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco; + using Disco.Models.Repository; + using Disco.Services; + using Disco.Services.Authorization; + using Disco.Services.Web; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "2.0.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/User/UserParts/Comments.cshtml")] + public partial class Comments : Disco.Services.Web.WebViewPage + { + public Comments() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\User\UserParts\Comments.cshtml" + + Authorization.Require(Claims.User.ShowComments); + var canAddComments = Authorization.Has(Claims.User.Actions.AddComments); + var canRemoveAnyComments = Authorization.Has(Claims.User.Actions.RemoveAnyComments); + var canRemoveOwnComments = Authorization.Has(Claims.User.Actions.RemoveOwnComments); + + + #line default + #line hidden +WriteLiteral("\r\n(canAddComments ? "canAddComments" : "cannotAddComments" + + #line default + #line hidden +, 385), false) + + #line 8 "..\..\Views\User\UserParts\Comments.cshtml" + , Tuple.Create(Tuple.Create(" ", 443), Tuple.Create(canRemoveAnyComments ? "canRemoveAnyComments" : "cannotRemoveAnyComments" + + #line default + #line hidden +, 444), false) + + #line 8 "..\..\Views\User\UserParts\Comments.cshtml" + , Tuple.Create(Tuple.Create(" ", 520), Tuple.Create(canRemoveOwnComments ? "canRemoveOwnComments" : "cannotRemoveOwnComments" + + #line default + #line hidden +, 521), false) +); + +WriteLiteral(" data-id=\""); + + + #line 8 "..\..\Views\User\UserParts\Comments.cshtml" + Write(Model.User.UserId); + + + #line default + #line hidden +WriteLiteral("\""); + +WriteLiteral(" data-userid=\""); + + + #line 8 "..\..\Views\User\UserParts\Comments.cshtml" + Write(CurrentUser.UserId); + + + #line default + #line hidden +WriteLiteral("\""); + +WriteLiteral(" data-addurl=\""); + + + #line 8 "..\..\Views\User\UserParts\Comments.cshtml" + Write(Url.Action(MVC.API.User.CommentAdd(Model.User.UserId))); + + + #line default + #line hidden +WriteLiteral("\""); + +WriteLiteral(" data-removeurl=\""); + + + #line 8 "..\..\Views\User\UserParts\Comments.cshtml" + Write(Url.Action(MVC.API.User.CommentRemove())); + + + #line default + #line hidden +WriteLiteral("\""); + +WriteLiteral(" data-geturl=\""); + + + #line 8 "..\..\Views\User\UserParts\Comments.cshtml" + Write(Url.Action(MVC.API.User.Comment())); + + + #line default + #line hidden +WriteLiteral("\""); + +WriteLiteral(">\r\n"); + +WriteLiteral(" "); + + + #line 9 "..\..\Views\User\UserParts\Comments.cshtml" +Write(Html.AntiForgeryToken()); + + + #line default + #line hidden +WriteLiteral("\r\n"); + + + #line 10 "..\..\Views\User\UserParts\Comments.cshtml" + + + #line default + #line hidden + + #line 10 "..\..\Views\User\UserParts\Comments.cshtml" + if (canAddComments) + { + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n \r\n \r\n"); + + + #line 16 "..\..\Views\User\UserParts\Comments.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + #line 18 "..\..\Views\User\UserParts\Comments.cshtml" + + + #line default + #line hidden + + #line 18 "..\..\Views\User\UserParts\Comments.cshtml" + foreach (var c in Model.User.UserComments.OrderBy(m => m.Timestamp)) + { + + + #line default + #line hidden +WriteLiteral("