From 56236104bc306962641bf56e15fb00b812ef7f61 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Wed, 2 Sep 2026 20:25:04 +0000 Subject: [PATCH] Fix knowledge review paging actions --- PlotLine.Tests/Program.cs | 13 ++++++++++ PlotLine/Controllers/OnboardingController.cs | 25 ++++++++++++++++--- PlotLine/ViewModels/OnboardingViewModels.cs | 9 ++++++- .../StoryIntelligenceKnowledge.cshtml | 18 +++++++------ 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 77578f5..daf4f45 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -1294,6 +1294,9 @@ static void StoryIntelligenceDetailedReviewsReadPersistedCandidatePages() var migration = File.ReadAllText(Path.Combine(root, "Sql/177_Phase27D_StoryIntelligencePersistedReviewCandidates.sql")); var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs")); var program = File.ReadAllText(Path.Combine(root, "Program.cs")); + var knowledgeView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceKnowledge.cshtml")); + var viewModels = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs")); + var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs")); foreach (var module in new[] { "Characters", "Locations", "Assets", "Relationships", "Knowledge" }) { @@ -1315,6 +1318,16 @@ static void StoryIntelligenceDetailedReviewsReadPersistedCandidatePages() Assert(!ExtractBetween(service, "public async Task GetRelationshipReviewAsync", "public async Task GetKnowledgeReviewAsync").Contains("BuildReviewAsync", StringComparison.Ordinal), "Relationship GET must not call the old builder."); Assert(program.Contains("IStoryIntelligenceReviewCandidateRepository, StoryIntelligenceReviewCandidateRepository", StringComparison.Ordinal), "Persisted candidate repository should be registered."); Assert(program.Contains("IStoryIntelligenceReviewCandidateService, StoryIntelligenceReviewCandidateService", StringComparison.Ordinal), "Persisted candidate service should be registered."); + Assert(knowledgeView.Contains("Page @Model.KnowledgeReview.PageNumber", StringComparison.Ordinal) + && knowledgeView.Contains("Showing @Model.KnowledgeReview.ShowingStart", StringComparison.Ordinal), "Knowledge review should show page and pending range counts."); + Assert(knowledgeView.Contains("name=\"PageNumber\"", StringComparison.Ordinal), "Knowledge review should post the current page number."); + Assert(knowledgeView.Contains("name=\"intent\" value=\"previous\"", StringComparison.Ordinal) + && knowledgeView.Contains("name=\"intent\" value=\"next\"", StringComparison.Ordinal), "Knowledge review Previous and Next should submit current-page decisions before navigation."); + Assert(knowledgeView.Contains("if (Model.KnowledgeReview.HasNextPage)", StringComparison.Ordinal) + && knowledgeView.Contains("name=\"intent\" value=\"save\"", StringComparison.Ordinal), "Knowledge review should show Next before the last page and Save only on the last page."); + Assert(viewModels.Contains("public int PageNumber { get; set; } = 1;", StringComparison.Ordinal), "Knowledge review posts should preserve the page being saved."); + Assert(controller.Contains("ImportStoryIntelligenceKnowledge(StoryIntelligenceKnowledgeImportForm form, string intent = \"save\")", StringComparison.Ordinal), "Knowledge POST should accept a save/navigation intent."); + Assert(controller.Contains("RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID, page = targetPage })", StringComparison.Ordinal), "Knowledge review should stay in-module while pending candidates remain."); } static void StoryIntelligenceModuleCompletionLifecycleUsesPersistedCandidates() diff --git a/PlotLine/Controllers/OnboardingController.cs b/PlotLine/Controllers/OnboardingController.cs index 93c82b3..223f6b7 100644 --- a/PlotLine/Controllers/OnboardingController.cs +++ b/PlotLine/Controllers/OnboardingController.cs @@ -655,7 +655,7 @@ public sealed class OnboardingController( [HttpPost("story-intelligence/knowledge")] [ValidateAntiForgeryToken] - public async Task ImportStoryIntelligenceKnowledge(StoryIntelligenceKnowledgeImportForm form) + public async Task ImportStoryIntelligenceKnowledge(StoryIntelligenceKnowledgeImportForm form, string intent = "save") { var (progress, result) = await storyIntelligence.ImportKnowledgeAsync(form.BatchID, form); if (progress is null) @@ -664,9 +664,26 @@ public sealed class OnboardingController( } TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; - return result.Success - ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) - : RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID }); + if (!result.Success) + { + return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID, page = Math.Max(1, form.PageNumber) }); + } + + var pendingCount = progress.KnowledgeReview.SummaryNeedsAttentionCount ?? progress.KnowledgeReview.PendingCandidateCount; + if (pendingCount <= 0) + { + return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }); + } + + const int pageSize = 25; + var pageCount = Math.Max(1, (int)Math.Ceiling(pendingCount / (double)pageSize)); + var currentPage = Math.Max(1, form.PageNumber); + var targetPage = string.Equals(intent, "previous", StringComparison.OrdinalIgnoreCase) + ? currentPage - 1 + : currentPage; + targetPage = Math.Clamp(targetPage, 1, pageCount); + + return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID, page = targetPage }); } [HttpGet("story-intelligence/narrative")] diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index eb29bb6..c13b800 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -373,6 +373,9 @@ public sealed class StoryIntelligenceCharacterReviewViewModel public int PageSize { get; init; } = 25; public int TotalCandidateCount { get; init; } public int PendingCandidateCount { get; init; } + public int PageCount => PendingCandidateCount <= 0 ? 1 : (int)Math.Ceiling(PendingCandidateCount / (double)PageSize); + public int ShowingStart => PendingCandidateCount == 0 || Candidates.Count == 0 ? 0 : ((PageNumber - 1) * PageSize) + 1; + public int ShowingEnd => ShowingStart == 0 ? 0 : Math.Min(PendingCandidateCount, ShowingStart + Candidates.Count - 1); public bool HasPreviousPage => PageNumber > 1; public bool HasNextPage => PageNumber * PageSize < PendingCandidateCount; public IReadOnlyList Candidates { get; init; } = []; @@ -685,8 +688,11 @@ public sealed class StoryIntelligenceKnowledgeReviewViewModel public int PageSize { get; init; } = 25; public int TotalCandidateCount { get; init; } public int PendingCandidateCount { get; init; } + public int PageCount => PendingCandidateCount <= 0 ? 1 : (int)Math.Ceiling(PendingCandidateCount / (double)PageSize); + public int ShowingStart => PendingCandidateCount == 0 || Candidates.Count == 0 ? 0 : ((PageNumber - 1) * PageSize) + 1; + public int ShowingEnd => ShowingStart == 0 ? 0 : Math.Min(PendingCandidateCount, ShowingStart + Candidates.Count - 1); public bool HasPreviousPage => PageNumber > 1; - public bool HasNextPage => PageNumber * PageSize < PendingCandidateCount; + public bool HasNextPage => PageNumber < PageCount; public IReadOnlyList KnowledgeStateOptions { get; init; } = []; public IReadOnlyList Candidates { get; init; } = []; } @@ -711,6 +717,7 @@ public sealed class StoryIntelligenceKnowledgeReviewCandidateViewModel public sealed class StoryIntelligenceKnowledgeImportForm { public Guid BatchID { get; set; } + public int PageNumber { get; set; } = 1; public List Knowledge { get; set; } = []; } diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceKnowledge.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceKnowledge.cshtml index 19baa7a..beed43c 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceKnowledge.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceKnowledge.cshtml @@ -54,6 +54,7 @@
+
@for (var i = 0; i < knowledgeCandidates.Count; i++) { @@ -147,20 +148,23 @@
- @if (hiddenCandidateCount > 0) - { - Save this batch to continue with @hiddenCandidateCount.ToString("N0") more knowledge decision@(hiddenCandidateCount == 1 ? string.Empty : "s"). - } + + Page @Model.KnowledgeReview.PageNumber.ToString("N0") of @Model.KnowledgeReview.PageCount.ToString("N0") + · Showing @Model.KnowledgeReview.ShowingStart.ToString("N0")-@Model.KnowledgeReview.ShowingEnd.ToString("N0") of @Model.KnowledgeReview.PendingCandidateCount.ToString("N0") pending + Back to Review Centre @if (Model.KnowledgeReview.HasPreviousPage) { - Previous + } @if (Model.KnowledgeReview.HasNextPage) { - Next + + } + else + { + } -
}