From a6adaea2dac68c295517898c08ee7a6eca241a9b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 1 Mar 2026 22:37:29 -0500 Subject: [PATCH] feat(web): add Mappings page with inline CRUD table Co-Authored-By: Claude Opus 4.6 --- TaskTracker.Api/Pages/Mappings.cshtml | 48 ++++++++++ TaskTracker.Api/Pages/Mappings.cshtml.cs | 96 +++++++++++++++++++ .../Pages/Partials/_MappingEditRow.cshtml | 67 +++++++++++++ .../Pages/Partials/_MappingRow.cshtml | 41 ++++++++ TaskTracker.Api/wwwroot/css/site.css | 6 ++ 5 files changed, 258 insertions(+) create mode 100644 TaskTracker.Api/Pages/Mappings.cshtml create mode 100644 TaskTracker.Api/Pages/Mappings.cshtml.cs create mode 100644 TaskTracker.Api/Pages/Partials/_MappingEditRow.cshtml create mode 100644 TaskTracker.Api/Pages/Partials/_MappingRow.cshtml diff --git a/TaskTracker.Api/Pages/Mappings.cshtml b/TaskTracker.Api/Pages/Mappings.cshtml new file mode 100644 index 0000000..7ae32cb --- /dev/null +++ b/TaskTracker.Api/Pages/Mappings.cshtml @@ -0,0 +1,48 @@ +@page +@using TaskTracker.Api.Pages +@model MappingsModel + +
+
+

App Mappings

+ +
+ + @if (Model.Mappings.Count == 0) + { +
+

No mappings configured

+ +
+ } + +
+ + + + + + + + + + + + @foreach (var m in Model.Mappings) + { + + } + +
PatternMatch TypeCategoryFriendly NameActions
+
+
diff --git a/TaskTracker.Api/Pages/Mappings.cshtml.cs b/TaskTracker.Api/Pages/Mappings.cshtml.cs new file mode 100644 index 0000000..aef56ab --- /dev/null +++ b/TaskTracker.Api/Pages/Mappings.cshtml.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using TaskTracker.Core.Entities; +using TaskTracker.Core.Interfaces; + +namespace TaskTracker.Api.Pages; + +[IgnoreAntiforgeryToken] +public class MappingsModel : PageModel +{ + private readonly IAppMappingRepository _mappingRepo; + + public MappingsModel(IAppMappingRepository mappingRepo) => _mappingRepo = mappingRepo; + + public List Mappings { get; set; } = []; + + // Category colors (same as Board) + public static readonly Dictionary CategoryColors = new() + { + ["Development"] = "#6366f1", ["Research"] = "#06b6d4", ["Communication"] = "#8b5cf6", + ["DevOps"] = "#f97316", ["Documentation"] = "#14b8a6", ["Design"] = "#ec4899", + ["Testing"] = "#3b82f6", ["General"] = "#64748b", ["Email"] = "#f59e0b", + ["Engineering"] = "#6366f1", ["LaserCutting"] = "#ef4444", ["Unknown"] = "#475569", + }; + + public static readonly Dictionary MatchTypeColors = new() + { + ["ProcessName"] = "#6366f1", + ["TitleContains"] = "#06b6d4", + ["UrlContains"] = "#f97316", + }; + + public async Task OnGetAsync() + { + Mappings = await _mappingRepo.GetAllAsync(); + } + + // Return an empty edit row for adding new + public IActionResult OnGetAddRow() + { + return Partial("Partials/_MappingEditRow", new AppMapping()); + } + + // Return the edit form for an existing mapping + public async Task OnGetEditRowAsync(int id) + { + var mapping = await _mappingRepo.GetByIdAsync(id); + if (mapping is null) return NotFound(); + return Partial("Partials/_MappingEditRow", mapping); + } + + // Return the display row for an existing mapping (cancel edit) + public async Task OnGetRowAsync(int id) + { + var mapping = await _mappingRepo.GetByIdAsync(id); + if (mapping is null) return NotFound(); + return Partial("Partials/_MappingRow", mapping); + } + + // Create or update a mapping + public async Task OnPostSaveAsync(int? id, string pattern, string matchType, string category, string? friendlyName) + { + if (id.HasValue && id.Value > 0) + { + // Update existing + var mapping = await _mappingRepo.GetByIdAsync(id.Value); + if (mapping is null) return NotFound(); + mapping.Pattern = pattern; + mapping.MatchType = matchType; + mapping.Category = category; + mapping.FriendlyName = friendlyName; + await _mappingRepo.UpdateAsync(mapping); + return Partial("Partials/_MappingRow", mapping); + } + else + { + // Create new + var mapping = new AppMapping + { + Pattern = pattern, + MatchType = matchType, + Category = category, + FriendlyName = friendlyName, + }; + var created = await _mappingRepo.CreateAsync(mapping); + return Partial("Partials/_MappingRow", created); + } + } + + // Delete a mapping + public async Task OnDeleteAsync(int id) + { + await _mappingRepo.DeleteAsync(id); + return Content(""); // htmx will remove the row + } +} diff --git a/TaskTracker.Api/Pages/Partials/_MappingEditRow.cshtml b/TaskTracker.Api/Pages/Partials/_MappingEditRow.cshtml new file mode 100644 index 0000000..70c561f --- /dev/null +++ b/TaskTracker.Api/Pages/Partials/_MappingEditRow.cshtml @@ -0,0 +1,67 @@ +@using TaskTracker.Api.Pages +@model TaskTracker.Core.Entities.AppMapping +@{ + var isNew = Model.Id == 0; + var formId = isNew ? "mapping-form-new" : $"mapping-form-{Model.Id}"; + var rowId = isNew ? "mapping-row-new" : $"mapping-row-{Model.Id}"; +} + + + + + + + + + + + + + + +
+ + @if (isNew) + { + + } + else + { + + } +
+ + diff --git a/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml b/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml new file mode 100644 index 0000000..461ef58 --- /dev/null +++ b/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml @@ -0,0 +1,41 @@ +@using TaskTracker.Api.Pages +@model TaskTracker.Core.Entities.AppMapping +@{ + var matchColor = MappingsModel.MatchTypeColors.GetValueOrDefault(Model.MatchType, "#64748b"); + var catColor = MappingsModel.CategoryColors.GetValueOrDefault(Model.Category, "#64748b"); +} + + @Model.Pattern + + + @Model.MatchType + + + + + + @Model.Category + + + @(Model.FriendlyName ?? "\u2014") + +
+ + +
+ + diff --git a/TaskTracker.Api/wwwroot/css/site.css b/TaskTracker.Api/wwwroot/css/site.css index 18ad78e..25cfd94 100644 --- a/TaskTracker.Api/wwwroot/css/site.css +++ b/TaskTracker.Api/wwwroot/css/site.css @@ -884,6 +884,12 @@ body::before { } +/* ============================================================ + MAPPINGS PAGE + ============================================================ */ + +.mappings-page { max-width: 1152px; margin: 0 auto; } + /* ============================================================ MAPPINGS TABLE ============================================================ */