diff --git a/.vscode/settings.json b/.vscode/settings.json index 4148bb6..ea23849 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,7 @@ "Kofax", "Linc", "mesfs", + "mestsa", "NpgSql", "NSFX", "OBJE", diff --git a/ADO2024/PI4/Helper-2024-11-08.cs b/ADO2024/PI4/Helper-2024-11-08.cs index 1e349b4..5707352 100644 --- a/ADO2024/PI4/Helper-2024-11-08.cs +++ b/ADO2024/PI4/Helper-2024-11-08.cs @@ -1,13 +1,17 @@ using Microsoft.Extensions.Logging; +#if WorkItems using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +#endif namespace File_Folder_Helper.ADO2024.PI4; internal static partial class Helper20241108 { +#if WorkItems + private record Attribute([property: JsonPropertyName("isLocked")] bool IsLocked, [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("parameterTitle")] string? ParameterTitle, @@ -367,4 +371,14 @@ internal static partial class Helper20241108 } } +#else + + internal static void WriteMarkdown(ILogger logger, List args) + { + logger.LogError("WriteMarkdown is not available in WorkItems {args[0]}", args[0]); + logger.LogError("WriteMarkdown is not available in WorkItems {args[1]}", args[1]); + } + +#endif + } \ No newline at end of file diff --git a/ADO2025/PI4/Helper-2025-02-04.cs b/ADO2025/PI4/Helper-2025-02-04.cs new file mode 100644 index 0000000..025fc76 --- /dev/null +++ b/ADO2025/PI4/Helper-2025-02-04.cs @@ -0,0 +1,305 @@ +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace File_Folder_Helper.ADO2025.PI4; + +internal static partial class Helper20250204 +{ + + [GeneratedRegex("([A-Z]+(.))")] + private static partial Regex UpperCase(); + + [GeneratedRegex("[\\s!?.,@:;|\\\\/\"'`£$%\\^&*{}[\\]()<>~#+\\-=_¬]+")] + private static partial Regex InvalidCharacter(); + + private record H1ParamCaseAndState(string H1, string ParamCase, string State) + { + + private static string GetParamCase(string value) + { + string result; + StringBuilder stringBuilder = new(value); + Match[] matches = UpperCase().Matches(value).ToArray(); + for (int i = matches.Length - 1; i > -1; i--) + _ = stringBuilder.Insert(matches[i].Index, '-'); + string[] segments = InvalidCharacter().Split(stringBuilder.ToString().ToLower()); + result = string.Join('-', segments).Trim('-'); + return result; + } + + private static string GetState(string value) => + value switch + { + "New" => "ToDo", + "Active" => "In Progress", + "Closed" => "Done", + _ => "Backlog", + }; + + internal static H1ParamCaseAndState Get(WorkItem workItem) + { + H1ParamCaseAndState result; + string paramCase = GetParamCase(workItem.Title); + string state = GetState(workItem.State); + result = new(workItem.Title, paramCase, state); + return result; + } + + } + + private record Attribute([property: JsonPropertyName("isLocked")] bool IsLocked, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("parameterTitle")] string? ParameterTitle, + [property: JsonPropertyName("state")] string? State, + [property: JsonPropertyName("workItemType")] string? WorkItemType); + + private record Relation([property: JsonPropertyName("attributes")] Attribute Attributes, + [property: JsonPropertyName("id")] int Id, + [property: JsonPropertyName("rel")] string Rel); + + private record WorkItem(DateTime? ActivatedDate, + string AreaPath, + string? AssignedTo, + long? BusinessValue, + DateTime ChangedDate, + DateTime? ClosedDate, + int CommentCount, + DateTime CreatedDate, + string Description, + long? Effort, + int Id, + string IterationPath, + int? Parent, + int? Priority, + Relation[]? Relations, + string? Requester, + DateTime? ResolvedDate, + int Revision, + long? RiskReductionMinusOpportunityEnablement, + DateTime? StartDate, + string State, + string Tags, + DateTime? TargetDate, + long? TimeCriticality, + string Title, + string? Violation, + long? WeightedShortestJobFirst, + string WorkItemType) + { + + public override string ToString() => $"{Id} - {WorkItemType} - {Title}"; + + } + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(WorkItem))] + private partial class WorkItemSourceGenerationContext : JsonSerializerContext + { + } + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(WorkItem[]))] + private partial class WorkItemCollectionSourceGenerationContext : JsonSerializerContext + { + } + + private static string GetTaskText(string directory, string rootDirectory) => + string.Join(Environment.NewLine, + [ + "{", + "\"version\": \"2.0.0\",", + "\"tasks\": [", + "{", + "\"label\": \"File-Folder-Helper AOT s X Day-Helper-2025-02-04\",", + "\"type\": \"shell\",", + "\"command\": \"L:/DevOps/Mesa_FI/File-Folder-Helper/bin/Release/net8.0/win-x64/publish/File-Folder-Helper.exe\",", + "\"args\": [", + "\"s\",", + "\"X\",", + $"\"{directory}\",", + "\"Day-Helper-2025-02-04\",", + $"\"{rootDirectory}\",", + "],", + "\"problemMatcher\": []", + "},", + "{", + "\"label\": \"File-Folder-Helper AOT s X Day-Helper-2024-06-23\",", + "\"type\": \"shell\",", + "\"command\": \"L:/DevOps/Mesa_FI/File-Folder-Helper/bin/Release/net8.0/win-x64/publish/File-Folder-Helper.exe\",", + "\"args\": [", + "\"s\",", + "\"X\",", + $"\"{directory}\",", + "\"Day-Helper-2024-06-23\",", + "\"*.md\",", + "\"##_Sub-tasks\",", + "\"-_[code-insiders](\",", + "\"index.md\",", + "\"-_[,](\",", + "\"##_Done\",", + "\".kan\",", + $"\"{rootDirectory}\",", + "\"316940400000\",", + "],", + "\"problemMatcher\": []", + "}", + "]", + "}", + ]); + + private static void WriteTaskFile(string sourceDirectory, string rootDirectory) + { + string tasksFile = Path.Combine(sourceDirectory, ".vscode", "tasks.json"); + string oldText = File.ReadAllText(tasksFile); + string jsonSafeDirectory = sourceDirectory.Replace('\\', '/'); + if (!oldText.Contains(jsonSafeDirectory)) + { + string text = GetTaskText(jsonSafeDirectory, rootDirectory); + File.WriteAllText(tasksFile, text); + } + } + + private static string GetFilter(ReadOnlyCollection collection, string filter) => + string.Join(Environment.NewLine, from l in collection where l.State == filter select $"- [{l.ParamCase}](tasks/{l.ParamCase}.md)"); + + private static string GetIndexText(WorkItem workItem, H1ParamCaseAndState h1ParamCaseAndState, ReadOnlyCollection collection) => + string.Join(Environment.NewLine, [ + "---", + "startedColumns:", + " - 'In Progress'", + "completedColumns:", + " - Done", + "---", + string.Empty, + $"# {workItem.Id} - {h1ParamCaseAndState.H1}", + string.Empty, + "## Backlog", + string.Empty, + GetFilter(collection, "Backlog"), + string.Empty, + "## Todo", + string.Empty, + GetFilter(collection, "ToDo"), + string.Empty, + "## In Progress", + string.Empty, + GetFilter(collection, "In Progress"), + string.Empty, + "## Done", + string.Empty, + GetFilter(collection, "Done"), + string.Empty + ]); + + private static string GetIndexMarkdown(FileInfo fileInfo, ReadOnlyCollection workItems) + { + string result; + H1ParamCaseAndState h1ParamCaseAndState; + List collection = []; + foreach (WorkItem w in workItems) + { + h1ParamCaseAndState = H1ParamCaseAndState.Get(w); + collection.Add(h1ParamCaseAndState); + } + string line = Environment.NewLine; + string json = File.ReadAllText(fileInfo.FullName); + WorkItem? workItem = JsonSerializer.Deserialize(json, WorkItemSourceGenerationContext.Default.WorkItem) ?? + throw new NullReferenceException(nameof(WorkItem)); + h1ParamCaseAndState = H1ParamCaseAndState.Get(workItem); + string text = GetIndexText(workItem, h1ParamCaseAndState, collection.AsReadOnly()); + result = text.Replace($"{line}{line}{line}{line}", $"{line}{line}").Replace("408m](tasks", "408M](tasks"); + return result; + } + + private static ReadOnlyCollection GetWorkItems(string[] files) + { + List results = []; + string json; + WorkItem? workItem; + foreach (string file in files) + { + json = File.ReadAllText(file); + workItem = JsonSerializer.Deserialize(json, WorkItemSourceGenerationContext.Default.WorkItem); + if (workItem is null) + continue; + results.Add(workItem); + } + return results.AsReadOnly(); + } + + private static void ExtractKanban(string searchPattern, string rootDirectory, DirectoryInfo kanbanDirectory, FileInfo fileInfo) + { + string checkFile; + string weekOfYear; + string workItemDirectory; + string line = Environment.NewLine; + H1ParamCaseAndState h1ParamCaseAndState; + Calendar calendar = new CultureInfo("en-US").Calendar; + string tasksDirectory = Path.Combine(kanbanDirectory.FullName, "tasks"); + if (!Directory.Exists(tasksDirectory)) + _ = Directory.CreateDirectory(tasksDirectory); + string[] files = Directory.GetFiles(tasksDirectory, searchPattern, SearchOption.TopDirectoryOnly); + ReadOnlyCollection workItems = GetWorkItems(files); + string markdown = GetIndexMarkdown(fileInfo, workItems); + string indexFile = Path.Combine(kanbanDirectory.FullName, "index.md"); + string markdownOld = File.Exists(indexFile) ? File.ReadAllText(indexFile) : string.Empty; + if (markdown != markdownOld) + File.WriteAllText(indexFile, markdown); + foreach (WorkItem workItem in workItems) + { + h1ParamCaseAndState = H1ParamCaseAndState.Get(workItem); + checkFile = Path.Combine(tasksDirectory, $"{h1ParamCaseAndState.ParamCase}.md"); + markdownOld = File.Exists(checkFile) ? File.ReadAllText(checkFile) : string.Empty; + if (markdownOld.Contains("](")) + continue; + weekOfYear = calendar.GetWeekOfYear(workItem.CreatedDate, CalendarWeekRule.FirstDay, DayOfWeek.Sunday).ToString("00"); + workItemDirectory = Path.GetFullPath(Path.Combine(rootDirectory, $"{workItem.CreatedDate:yyyy}", $"{workItem.CreatedDate:yyyy}_Week_{weekOfYear}", $"{workItem.Id}")); + markdown = $"# {h1ParamCaseAndState.H1}{line}{line}## Id {workItem.Id}{line}{line}## Code Insiders{line}{line}- [code-insiders]({workItemDirectory}){line}"; + if (markdown != markdownOld) + File.WriteAllText(checkFile, markdown); + } + } + + private static string GetSourceDirectory(string directory) + { + string? result = null; + DirectoryInfo directoryInfo; + string? checkDirectory = directory; + string? pathRoot = Path.GetPathRoot(directory); + for (int i = 0; i < int.MaxValue; i++) + { + checkDirectory = Path.GetDirectoryName(checkDirectory); + if (string.IsNullOrEmpty(checkDirectory) || checkDirectory == pathRoot) + break; + directoryInfo = new(checkDirectory); + if (string.IsNullOrEmpty(directoryInfo.LinkTarget)) + continue; + result = directory.Replace(checkDirectory, directoryInfo.LinkTarget); + break; + } + result ??= directory; + return result; + } + + internal static void ExtractKanban(ILogger logger, List args) + { + string searchPattern = "*.json"; + string fullPath = Path.GetFullPath(args[0]); + string sourceDirectory = GetSourceDirectory(fullPath); + string rootDirectory = args.Count < 3 || args[2].Length < 16 ? "D:/5-Other-Small/Kanban-mestsa003/{}" : args[2]; + WriteTaskFile(sourceDirectory, rootDirectory); + string sourceDirectoryName = Path.GetFileName(sourceDirectory); + DirectoryInfo directoryInfo = new(Path.Combine(sourceDirectory, ".kanbn")); + FileInfo? fileInfo = !directoryInfo.Exists ? null : new(Path.Combine(directoryInfo.FullName, $"{sourceDirectoryName}.json")); + if (directoryInfo.Exists && fileInfo is not null && fileInfo.Exists) + ExtractKanban(searchPattern, rootDirectory, directoryInfo, fileInfo); + else + logger.LogWarning("<{directoryInfo}> doesn't exist", directoryInfo.FullName); + } + +} \ No newline at end of file diff --git a/Day/HelperDay.cs b/Day/HelperDay.cs index 8681ae9..c874904 100644 --- a/Day/HelperDay.cs +++ b/Day/HelperDay.cs @@ -131,6 +131,8 @@ internal static class HelperDay ADO2025.PI4.Helper20250114.Rename(logger, args); else if (args[1] == "Day-Helper-2025-01-26") ADO2025.PI4.Helper20250126.Move(logger, args); + else if (args[1] == "Day-Helper-2025-02-04") + ADO2025.PI4.Helper20250204.ExtractKanban(logger, args); else throw new Exception(appSettings.Company); }