From d38cdb9eea7673fa1b0a19c002cacac3aef30e76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:06:56 +0000 Subject: [PATCH 1/4] Initial plan From 3f9a73e02aff4ed6979a6bede0106f47202e3621 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:22:45 +0000 Subject: [PATCH 2/4] Add pinned notes feature with database migration and UI Co-authored-by: danielchalmers <7112040+danielchalmers@users.noreply.github.com> --- JournalApp/Components/DataPointView.razor | 13 ++ JournalApp/Data/DataPoint.cs | 5 + ...2061630_AddIsPinnedToDataPoint.Designer.cs | 162 ++++++++++++++++++ .../20251102061630_AddIsPinnedToDataPoint.cs | 29 ++++ .../Migrations/AppDbContextModelSnapshot.cs | 3 + JournalApp/Pages/Index.razor | 31 ++++ JournalApp/Pages/PinnedNotesPage.razor | 93 ++++++++++ JournalApp/Pages/PinnedNotesPage.razor.css | 22 +++ 8 files changed, 358 insertions(+) create mode 100644 JournalApp/Migrations/20251102061630_AddIsPinnedToDataPoint.Designer.cs create mode 100644 JournalApp/Migrations/20251102061630_AddIsPinnedToDataPoint.cs create mode 100644 JournalApp/Pages/PinnedNotesPage.razor create mode 100644 JournalApp/Pages/PinnedNotesPage.razor.css diff --git a/JournalApp/Components/DataPointView.razor b/JournalApp/Components/DataPointView.razor index 5b8225df..588d309c 100644 --- a/JournalApp/Components/DataPointView.razor +++ b/JournalApp/Components/DataPointView.razor @@ -67,6 +67,13 @@ else if (Point.Type == PointType.Note) AutoGrow Lines="1" MaxLines="10" /> } + + public bool Deleted { get; set; } + /// + /// Indicates whether the note is pinned. + /// + public bool IsPinned { get; set; } + /// /// The mood value of the data point, if applicable. /// diff --git a/JournalApp/Migrations/20251102061630_AddIsPinnedToDataPoint.Designer.cs b/JournalApp/Migrations/20251102061630_AddIsPinnedToDataPoint.Designer.cs new file mode 100644 index 00000000..3ee36bbb --- /dev/null +++ b/JournalApp/Migrations/20251102061630_AddIsPinnedToDataPoint.Designer.cs @@ -0,0 +1,162 @@ +// +using System; +using JournalApp; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace JournalApp.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251102061630_AddIsPinnedToDataPoint")] + partial class AddIsPinnedToDataPoint + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("JournalApp.DataPoint", b => + { + b.Property("Guid") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bool") + .HasColumnType("INTEGER"); + + b.Property("CategoryGuid") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DayGuid") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("IsPinned") + .HasColumnType("INTEGER"); + + b.Property("MedicationDose") + .HasColumnType("TEXT"); + + b.Property("Mood") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("REAL"); + + b.Property("ScaleIndex") + .HasColumnType("INTEGER"); + + b.Property("SleepHours") + .HasColumnType("TEXT"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Guid"); + + b.HasIndex("CategoryGuid"); + + b.HasIndex("DayGuid"); + + b.ToTable("Points"); + }); + + modelBuilder.Entity("JournalApp.DataPointCategory", b => + { + b.Property("Guid") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Group") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MedicationDose") + .HasColumnType("TEXT"); + + b.Property("MedicationEveryDaySince") + .HasColumnType("TEXT"); + + b.Property("MedicationUnit") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Guid"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("JournalApp.Day", b => + { + b.Property("Guid") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.HasKey("Guid"); + + b.ToTable("Days"); + }); + + modelBuilder.Entity("JournalApp.DataPoint", b => + { + b.HasOne("JournalApp.DataPointCategory", "Category") + .WithMany("Points") + .HasForeignKey("CategoryGuid"); + + b.HasOne("JournalApp.Day", "Day") + .WithMany("Points") + .HasForeignKey("DayGuid"); + + b.Navigation("Category"); + + b.Navigation("Day"); + }); + + modelBuilder.Entity("JournalApp.DataPointCategory", b => + { + b.Navigation("Points"); + }); + + modelBuilder.Entity("JournalApp.Day", b => + { + b.Navigation("Points"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JournalApp/Migrations/20251102061630_AddIsPinnedToDataPoint.cs b/JournalApp/Migrations/20251102061630_AddIsPinnedToDataPoint.cs new file mode 100644 index 00000000..b125bbf8 --- /dev/null +++ b/JournalApp/Migrations/20251102061630_AddIsPinnedToDataPoint.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JournalApp.Migrations +{ + /// + public partial class AddIsPinnedToDataPoint : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsPinned", + table: "Points", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsPinned", + table: "Points"); + } + } +} diff --git a/JournalApp/Migrations/AppDbContextModelSnapshot.cs b/JournalApp/Migrations/AppDbContextModelSnapshot.cs index e78386e0..53b18f15 100644 --- a/JournalApp/Migrations/AppDbContextModelSnapshot.cs +++ b/JournalApp/Migrations/AppDbContextModelSnapshot.cs @@ -38,6 +38,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Deleted") .HasColumnType("INTEGER"); + b.Property("IsPinned") + .HasColumnType("INTEGER"); + b.Property("MedicationDose") .HasColumnType("TEXT"); diff --git a/JournalApp/Pages/Index.razor b/JournalApp/Pages/Index.razor index e37d5611..61e11401 100644 --- a/JournalApp/Pages/Index.razor +++ b/JournalApp/Pages/Index.razor @@ -18,6 +18,7 @@ + Pinned notes Elements Medications Trends @@ -126,6 +127,9 @@ [Parameter] public string OpenToDateString { get; set; } + [SupplyParameterFromQuery(Name = "scrollToNote")] + public string ScrollToNoteGuid { get; set; } + protected override async Task OnInitializedAsync() { logger.LogDebug("Initializing asynchronously"); @@ -149,6 +153,12 @@ logger.LogInformation($"Opening to {date}"); await GoToDay(date); + + // Scroll to the specific note if requested + if (!string.IsNullOrEmpty(ScrollToNoteGuid) && Guid.TryParse(ScrollToNoteGuid, out var noteGuid)) + { + await ScrollToNote(noteGuid); + } } protected override void OnWindowDeactivatedOrDestroying(object sender, EventArgs e) @@ -229,6 +239,12 @@ NavigationManager.NavigateTo($"/calendar/{_day.Date:yyyyMMdd}", false, true); } + void OpenPinnedNotes() + { + logger.LogInformation("Opening Pinned notes"); + NavigationManager.NavigateTo($"/pinned-notes", false, true); + } + void ManageCategories() { logger.LogInformation("Opening category manager"); @@ -292,6 +308,21 @@ await db.SaveChangesAsync(); } + async Task ScrollToNote(Guid noteGuid) + { + logger.LogInformation($"Scrolling to note {noteGuid}"); + + // Wait for the page to render + await Task.Delay(100); + + // Try to scroll to the note's container + var categoryGuid = _day.Points.FirstOrDefault(p => p.Guid == noteGuid)?.Category?.Guid; + if (categoryGuid.HasValue) + { + await JSRuntime.InvokeVoidAsync("scrollToTopOfNestedElement", "main", $"[data-category-guid='{categoryGuid.Value}']", "smooth"); + } + } + protected override void SaveState() { base.SaveState(); diff --git a/JournalApp/Pages/PinnedNotesPage.razor b/JournalApp/Pages/PinnedNotesPage.razor new file mode 100644 index 00000000..9e672487 --- /dev/null +++ b/JournalApp/Pages/PinnedNotesPage.razor @@ -0,0 +1,93 @@ +@namespace JournalApp +@page "/pinned-notes" +@inherits JaPage +@implements IDisposable +@inject ILogger logger +@inject IDbContextFactory DbFactory + + + +
+
+ @if (!_pinnedNotes.Any()) + { + + + No pinned notes yet. Pin a note to see it here. + + + } + else + { + @foreach (var note in _pinnedNotes) + { + + + @note.Day.Date.ToString("ddd, MMM d, yyyy") - @note.CreatedAt.ToLocalTime().ToString("h:mm tt") + @if (!string.IsNullOrWhiteSpace(note.Text)) + { + @note.Text + } + + + } + } +
+
+ +@code { + AppDbContext db; + List _pinnedNotes = new(); + + protected override async Task OnInitializedAsync() + { + logger.LogDebug("Initializing pinned notes page"); + db = await DbFactory.CreateDbContextAsync(); + await base.OnInitializedAsync(); + + await LoadPinnedNotes(); + } + + async Task LoadPinnedNotes() + { + logger.LogInformation("Loading pinned notes"); + _pinnedNotes = await db.Points + .Include(p => p.Day) + .Include(p => p.Category) + .Where(p => p.IsPinned && !p.Deleted && p.Type == PointType.Note) + .OrderBy(p => p.Day.Date) + .ThenBy(p => p.CreatedAt) + .ToListAsync(); + + logger.LogInformation($"Loaded {_pinnedNotes.Count} pinned notes"); + StateHasChanged(); + } + + void NavigateToNote(DataPoint note) + { + logger.LogInformation($"Navigating to note on {note.Day.Date}"); + + // Navigate to the day and use a query parameter to indicate which note to scroll to + var dateString = note.Day.Date.ToString("yyyyMMdd"); + NavigationManager.NavigateTo($"/{dateString}?scrollToNote={note.Guid}"); + } + + void Close() + { + logger.LogDebug("Closing pinned notes page"); + NavigationManager.NavigateTo("/"); + } + + protected override void Dispose(bool disposing) + { + logger.LogDebug("Disposing"); + base.Dispose(disposing); + + db?.Dispose(); + } +} diff --git a/JournalApp/Pages/PinnedNotesPage.razor.css b/JournalApp/Pages/PinnedNotesPage.razor.css new file mode 100644 index 00000000..b227b03a --- /dev/null +++ b/JournalApp/Pages/PinnedNotesPage.razor.css @@ -0,0 +1,22 @@ +.pinned-notes-list { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; +} + +.pinned-note-card { + cursor: pointer; + transition: transform 0.2s ease-in-out; +} + +.pinned-note-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.pinned-note-card .mud-card-content { + display: flex; + flex-direction: column; + gap: 0.5rem; +} From 770aa9bce8b46338cbf318d5068a40f44c6e7664 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:27:26 +0000 Subject: [PATCH 3/4] Add tests for pinned notes and fix SQLite ordering issue Co-authored-by: danielchalmers <7112040+danielchalmers@users.noreply.github.com> --- JournalApp.Tests/PinnedNotesTests.razor | 102 ++++++++++++++++++++++++ JournalApp/Pages/PinnedNotesPage.razor | 6 +- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 JournalApp.Tests/PinnedNotesTests.razor diff --git a/JournalApp.Tests/PinnedNotesTests.razor b/JournalApp.Tests/PinnedNotesTests.razor new file mode 100644 index 00000000..b74d9730 --- /dev/null +++ b/JournalApp.Tests/PinnedNotesTests.razor @@ -0,0 +1,102 @@ +@namespace JournalApp.Tests +@inherits JaTestContext + +@code { + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + AddDbContext(); + Services.GetService().SeedCategories(); + } + + [Fact] + public void CanPinAndUnpinNote() + { + // Arrange + var category = new DataPointCategory + { + Guid = Guid.NewGuid(), + Type = PointType.Note, + Group = "Notes", + }; + + var day = Day.Create(new(2024, 01, 01)); + var point = DataPoint.Create(day, category); + point.Text = "Test note"; + + var layout = Render( + @ + + + + + ); + + // Assert - Note should not be pinned initially + point.IsPinned.Should().BeFalse(); + + // Act - Pin the note + var pinButton = layout.Find(".note-pin-button"); + pinButton.Click(); + + // Assert - Note should be pinned + point.IsPinned.Should().BeTrue(); + + // Act - Unpin the note + pinButton.Click(); + + // Assert - Note should be unpinned + point.IsPinned.Should().BeFalse(); + } + + [Fact] + public async Task PinnedNotesPageShowsPinnedNotes() + { + // Arrange + using var db = Services.GetService>().CreateDbContext(); + var today = DateOnly.FromDateTime(DateTime.Now); + var day = await db.GetOrCreateDayAndAddPoints(today); + + // Create and pin a note + var note = db.CreateNote(day); + note.Text = "Pinned test note"; + note.IsPinned = true; + note.Category.Points.Add(note); + await db.SaveChangesAsync(); + + // Act + var layout = Render( + @ + + + + + ); + + // Wait for the component to load + await Task.Delay(100); + + // Assert - Pinned note should be displayed + layout.FindAll(".pinned-note-card").Count.Should().BeGreaterThan(0); + layout.Markup.Should().Contain("Pinned test note"); + } + + [Fact] + public void PinnedNotesPageShowsEmptyMessageWhenNoPinnedNotes() + { + // Arrange - No pinned notes + + // Act + var layout = Render( + @ + + + + + ); + + // Assert + layout.Markup.Should().Contain("No pinned notes yet"); + } +} diff --git a/JournalApp/Pages/PinnedNotesPage.razor b/JournalApp/Pages/PinnedNotesPage.razor index 9e672487..db57f8f2 100644 --- a/JournalApp/Pages/PinnedNotesPage.razor +++ b/JournalApp/Pages/PinnedNotesPage.razor @@ -60,9 +60,13 @@ .Include(p => p.Day) .Include(p => p.Category) .Where(p => p.IsPinned && !p.Deleted && p.Type == PointType.Note) + .ToListAsync(); + + // Order on the client side to avoid SQLite DateTimeOffset ordering issues + _pinnedNotes = _pinnedNotes .OrderBy(p => p.Day.Date) .ThenBy(p => p.CreatedAt) - .ToListAsync(); + .ToList(); logger.LogInformation($"Loaded {_pinnedNotes.Count} pinned notes"); StateHasChanged(); From 925be1dd950798ff5a7f643e1b94f6afe753b6bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:29:39 +0000 Subject: [PATCH 4/4] Address code review feedback: improve test and add clarifying comments Co-authored-by: danielchalmers <7112040+danielchalmers@users.noreply.github.com> --- JournalApp.Tests/PinnedNotesTests.razor | 7 ++----- JournalApp/Components/DataPointView.razor | 2 ++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/JournalApp.Tests/PinnedNotesTests.razor b/JournalApp.Tests/PinnedNotesTests.razor index b74d9730..2336e602 100644 --- a/JournalApp.Tests/PinnedNotesTests.razor +++ b/JournalApp.Tests/PinnedNotesTests.razor @@ -74,11 +74,8 @@ ); - // Wait for the component to load - await Task.Delay(100); - - // Assert - Pinned note should be displayed - layout.FindAll(".pinned-note-card").Count.Should().BeGreaterThan(0); + // Assert - Wait for the component to render and show pinned notes + layout.WaitForState(() => layout.FindAll(".pinned-note-card").Count > 0, timeout: TimeSpan.FromSeconds(5)); layout.Markup.Should().Contain("Pinned test note"); } diff --git a/JournalApp/Components/DataPointView.razor b/JournalApp/Components/DataPointView.razor index 588d309c..851af64c 100644 --- a/JournalApp/Components/DataPointView.razor +++ b/JournalApp/Components/DataPointView.razor @@ -149,6 +149,8 @@ else if (Point.Type == PointType.Medication) { logger.LogDebug("Toggling pin for note"); Point.IsPinned = !Point.IsPinned; + // Note: Changes are persisted by the parent page's SaveState method + // which is called on navigation, window deactivation, or disposal } async Task OnMedicationTakenChanged()