Fix knowledge review paging actions

This commit is contained in:
Nick Beckley 2026-09-02 20:25:04 +00:00
parent ba8edd26b0
commit 56236104bc
4 changed files with 53 additions and 12 deletions

View File

@ -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<StoryIntelligenceRelationshipReviewViewModel> GetRelationshipReviewAsync", "public async Task<StoryIntelligenceKnowledgeReviewViewModel> 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()

View File

@ -655,7 +655,7 @@ public sealed class OnboardingController(
[HttpPost("story-intelligence/knowledge")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ImportStoryIntelligenceKnowledge(StoryIntelligenceKnowledgeImportForm form)
public async Task<IActionResult> 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")]

View File

@ -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<StoryIntelligenceCharacterReviewCandidateViewModel> 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<SelectListItem> KnowledgeStateOptions { get; init; } = [];
public IReadOnlyList<StoryIntelligenceKnowledgeReviewCandidateViewModel> 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<StoryIntelligenceKnowledgeImportChoiceForm> Knowledge { get; set; } = [];
}

View File

@ -54,6 +54,7 @@
<form asp-action="ImportStoryIntelligenceKnowledge" method="post" data-knowledge-review-form data-story-review-form>
<input type="hidden" name="BatchID" value="@Model.BatchID" />
<input type="hidden" name="PageNumber" value="@Model.KnowledgeReview.PageNumber" />
<div class="story-character-card-grid">
@for (var i = 0; i < knowledgeCandidates.Count; i++)
{
@ -147,20 +148,23 @@
</div>
<div class="onboarding-actions">
@if (hiddenCandidateCount > 0)
{
<span class="text-muted">Save this batch to continue with @hiddenCandidateCount.ToString("N0") more knowledge decision@(hiddenCandidateCount == 1 ? string.Empty : "s").</span>
}
<span class="text-muted">
Page @Model.KnowledgeReview.PageNumber.ToString("N0") of @Model.KnowledgeReview.PageCount.ToString("N0")
&middot; Showing @Model.KnowledgeReview.ShowingStart.ToString("N0")-@Model.KnowledgeReview.ShowingEnd.ToString("N0") of @Model.KnowledgeReview.PendingCandidateCount.ToString("N0") pending
</span>
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
@if (Model.KnowledgeReview.HasPreviousPage)
{
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceKnowledge" asp-route-batchId="@Model.BatchID" asp-route-page="@(Model.KnowledgeReview.PageNumber - 1)">Previous</a>
<button class="btn btn-outline-secondary" type="submit" name="intent" value="previous" data-busy-label="Saving page...">Previous</button>
}
@if (Model.KnowledgeReview.HasNextPage)
{
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceKnowledge" asp-route-batchId="@Model.BatchID" asp-route-page="@(Model.KnowledgeReview.PageNumber + 1)">Next</a>
<button class="btn btn-primary" type="submit" name="intent" value="next" data-busy-label="Saving page...">Next</button>
}
else
{
<button class="btn btn-primary" type="submit" name="intent" value="save" data-busy-label="Saving knowledge...">Save knowledge decisions</button>
}
<button class="btn btn-primary" type="submit" data-busy-label="Saving knowledge...">Save knowledge decisions</button>
</div>
</form>
}