diff --git a/AzureDevopsTracker/Adapters/WorkItemAdapter.cs b/AzureDevopsTracker/Adapters/WorkItemAdapter.cs index 6ea1a4b..8b8492b 100644 --- a/AzureDevopsTracker/Adapters/WorkItemAdapter.cs +++ b/AzureDevopsTracker/Adapters/WorkItemAdapter.cs @@ -12,7 +12,7 @@ internal class WorkItemAdapter : IWorkItemAdapter { public WorkItemDTO ToWorkItemDTO(WorkItem workItem) { - if (workItem == null) return null; + if (workItem is null) return null; return new WorkItemDTO() { @@ -31,7 +31,7 @@ public WorkItemDTO ToWorkItemDTO(WorkItem workItem) OriginalEstimate = workItem.OriginalEstimate, WorkItemParentId = workItem.WorkItemParentId, Activity = workItem.Activity, - Tags = workItem.Tags == null ? new List() : workItem.Tags.Split(';').ToList(), + Tags = workItem.Tags is null ? new List() : workItem.Tags.Split(';').ToList(), WorkItemsChangesDTO = ToWorkItemsChangeDTO(workItem.WorkItemsChanges.OrderBy(x => x.CreatedAt).ToList()), TimesByStateDTO = ToTimeByStatesDTO(workItem.CalculateTotalTimeByState().ToList()), }; @@ -41,7 +41,7 @@ public List ToWorkItemsDTO(List workItems) { var workItemsDTO = new List(); - if (workItems == null) return workItemsDTO; + if (workItems is null) return workItemsDTO; workItems.ForEach( workItem => @@ -53,7 +53,7 @@ public List ToWorkItemsDTO(List workItems) public WorkItemChangeDTO ToWorkItemChangeDTO(WorkItemChange workIteChange) { - if (workIteChange == null) return null; + if (workIteChange is null) return null; return new WorkItemChangeDTO() { @@ -69,20 +69,20 @@ public List ToWorkItemsChangeDTO(List workIte { var workItemsChangeDTO = new List(); - if (workItemsChanges == null) return workItemsChangeDTO; + if (workItemsChanges is null) return workItemsChangeDTO; workItemsChanges.ForEach( workItemsChange => workItemsChangeDTO.Add(ToWorkItemChangeDTO(workItemsChange))); return workItemsChangeDTO - .Where(w => w != null) + .Where(w => w is not null) .ToList(); } public TimeByStateDTO ToTimeByStateDTO(TimeByState workItemStatusTime) { - if (workItemStatusTime == null) return null; + if (workItemStatusTime is null) return null; return new TimeByStateDTO() { @@ -97,14 +97,14 @@ public List ToTimeByStatesDTO(List workItemStatusTi { var workItemStatusTimeDTO = new List(); - if (workItemStatusTimes == null) return workItemStatusTimeDTO; + if (workItemStatusTimes is null) return workItemStatusTimeDTO; workItemStatusTimes.ForEach( workItemStatusTime => workItemStatusTimeDTO.Add(ToTimeByStateDTO(workItemStatusTime))); return workItemStatusTimeDTO - .Where(w => w != null) + .Where(w => w is not null) .ToList(); } } diff --git a/AzureDevopsTracker/AzureDevopsTracker.csproj b/AzureDevopsTracker/AzureDevopsTracker.csproj index 28df536..7af5ce6 100644 --- a/AzureDevopsTracker/AzureDevopsTracker.csproj +++ b/AzureDevopsTracker/AzureDevopsTracker.csproj @@ -18,14 +18,15 @@ LICENSE.md.txt A NuGet that allows you to use a Azure DevOps Service Hook to track workitems changes in a simply and detailed way. 5.0.0.0 - 5.0.3 - 5.0.3 + 5.0.4 + 5.0.4 Add ChangeLog Feature + - + diff --git a/AzureDevopsTracker/Configurations/Configuration.cs b/AzureDevopsTracker/Configurations/Configuration.cs index 04999e8..c9adcd0 100644 --- a/AzureDevopsTracker/Configurations/Configuration.cs +++ b/AzureDevopsTracker/Configurations/Configuration.cs @@ -6,6 +6,7 @@ using AzureDevopsTracker.Interfaces.Internals; using AzureDevopsTracker.Services; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; @@ -21,6 +22,8 @@ public static IServiceCollection AddAzureDevopsTracker(this IServiceCollection s services.AddMessageIntegrations(); + services.AddSingleton(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -44,7 +47,7 @@ private static IServiceCollection AddMessageIntegrations(this IServiceCollection case EMessengers.MICROSOFT_TEAMS: services.AddScoped(); break; - default: + default: services.AddScoped(); break; } diff --git a/AzureDevopsTracker/DTOs/Fields.cs b/AzureDevopsTracker/DTOs/Fields.cs index 6d24cb6..aef4bd1 100644 --- a/AzureDevopsTracker/DTOs/Fields.cs +++ b/AzureDevopsTracker/DTOs/Fields.cs @@ -74,9 +74,6 @@ public class Fields [JsonProperty("Microsoft.VSTS.Common.Activity")] public string Activity { get; set; } - [JsonProperty("Custom.01f51eeb-416d-49b1-b7f9-b92f3a675de1")] - public string Lancado { get; set; } - [JsonProperty("Custom.ChangeLogDescription")] public string ChangeLogDescription { get; set; } } diff --git a/AzureDevopsTracker/Data/Context/AzureDevopsTrackerContext.cs b/AzureDevopsTracker/Data/Context/AzureDevopsTrackerContext.cs index 74f73ee..85de0e9 100644 --- a/AzureDevopsTracker/Data/Context/AzureDevopsTrackerContext.cs +++ b/AzureDevopsTracker/Data/Context/AzureDevopsTrackerContext.cs @@ -15,6 +15,7 @@ public AzureDevopsTrackerContext(DbContextOptions options) : base(options) public DbSet TimeByStates { get; set; } public DbSet ChangeLogItems { get; set; } public DbSet ChangeLogs { get; set; } + public DbSet CustomFields { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/AzureDevopsTracker/Data/DataBaseConfig.cs b/AzureDevopsTracker/Data/DataBaseConfig.cs index 47bd959..611f02f 100644 --- a/AzureDevopsTracker/Data/DataBaseConfig.cs +++ b/AzureDevopsTracker/Data/DataBaseConfig.cs @@ -11,7 +11,7 @@ public DataBaseConfig(string connectionsString, string schemaName = "dbo", TimeZ if (connectionsString.IsNullOrEmpty()) throw new ArgumentException("The ConnectionsString is required"); - if (timeZoneInfo == null) + if (timeZoneInfo is null) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time"); else diff --git a/AzureDevopsTracker/Data/Mapping/ChangeLogMapping.cs b/AzureDevopsTracker/Data/Mapping/ChangeLogMapping.cs index 6f2f973..cf0b496 100644 --- a/AzureDevopsTracker/Data/Mapping/ChangeLogMapping.cs +++ b/AzureDevopsTracker/Data/Mapping/ChangeLogMapping.cs @@ -12,4 +12,4 @@ public void Configure(EntityTypeBuilder builder) .HasColumnType("varchar(max)"); } } -} +} \ No newline at end of file diff --git a/AzureDevopsTracker/Data/Mapping/CustomFieldMapping.cs b/AzureDevopsTracker/Data/Mapping/CustomFieldMapping.cs new file mode 100644 index 0000000..bfa4432 --- /dev/null +++ b/AzureDevopsTracker/Data/Mapping/CustomFieldMapping.cs @@ -0,0 +1,20 @@ +using AzureDevopsTracker.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AzureDevopsTracker.Data.Mapping +{ + public class CustomFieldMapping : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(k => new { k.WorkItemId, k.Key }); + + builder.Property(p => p.Key) + .HasColumnType("varchar(1000)"); + + builder.Property(p => p.Value) + .HasColumnType("varchar(max)"); + } + } +} \ No newline at end of file diff --git a/AzureDevopsTracker/Data/WorkItemRepository.cs b/AzureDevopsTracker/Data/WorkItemRepository.cs index 3e7bb25..68a43f3 100644 --- a/AzureDevopsTracker/Data/WorkItemRepository.cs +++ b/AzureDevopsTracker/Data/WorkItemRepository.cs @@ -18,6 +18,7 @@ public async Task GetByWorkItemId(string workItemId) .Include(x => x.WorkItemsChanges) .Include(x => x.TimeByStates) .Include(x => x.ChangeLogItem) + .Include(x => x.CustomFields) .FirstOrDefaultAsync(x => x.Id == workItemId); } @@ -27,6 +28,7 @@ public async Task> ListByWorkItemId(IEnumerable wo .Include(x => x.WorkItemsChanges) .Include(x => x.TimeByStates) .Include(x => x.ChangeLogItem) + .Include(x => x.CustomFields) .Where(x => workItemsId.Contains(x.Id)) .ToListAsync(); } @@ -37,6 +39,7 @@ public async Task> ListByIterationPath(string iterationPat .Include(x => x.WorkItemsChanges) .Include(x => x.TimeByStates) .Include(x => x.ChangeLogItem) + .Include(x => x.CustomFields) .Where(x => x.IterationPath == iterationPath) .ToListAsync(); } diff --git a/AzureDevopsTracker/Entities/ChangeLog.cs b/AzureDevopsTracker/Entities/ChangeLog.cs index 17aabac..f8c2109 100644 --- a/AzureDevopsTracker/Entities/ChangeLog.cs +++ b/AzureDevopsTracker/Entities/ChangeLog.cs @@ -37,7 +37,7 @@ public void ClearResponse() public void AddChangeLogItem(ChangeLogItem changeLogItem) { - if (changeLogItem == null) + if (changeLogItem is null) throw new Exception("ChangeLogItem is required"); if (CheckChangeLogItem(changeLogItem)) @@ -49,7 +49,7 @@ public void AddChangeLogItem(ChangeLogItem changeLogItem) public void AddChangeLogItems(IEnumerable changeLogItems) { - if (changeLogItems == null) + if (changeLogItems is null) throw new Exception("ChangeLogItems is required"); foreach (var changeLogItem in changeLogItems) diff --git a/AzureDevopsTracker/Entities/Entity.cs b/AzureDevopsTracker/Entities/Entity.cs index 21e5f98..46f0a70 100644 --- a/AzureDevopsTracker/Entities/Entity.cs +++ b/AzureDevopsTracker/Entities/Entity.cs @@ -8,7 +8,7 @@ public abstract class Entity public DateTime CreatedAt { get; private set; } public Entity(string id) { - Id = id; + Id = id ?? Guid.NewGuid().ToString(); CreatedAt = DateTime.UtcNow; } diff --git a/AzureDevopsTracker/Entities/WorkItem.cs b/AzureDevopsTracker/Entities/WorkItem.cs index 35c1b98..cacdda3 100644 --- a/AzureDevopsTracker/Entities/WorkItem.cs +++ b/AzureDevopsTracker/Entities/WorkItem.cs @@ -20,29 +20,25 @@ public class WorkItem : Entity public string StoryPoints { get; private set; } public string WorkItemParentId { get; private set; } public string Activity { get; private set; } - public string Lancado { get; private set; } public bool Deleted { get; private set; } public ChangeLogItem ChangeLogItem { get; private set; } - private readonly List _workItemsChanges; + private readonly List _workItemsChanges = new(); public IReadOnlyCollection WorkItemsChanges => _workItemsChanges; - private readonly List _timeByState; + private readonly List _timeByState = new(); public IReadOnlyCollection TimeByStates => _timeByState; + + private readonly List _workItemCustomFields = new(); + public IReadOnlyCollection CustomFields => _workItemCustomFields; public string CurrentStatus => _workItemsChanges?.OrderBy(x => x.CreatedAt)?.LastOrDefault()?.NewState; public string LastStatus => _workItemsChanges?.OrderBy(x => x.CreatedAt)?.ToList()?.Skip(1)?.LastOrDefault()?.OldState; - private WorkItem() - { - _workItemsChanges = new List(); - _timeByState = new List(); - } + private WorkItem() { } public WorkItem(string workItemId) : base(workItemId) { - _workItemsChanges = new List(); - _timeByState = new List(); Validate(); } @@ -55,8 +51,7 @@ public void Update(string title, string effort, string storyPoint, string originalEstimate, - string activity, - string lancado) + string activity) { TeamProject = teamProject; AreaPath = areaPath; @@ -71,7 +66,6 @@ public void Update(string title, StoryPoints = storyPoint; OriginalEstimate = originalEstimate; Activity = activity; - Lancado = lancado; } public void Restore() @@ -87,34 +81,60 @@ public void Delete() public void Validate() { if (Id.IsNullOrEmpty()) - throw new Exception("WorkItemId is required"); + throw new ArgumentException("WorkItemId is required"); } public void AddWorkItemChange(WorkItemChange workItemChange) { - if (workItemChange == null) - throw new Exception("WorkItemChange is null"); + if (workItemChange is null) + throw new ArgumentException("WorkItemChange is null"); _workItemsChanges.Add(workItemChange); } public void AddTimeByState(TimeByState timeByState) { - if (timeByState == null) - throw new Exception("TimeByState is null"); + if (timeByState is null) + throw new ArgumentException("TimeByState is null"); _timeByState.Add(timeByState); } public void AddTimesByState(IEnumerable timesByState) { - if (!timesByState.Any()) + if (timesByState is not null && !timesByState.Any()) return; foreach (var timeByState in timesByState) AddTimeByState(timeByState); } + public void AddCustomField(WorkItemCustomField customField) + { + if (customField is null) + throw new ArgumentException("CustomField is null"); + + _workItemCustomFields.Add(customField); + } + + public void AddCustomFields(IEnumerable customFields) + { + if (customFields is null || !customFields.Any()) + return; + + foreach (var customField in customFields) + AddCustomField(customField); + } + + public void UpdateCustomFields(IEnumerable newCustomFields) + { + if (newCustomFields is null || !newCustomFields.Any()) + return; + + _workItemCustomFields.Clear(); + AddCustomFields(newCustomFields); + } + public void ClearTimesByState() { _timeByState.Clear(); @@ -127,8 +147,8 @@ public void RemoveChangeLogItem() public void VinculateChangeLogItem(ChangeLogItem changeLogItem) { - if (changeLogItem == null) - throw new Exception("ChangeLogItem is null"); + if (changeLogItem is null) + throw new ArgumentException("ChangeLogItem is null"); ChangeLogItem = changeLogItem; } @@ -139,7 +159,7 @@ public IEnumerable CalculateTotalTimeByState() if (!_workItemsChanges.Any()) return timesByStateList; - foreach (var workItemChange in _workItemsChanges.OrderBy(x => x.CreatedAt).GroupBy(x => x.OldState).Where(x => x.Key != null)) + foreach (var workItemChange in _workItemsChanges.OrderBy(x => x.CreatedAt).GroupBy(x => x.OldState).Where(x => x.Key is not null)) { var totalTime = TimeSpan.Zero; var totalWorkedTime = TimeSpan.Zero; diff --git a/AzureDevopsTracker/Entities/WorkItemChange.cs b/AzureDevopsTracker/Entities/WorkItemChange.cs index f00a73d..29a276e 100644 --- a/AzureDevopsTracker/Entities/WorkItemChange.cs +++ b/AzureDevopsTracker/Entities/WorkItemChange.cs @@ -40,7 +40,7 @@ public void Validate() public TimeSpan CalculateTotalTime() { - return OldDate == null ? TimeSpan.Zero : NewDate.ToDateTimeFromTimeZoneInfo() - OldDate.Value.ToDateTimeFromTimeZoneInfo(); + return OldDate is null ? TimeSpan.Zero : NewDate.ToDateTimeFromTimeZoneInfo() - OldDate.Value.ToDateTimeFromTimeZoneInfo(); } public double CalculateTotalWorkedTime() diff --git a/AzureDevopsTracker/Entities/WorkItemCustomField.cs b/AzureDevopsTracker/Entities/WorkItemCustomField.cs new file mode 100644 index 0000000..919a695 --- /dev/null +++ b/AzureDevopsTracker/Entities/WorkItemCustomField.cs @@ -0,0 +1,26 @@ +using AzureDevopsTracker.Extensions; +using System; + +namespace AzureDevopsTracker.Entities +{ + public class WorkItemCustomField + { + public string WorkItemId { get; private set; } + public string Key { get; private set; } + public string Value { get; private set; } + + public WorkItem WorkItem { get; private set; } + + public WorkItemCustomField(string workItemId, string key, string value) + { + WorkItemId = workItemId; + Key = key.Truncate(1000); + Value = value.Truncate(); + } + + public void Update(string value) + { + Value = value; + } + } +} \ No newline at end of file diff --git a/AzureDevopsTracker/Extensions/HelperExtenions.cs b/AzureDevopsTracker/Extensions/HelperExtenions.cs index 372e2dd..f4a5859 100644 --- a/AzureDevopsTracker/Extensions/HelperExtenions.cs +++ b/AzureDevopsTracker/Extensions/HelperExtenions.cs @@ -11,6 +11,12 @@ public static bool IsNullOrEmpty(this string text) return string.IsNullOrEmpty(text?.Trim()); } + public static string Truncate(this string value, int maxLength = 8000) + { + if (value.IsNullOrEmpty()) return value; + return value.Length <= maxLength ? value : value[..maxLength]; + } + public static string ExtractEmail(this string user) { if (user is null) diff --git a/AzureDevopsTracker/Helpers/ReadJsonHelper.cs b/AzureDevopsTracker/Helpers/ReadJsonHelper.cs new file mode 100644 index 0000000..1c00613 --- /dev/null +++ b/AzureDevopsTracker/Helpers/ReadJsonHelper.cs @@ -0,0 +1,46 @@ +using AzureDevopsTracker.Entities; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Linq; + +namespace AzureDevopsTracker.Helpers +{ + public static class ReadJsonHelper + { + public static IEnumerable ReadJson(string workItemId, string jsonTexto) + { + try + { + var workItemCustomFields = new List(); + foreach (KeyValuePair element in JObject.Parse(jsonTexto)) + { + if (element.Value is JObject) + ReadJsonObject(workItemId, workItemCustomFields, (JObject)element.Value); + else + GetWorkItemCustomField(workItemCustomFields, workItemId, element.Key, element.Value.ToString()); + } + + return workItemCustomFields; + } + catch + { + return Enumerable.Empty(); + } + } + + private static void ReadJsonObject(string workItemId, List workItemCustomFields, JObject objeto) + { + foreach (KeyValuePair item in objeto) + if (item.Value is JObject) + ReadJsonObject(workItemId, workItemCustomFields, (JObject)item.Value); + else + GetWorkItemCustomField(workItemCustomFields, workItemId, item.Key, item.Value.ToString()); + } + + private static void GetWorkItemCustomField(List workItemCustomFields, string workItemId, string key, string value) + { + if (key is not null && !key.ToLower().Contains("custom")) return; + workItemCustomFields.Add(new WorkItemCustomField(workItemId, key, value)); + } + } +} diff --git a/AzureDevopsTracker/Integrations/MessageFacade.cs b/AzureDevopsTracker/Integrations/MessageFacade.cs index 49e178e..b41e5b8 100644 --- a/AzureDevopsTracker/Integrations/MessageFacade.cs +++ b/AzureDevopsTracker/Integrations/MessageFacade.cs @@ -19,7 +19,7 @@ public void Send(ChangeLog changeLog) using var scope = _serviceScopeFactory.CreateScope(); var messageIntegration = scope.ServiceProvider.GetService(); - if (messageIntegration == null) throw new Exception("Configure the MessageConfig in Startup to send changelog messages"); + if (messageIntegration is null) throw new Exception("Configure the MessageConfig in Startup to send changelog messages"); messageIntegration.Send(changeLog); } diff --git a/AzureDevopsTracker/Integrations/MicrosoftTeamsIntegration.cs b/AzureDevopsTracker/Integrations/MicrosoftTeamsIntegration.cs index 6ec7eaf..ff9fd42 100644 --- a/AzureDevopsTracker/Integrations/MicrosoftTeamsIntegration.cs +++ b/AzureDevopsTracker/Integrations/MicrosoftTeamsIntegration.cs @@ -64,7 +64,7 @@ internal override void Send(ChangeLog changeLog) private string GetText(ChangeLog changeLog) { - if (changeLog == null || !changeLog.ChangeLogItems.Any()) return string.Empty; + if (changeLog is null || !changeLog.ChangeLogItems.Any()) return string.Empty; StringBuilder text = new(); text.AppendLine(GetWorkItemsDescriptionSection("Features", changeLog.ChangeLogItems.Where(x => x.WorkItemType != WorkItemStatics.WORKITEM_TYPE_BUG))); diff --git a/AzureDevopsTracker/Migrations/20220621170433_CustomFields.Designer.cs b/AzureDevopsTracker/Migrations/20220621170433_CustomFields.Designer.cs new file mode 100644 index 0000000..97ab753 --- /dev/null +++ b/AzureDevopsTracker/Migrations/20220621170433_CustomFields.Designer.cs @@ -0,0 +1,279 @@ +// +using System; +using AzureDevopsTracker.Data; +using AzureDevopsTracker.Data.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace AzureDevopsTracker.Migrations +{ + [DbContext(typeof(AzureDevopsTrackerContext))] + [Migration("20220621170433_CustomFields")] + partial class CustomFields + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema(DataBaseConfig.SchemaName) + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.10") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("AzureDevopsTracker.Entities.ChangeLog", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Number") + .HasColumnType("varchar(200)"); + + b.Property("Response") + .HasColumnType("varchar(max)"); + + b.Property("Revision") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("ChangeLogs"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.ChangeLogItem", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("ChangeLogId") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("varchar(max)"); + + b.Property("Title") + .HasColumnType("varchar(200)"); + + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.Property("WorkItemType") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("ChangeLogId"); + + b.HasIndex("WorkItemId") + .IsUnique() + .HasFilter("[WorkItemId] IS NOT NULL"); + + b.ToTable("ChangeLogItems"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.TimeByState", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("State") + .HasColumnType("varchar(200)"); + + b.Property("TotalTime") + .HasColumnType("float"); + + b.Property("TotalWorkedTime") + .HasColumnType("float"); + + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("WorkItemId"); + + b.ToTable("TimeByStates"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.WorkItem", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("Activity") + .HasColumnType("varchar(200)"); + + b.Property("AreaPath") + .HasColumnType("varchar(200)"); + + b.Property("AssignedTo") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("varchar(200)"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Effort") + .HasColumnType("varchar(200)"); + + b.Property("IterationPath") + .HasColumnType("varchar(200)"); + + b.Property("OriginalEstimate") + .HasColumnType("varchar(200)"); + + b.Property("StoryPoints") + .HasColumnType("varchar(200)"); + + b.Property("Tags") + .HasColumnType("varchar(200)"); + + b.Property("TeamProject") + .HasColumnType("varchar(200)"); + + b.Property("Title") + .HasColumnType("varchar(200)"); + + b.Property("Type") + .HasColumnType("varchar(200)"); + + b.Property("WorkItemParentId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("WorkItems"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.WorkItemChange", b => + { + b.Property("Id") + .HasColumnType("varchar(200)"); + + b.Property("ChangedBy") + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IterationPath") + .HasColumnType("varchar(200)"); + + b.Property("NewDate") + .HasColumnType("datetime2"); + + b.Property("NewState") + .HasColumnType("varchar(200)"); + + b.Property("OldDate") + .HasColumnType("datetime2"); + + b.Property("OldState") + .HasColumnType("varchar(200)"); + + b.Property("TotalWorkedTime") + .HasColumnType("float"); + + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("WorkItemId"); + + b.ToTable("WorkItemsChange"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.WorkItemCustomField", b => + { + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.Property("Key") + .HasColumnType("varchar(1000)"); + + b.Property("Value") + .HasColumnType("varchar(max)"); + + b.HasKey("WorkItemId", "Key"); + + b.ToTable("CustomFields"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.ChangeLogItem", b => + { + b.HasOne("AzureDevopsTracker.Entities.ChangeLog", "ChangeLog") + .WithMany("ChangeLogItems") + .HasForeignKey("ChangeLogId"); + + b.HasOne("AzureDevopsTracker.Entities.WorkItem", null) + .WithOne("ChangeLogItem") + .HasForeignKey("AzureDevopsTracker.Entities.ChangeLogItem", "WorkItemId"); + + b.Navigation("ChangeLog"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.TimeByState", b => + { + b.HasOne("AzureDevopsTracker.Entities.WorkItem", "WorkItem") + .WithMany("TimeByStates") + .HasForeignKey("WorkItemId"); + + b.Navigation("WorkItem"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.WorkItemChange", b => + { + b.HasOne("AzureDevopsTracker.Entities.WorkItem", "WorkItem") + .WithMany("WorkItemsChanges") + .HasForeignKey("WorkItemId"); + + b.Navigation("WorkItem"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.WorkItemCustomField", b => + { + b.HasOne("AzureDevopsTracker.Entities.WorkItem", "WorkItem") + .WithMany("CustomFields") + .HasForeignKey("WorkItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WorkItem"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.ChangeLog", b => + { + b.Navigation("ChangeLogItems"); + }); + + modelBuilder.Entity("AzureDevopsTracker.Entities.WorkItem", b => + { + b.Navigation("ChangeLogItem"); + + b.Navigation("CustomFields"); + + b.Navigation("TimeByStates"); + + b.Navigation("WorkItemsChanges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AzureDevopsTracker/Migrations/20220621170433_CustomFields.cs b/AzureDevopsTracker/Migrations/20220621170433_CustomFields.cs new file mode 100644 index 0000000..35b3674 --- /dev/null +++ b/AzureDevopsTracker/Migrations/20220621170433_CustomFields.cs @@ -0,0 +1,51 @@ +using AzureDevopsTracker.Data; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace AzureDevopsTracker.Migrations +{ + public partial class CustomFields : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Lancado", + schema: DataBaseConfig.SchemaName, + table: "WorkItems"); + + migrationBuilder.CreateTable( + name: "CustomFields", + schema: DataBaseConfig.SchemaName, + columns: table => new + { + WorkItemId = table.Column(type: "varchar(200)", nullable: false), + Key = table.Column(type: "varchar(1000)", nullable: false), + Value = table.Column(type: "varchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CustomFields", x => new { x.WorkItemId, x.Key }); + table.ForeignKey( + name: "FK_CustomFields_WorkItems_WorkItemId", + column: x => x.WorkItemId, + principalSchema: DataBaseConfig.SchemaName, + principalTable: "WorkItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CustomFields", + schema: DataBaseConfig.SchemaName); + + migrationBuilder.AddColumn( + name: "Lancado", + schema: DataBaseConfig.SchemaName, + table: "WorkItems", + type: "varchar(200)", + nullable: true); + } + } +} diff --git a/AzureDevopsTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs b/AzureDevopsTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs index 0978b18..e855a63 100644 --- a/AzureDevopsTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs +++ b/AzureDevopsTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs @@ -133,9 +133,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IterationPath") .HasColumnType("varchar(200)"); - b.Property("Lancado") - .HasColumnType("varchar(200)"); - b.Property("OriginalEstimate") .HasColumnType("varchar(200)"); @@ -201,6 +198,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("WorkItemsChange"); }); + modelBuilder.Entity("AzureDevopsTracker.Entities.WorkItemCustomField", b => + { + b.Property("WorkItemId") + .HasColumnType("varchar(200)"); + + b.Property("Key") + .HasColumnType("varchar(1000)"); + + b.Property("Value") + .HasColumnType("varchar(max)"); + + b.HasKey("WorkItemId", "Key"); + + b.ToTable("CustomFields"); + }); + modelBuilder.Entity("AzureDevopsTracker.Entities.ChangeLogItem", b => { b.HasOne("AzureDevopsTracker.Entities.ChangeLog", "ChangeLog") @@ -232,6 +245,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("WorkItem"); }); + modelBuilder.Entity("AzureDevopsTracker.Entities.WorkItemCustomField", b => + { + b.HasOne("AzureDevopsTracker.Entities.WorkItem", "WorkItem") + .WithMany("CustomFields") + .HasForeignKey("WorkItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WorkItem"); + }); + modelBuilder.Entity("AzureDevopsTracker.Entities.ChangeLog", b => { b.Navigation("ChangeLogItems"); @@ -241,6 +265,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("ChangeLogItem"); + b.Navigation("CustomFields"); + b.Navigation("TimeByStates"); b.Navigation("WorkItemsChanges"); diff --git a/AzureDevopsTracker/Services/AzureDevopsTrackerService.cs b/AzureDevopsTracker/Services/AzureDevopsTrackerService.cs index d52ead2..6aaf377 100644 --- a/AzureDevopsTracker/Services/AzureDevopsTrackerService.cs +++ b/AzureDevopsTracker/Services/AzureDevopsTrackerService.cs @@ -5,10 +5,14 @@ using AzureDevopsTracker.DTOs.Update; using AzureDevopsTracker.Entities; using AzureDevopsTracker.Extensions; +using AzureDevopsTracker.Helpers; using AzureDevopsTracker.Interfaces; using AzureDevopsTracker.Interfaces.Internals; +using Microsoft.AspNetCore.Http; using System; +using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; namespace AzureDevopsTracker.Services @@ -18,15 +22,18 @@ public class AzureDevopsTrackerService : IAzureDevopsTrackerService public readonly IWorkItemRepository _workItemRepository; public readonly IWorkItemAdapter _workItemAdapter; public readonly IChangeLogItemRepository _changeLogItemRepository; + private readonly IHttpContextAccessor _httpContextAccessor; public AzureDevopsTrackerService( IWorkItemAdapter workItemAdapter, IWorkItemRepository workItemRepository, - IChangeLogItemRepository changeLogItemRepository) + IChangeLogItemRepository changeLogItemRepository, + IHttpContextAccessor httpContextAccessor) { _workItemAdapter = workItemAdapter; _workItemRepository = workItemRepository; _changeLogItemRepository = changeLogItemRepository; + _httpContextAccessor = httpContextAccessor; } public async Task Create(CreateWorkItemDTO create, bool addWorkItemChange = true) @@ -45,14 +52,15 @@ public async Task Create(CreateWorkItemDTO create, bool addWorkItemChange = true create.Resource.Fields.Effort, create.Resource.Fields.StoryPoints, create.Resource.Fields.OriginalEstimate, - create.Resource.Fields.Activity, - create.Resource.Fields.Lancado); + create.Resource.Fields.Activity); if (addWorkItemChange) AddWorkItemChange(workItem, create); CheckWorkItemAvailableToChangeLog(workItem, create.Resource.Fields); + AddCustomFields(workItem); + await _workItemRepository.Add(workItem); await _workItemRepository.SaveChangesAsync(); } @@ -92,13 +100,14 @@ public async Task Update(UpdatedWorkItemDTO update) update.Resource.Revision.Fields.Effort, update.Resource.Revision.Fields.StoryPoints, update.Resource.Revision.Fields.OriginalEstimate, - update.Resource.Revision.Fields.Activity, - update.Resource.Revision.Fields.Lancado); + update.Resource.Revision.Fields.Activity); AddWorkItemChange(workItem, update); CheckWorkItemAvailableToChangeLog(workItem, update.Resource.Revision.Fields); + AddCustomFields(workItem); + _workItemRepository.Update(workItem); await _workItemRepository.SaveChangesAsync(); } @@ -126,8 +135,9 @@ public async Task Delete(DeleteWorkItemDTO delete) delete.Resource.Fields.Effort, delete.Resource.Fields.StoryPoints, delete.Resource.Fields.OriginalEstimate, - delete.Resource.Fields.Activity, - delete.Resource.Fields.Lancado); + delete.Resource.Fields.Activity); + + AddCustomFields(workItem); _workItemRepository.Update(workItem); await _workItemRepository.SaveChangesAsync(); @@ -156,8 +166,9 @@ public async Task Restore(RestoreWorkItemDTO restore) restore.Resource.Fields.Effort, restore.Resource.Fields.StoryPoints, restore.Resource.Fields.OriginalEstimate, - restore.Resource.Fields.Activity, - restore.Resource.Fields.Lancado); + restore.Resource.Fields.Activity); + + AddCustomFields(workItem); _workItemRepository.Update(workItem); await _workItemRepository.SaveChangesAsync(); @@ -173,6 +184,40 @@ public async Task GetByWorkItemId(string workItemId) } #region Support Methods + public void AddCustomFields(WorkItem workItem) + { + try + { + var jsonText = GetRequestBody(); + if (jsonText.IsNullOrEmpty()) + return; + + var customFields = ReadJsonHelper.ReadJson(workItem.Id, jsonText); + if (customFields is null || !customFields.Any()) + return; + + workItem.UpdateCustomFields(customFields); + } + catch + { } + } + + public string GetRequestBody() + { + string corpo; + var request = _httpContextAccessor?.HttpContext?.Request; + using (StreamReader reader = new(request.Body, + encoding: Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + leaveOpen: true)) + { + request.Body.Position = 0; + corpo = reader.ReadToEndAsync()?.Result; + } + + return corpo; + } + public WorkItemChange ToWorkItemChange( string workItemId, string changedBy, string iterationPath, DateTime newDate, string newState, @@ -231,7 +276,7 @@ public void CheckWorkItemAvailableToChangeLog(WorkItem workItem, Fields fields) { if (workItem.CurrentStatus != "Closed" && workItem.LastStatus == "Closed" && - workItem.ChangeLogItem != null && + workItem.ChangeLogItem is not null && !workItem.ChangeLogItem.WasReleased) RemoveChangeLogItem(workItem); @@ -239,7 +284,7 @@ public void CheckWorkItemAvailableToChangeLog(WorkItem workItem, Fields fields) fields.ChangeLogDescription.IsNullOrEmpty()) return; - if (workItem.ChangeLogItem == null) + if (workItem.ChangeLogItem is null) workItem.VinculateChangeLogItem(ToChangeLogItem(workItem, fields)); else workItem.ChangeLogItem.Update(workItem.Title, workItem.Type, fields.ChangeLogDescription); @@ -264,7 +309,7 @@ public ChangeLogItem ToChangeLogItem(WorkItem workItem, Fields fields) public void RemoveChangeLogItem(WorkItem workItem) { var changeLogItem = _changeLogItemRepository.GetById(workItem.ChangeLogItem?.Id).Result; - if (changeLogItem != null) + if (changeLogItem is not null) { _changeLogItemRepository.Delete(changeLogItem); _changeLogItemRepository.SaveChangesAsync().Wait();