Add persisted location review pagination
This commit is contained in:
parent
7990cb244e
commit
c82430da04
@ -1059,9 +1059,18 @@ static void StoryIntelligenceHeavyReviewModulesOfferBoundedRendering()
|
||||
foreach (var viewName in heavyViews)
|
||||
{
|
||||
var view = File.ReadAllText(Path.Combine(root, "Views/Onboarding", viewName));
|
||||
Assert(view.Contains("const int previewLimit = 25", StringComparison.Ordinal) || view.Contains("const pageSize = 24;", StringComparison.Ordinal), $"{viewName} should cap visible candidates to a small page.");
|
||||
if (string.Equals(viewName, "StoryIntelligenceLocations.cshtml", StringComparison.Ordinal))
|
||||
{
|
||||
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
|
||||
Assert(onboarding.Contains("reviewCandidates.GetLocationReviewAsync(frame.Batch, pageNumber, pageSize)", StringComparison.Ordinal), $"{viewName} should use persisted bounded candidate pages.");
|
||||
Assert(view.Contains("var locationCandidates = Model.LocationReview.Candidates.ToList();", StringComparison.Ordinal), $"{viewName} should render only the persisted candidate page.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert(view.Contains("const int previewLimit = 25", StringComparison.Ordinal) || view.Contains("const pageSize = 24;", StringComparison.Ordinal), $"{viewName} should cap visible candidates to a small page.");
|
||||
}
|
||||
Assert(!view.Contains("asp-route-view=\"all\"", StringComparison.Ordinal), $"{viewName} should not expose an unbounded View All path.");
|
||||
Assert(view.Contains("hiddenCandidateCount", StringComparison.Ordinal), $"{viewName} should disclose hidden remaining decisions.");
|
||||
Assert(view.Contains("hiddenCandidateCount", StringComparison.Ordinal) || view.Contains("PendingCandidateCount", StringComparison.Ordinal), $"{viewName} should disclose hidden remaining decisions.");
|
||||
}
|
||||
|
||||
var assets = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceAssets.cshtml"));
|
||||
@ -2700,14 +2709,28 @@ static void LocationReviewUxSupportsLargeCandidateSets()
|
||||
var view = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceLocations.cshtml"));
|
||||
var viewModels = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs"));
|
||||
var service = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceLocationImportService.cs"));
|
||||
var persisted = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceReviewCandidateService.cs"));
|
||||
var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs"));
|
||||
|
||||
Assert(view.Contains("const int previewLimit = 25", StringComparison.Ordinal), "Location review should render a bounded candidate batch.");
|
||||
Assert(view.Contains("hiddenCandidateCount", StringComparison.Ordinal), "Location review should disclose remaining decisions outside the current batch.");
|
||||
Assert(view.Contains("var locationCandidates = Model.LocationReview.Candidates.ToList();", StringComparison.Ordinal), "Location review should render only the persisted candidate page.");
|
||||
Assert(viewModels.Contains("PageCount => PendingCandidateCount <= 0 ? 1 : (int)Math.Ceiling(PendingCandidateCount / (double)PageSize)", StringComparison.Ordinal), "52 pending candidates should page as 25 / 25 / 2.");
|
||||
Assert(viewModels.Contains("ShowingStart", StringComparison.Ordinal) && viewModels.Contains("ShowingEnd", StringComparison.Ordinal), "Location review should show the visible pending range.");
|
||||
Assert(view.Contains("story-review-page-status", StringComparison.Ordinal), "Location review should show bounded review counts.");
|
||||
Assert(!view.Contains("data-location-page-next", StringComparison.Ordinal), "Location review should not hide a full candidate DOM behind client-side paging.");
|
||||
Assert(view.Contains("Page @Model.LocationReview.PageNumber", StringComparison.Ordinal), "Location review should show Page X of Y.");
|
||||
Assert(view.Contains("Showing @Model.LocationReview.ShowingStart", StringComparison.Ordinal), "Location review should show N-M of T pending.");
|
||||
Assert(view.Contains("name=\"intent\" value=\"previous\"", StringComparison.Ordinal)
|
||||
&& view.Contains("name=\"intent\" value=\"next\"", StringComparison.Ordinal), "Previous and Next should submit the current page so decisions persist before navigation.");
|
||||
Assert(view.Contains("name=\"PageNumber\"", StringComparison.Ordinal), "Location review should post the current page number.");
|
||||
Assert(view.Contains("EvidenceSummaries", StringComparison.Ordinal), "Location review should expose compact aggregated evidence.");
|
||||
Assert(!view.Contains("data-location-bulk=\"create-all\"", StringComparison.Ordinal), "Location review should not offer bulk create-all.");
|
||||
Assert(!view.Contains("data-location-bulk=\"ignore-all\"", StringComparison.Ordinal), "Location review should not offer bulk ignore-all.");
|
||||
Assert(viewModels.Contains("public int PageNumber { get; set; } = 1;", StringComparison.Ordinal), "Location review posts should preserve the page being saved.");
|
||||
Assert(persisted.Contains("pageNumber = Math.Clamp(pageNumber, 1, maxPage)", StringComparison.Ordinal), "Persisted candidate pages should clamp after pending totals change.");
|
||||
Assert(controller.Contains("ImportStoryIntelligenceLocations(StoryIntelligenceLocationImportForm form, string intent = \"save\")", StringComparison.Ordinal), "Location review POST should accept a save/navigation intent.");
|
||||
Assert(controller.Contains("pendingCount <= 0", StringComparison.Ordinal)
|
||||
&& controller.Contains("RedirectToAction(nameof(StoryIntelligenceReviewCentre)", StringComparison.Ordinal), "Location review should return to Review Centre only after module-wide pending reaches zero.");
|
||||
Assert(controller.Contains("RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID, page = targetPage })", StringComparison.Ordinal), "Location review should stay in-module while pending candidates remain.");
|
||||
Assert(controller.Contains("var targetPage = string.Equals(intent, \"previous\"", StringComparison.Ordinal), "Previous navigation should move to the prior persisted page after saving.");
|
||||
Assert(viewModels.Contains("IReadOnlyList<string> EvidenceSummaries", StringComparison.Ordinal), "Location candidates should carry aggregated evidence summaries.");
|
||||
Assert(service.Contains("candidate.Appearances.Any(existing => existing.SceneID == appearance.SceneID", StringComparison.Ordinal), "Location evidence should be deduplicated per scene/observation.");
|
||||
Assert(service.Contains("locations.ListAliasesAsync(location.LocationID)", StringComparison.Ordinal), "Existing Location aliases should participate in matching.");
|
||||
|
||||
@ -546,7 +546,7 @@ public sealed class OnboardingController(
|
||||
|
||||
[HttpPost("story-intelligence/locations")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ImportStoryIntelligenceLocations(StoryIntelligenceLocationImportForm form)
|
||||
public async Task<IActionResult> ImportStoryIntelligenceLocations(StoryIntelligenceLocationImportForm form, string intent = "save")
|
||||
{
|
||||
var (progress, result) = await storyIntelligence.ImportLocationsAsync(form.BatchID, form);
|
||||
if (progress is null)
|
||||
@ -555,9 +555,26 @@ public sealed class OnboardingController(
|
||||
}
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
return result.Success
|
||||
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID })
|
||||
: RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID });
|
||||
if (!result.Success)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID, page = Math.Max(1, form.PageNumber) });
|
||||
}
|
||||
|
||||
var pendingCount = progress.LocationReview.SummaryNeedsAttentionCount ?? progress.LocationReview.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(StoryIntelligenceLocations), new { batchId = form.BatchID, page = targetPage });
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/assets")]
|
||||
|
||||
@ -553,6 +553,10 @@ public sealed class StoryIntelligenceReviewCandidateService(
|
||||
return new CandidatePage<TCandidate>([], pageNumber, pageSize, 0, 0, false);
|
||||
}
|
||||
|
||||
var maxPage = generation.PendingCount <= 0
|
||||
? 1
|
||||
: (int)Math.Ceiling(generation.PendingCount / (double)pageSize);
|
||||
pageNumber = Math.Clamp(pageNumber, 1, maxPage);
|
||||
var rows = await candidates.ListPageAsync(bookId, module, pageNumber, pageSize);
|
||||
var items = rows.Select(row => JsonSerializer.Deserialize<TCandidate>(row.CandidatePayloadJson, JsonOptions))
|
||||
.Where(item => item is not null)
|
||||
|
||||
@ -451,8 +451,11 @@ public sealed class StoryIntelligenceLocationReviewViewModel
|
||||
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<StoryIntelligenceLocationParentOptionViewModel> ParentLocationOptions { get; init; } = [];
|
||||
public IReadOnlyList<StoryIntelligenceLocationIdentityOptionViewModel> SameLocationOptions { get; init; } = [];
|
||||
public IReadOnlyList<StoryIntelligenceLocationTargetOptionViewModel> LocationTargetOptions { get; init; } = [];
|
||||
@ -516,6 +519,7 @@ public sealed class StoryIntelligenceLocationReviewCandidateViewModel
|
||||
public sealed class StoryIntelligenceLocationImportForm
|
||||
{
|
||||
public Guid BatchID { get; set; }
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public List<StoryIntelligenceLocationImportChoiceForm> Locations { get; set; } = [];
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
@model StoryIntelligenceProgressViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Review locations";
|
||||
const int previewLimit = 25;
|
||||
var locationCandidates = Model.LocationReview.Candidates.Take(previewLimit).ToList();
|
||||
var hiddenCandidateCount = Math.Max(0, Model.LocationReview.PendingCandidateCount - locationCandidates.Count);
|
||||
var locationCandidates = Model.LocationReview.Candidates.ToList();
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-location-title">
|
||||
@ -40,14 +38,15 @@
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="link-existing">Use exact matches</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="expand">Expand all</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="collapse">Collapse all</button>
|
||||
@if (hiddenCandidateCount > 0)
|
||||
{
|
||||
<span class="story-review-page-status">Showing @locationCandidates.Count.ToString("N0") of @Model.LocationReview.PendingCandidateCount.ToString("N0") pending</span>
|
||||
}
|
||||
<span class="story-review-page-status">
|
||||
Page @Model.LocationReview.PageNumber.ToString("N0") of @Model.LocationReview.PageCount.ToString("N0")
|
||||
· Showing @Model.LocationReview.ShowingStart.ToString("N0")-@Model.LocationReview.ShowingEnd.ToString("N0") of @Model.LocationReview.PendingCandidateCount.ToString("N0") pending
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form asp-action="ImportStoryIntelligenceLocations" method="post" data-location-review-form data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<input type="hidden" name="PageNumber" value="@Model.LocationReview.PageNumber" />
|
||||
<div class="story-character-card-grid">
|
||||
@for (var i = 0; i < locationCandidates.Count; i++)
|
||||
{
|
||||
@ -197,20 +196,19 @@
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
@if (hiddenCandidateCount > 0)
|
||||
{
|
||||
<span class="text-muted">Save this batch to continue with @hiddenCandidateCount.ToString("N0") more location decision@(hiddenCandidateCount == 1 ? string.Empty : "s").</span>
|
||||
}
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<span class="text-muted">
|
||||
Page @Model.LocationReview.PageNumber.ToString("N0") of @Model.LocationReview.PageCount.ToString("N0")
|
||||
· Showing @Model.LocationReview.ShowingStart.ToString("N0")-@Model.LocationReview.ShowingEnd.ToString("N0") of @Model.LocationReview.PendingCandidateCount.ToString("N0") pending
|
||||
</span>
|
||||
@if (Model.LocationReview.HasPreviousPage)
|
||||
{
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceLocations" asp-route-batchId="@Model.BatchID" asp-route-page="@(Model.LocationReview.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.LocationReview.HasNextPage)
|
||||
{
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceLocations" asp-route-batchId="@Model.BatchID" asp-route-page="@(Model.LocationReview.PageNumber + 1)">Next</a>
|
||||
<button class="btn btn-outline-secondary" type="submit" name="intent" value="next" data-busy-label="Saving page...">Next</button>
|
||||
}
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving locations...">Save location decisions</button>
|
||||
<button class="btn btn-primary" type="submit" name="intent" value="save" data-busy-label="Saving locations...">Save location decisions</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user