Repair a failed import row from the place form #158

Merged
rob merged 1 commit from feature/task-219-repair-failed-import-rows into main 2026-08-14 08:52:25 +00:00
Owner

Closes Vikunja task 219. ADR-0135.

Summary

  • Parser-rejected rows (missing/invalid name, coordinates) now carry their partial data forward through a new PartialPlace type (Name, Description, Latitude, Longitude, all optional) — PlaceImportRow.Invalid takes it as a required third argument, PlaceImportRowFailure.Partial carries it into the report. Rows the API refused, or ones never attempted, stay read-only (Partial is null).
  • PlaceImportReport now carries GroupId — the group the import targeted.
  • PlaceForm gains a third seeding surface, InitialValues (a PartialPlace?), alongside the existing InitialCoordinates/DefaultGroupId — either coordinate may be present without the other, unlike a map-click seed. Follows the same "seeded once, never re-derived" shape, with its own _seededInitialValues guard.
  • A repair seed does not make the form dirty, matching ADR-0126's map-click precedent — the data is recoverable (still held by the failure list until an actual save), so nothing is lost by dismissing. Proved directly: a repair dismissed after typing over the seed, then reopened, reseeds from the original row, not the discarded edit.
  • ImportReportBanner renders a real <button class="btn-ghost import-report-repair"> for each repairable failure, hosting its own PlaceFormPanel instance (the same pattern Home/Drawer already use for their own add-mode entry points). PlaceFormPanel gains OnPlaceSaved, distinct from OnClose, since the banner needs to know specifically that a save happened.
  • CallerGroupsState.EnsureLoadedAsync is deferred to the repair button's own click handler, not loaded unconditionally on every page — this banner is mounted everywhere, authenticated or not.
  • Repairing the last outstanding failure dismisses the whole report; repairing one of several removes only that entry. A repair landing in a group the reader has hidden un-hides it (task 215/ADR-0131's mechanism), proved directly rather than assumed from the call site's signature being unchanged.
  • A second import replacing an unrepaired report is left as ImportReportState.Show's existing behaviour — deliberate, recorded in the ADR, not an oversight.

See ADR-0135 for the full reasoning and alternatives considered.

Test plan

  • dotnet build PlaceMark.slnx clean (SDK 10.0.100)
  • PlaceMark.WebUI.Tests: 754 total, 750 passed, 4 skipped (pre-existing), stable across repeated full runs
  • PlaceMark.Architecture.Tests: 123 total, all passed (ADR index, ButtonWeightCoverage, FieldErrors validation)
  • Mutation-tested: partial-data carrying (CSV parser), RemoveRepairedRow, the banner's repairable/non-repairable gate, the IsDirty repair-seed baseline, and the hidden-group EnsureGroupVisible unhide — each reddened under a targeted mutation and was restored
  • The reseed test's WaitForAssertion is ADR-0119's own class of gap (OverlayFocusTrap.ExitAsync scheduled asynchronously after a panel closes), confirmed empirically — reddens 9/10 in isolation without it, and an isolated no-typing/no-discard repro reddens 8/8 on nothing but "close a panel, open a new one straight after" — not a StartRepair ordering defect
Closes Vikunja task 219. ADR-0135. ## Summary - Parser-rejected rows (missing/invalid name, coordinates) now carry their partial data forward through a new `PartialPlace` type (`Name`, `Description`, `Latitude`, `Longitude`, all optional) — `PlaceImportRow.Invalid` takes it as a required third argument, `PlaceImportRowFailure.Partial` carries it into the report. Rows the API refused, or ones never attempted, stay read-only (`Partial` is `null`). - `PlaceImportReport` now carries `GroupId` — the group the import targeted. - `PlaceForm` gains a third seeding surface, `InitialValues` (a `PartialPlace?`), alongside the existing `InitialCoordinates`/`DefaultGroupId` — either coordinate may be present without the other, unlike a map-click seed. Follows the same "seeded once, never re-derived" shape, with its own `_seededInitialValues` guard. - A repair seed does **not** make the form dirty, matching ADR-0126's map-click precedent — the data is recoverable (still held by the failure list until an actual save), so nothing is lost by dismissing. Proved directly: a repair dismissed after typing over the seed, then reopened, reseeds from the original row, not the discarded edit. - `ImportReportBanner` renders a real `<button class="btn-ghost import-report-repair">` for each repairable failure, hosting its own `PlaceFormPanel` instance (the same pattern `Home`/`Drawer` already use for their own add-mode entry points). `PlaceFormPanel` gains `OnPlaceSaved`, distinct from `OnClose`, since the banner needs to know specifically that a save happened. - `CallerGroupsState.EnsureLoadedAsync` is deferred to the repair button's own click handler, not loaded unconditionally on every page — this banner is mounted everywhere, authenticated or not. - Repairing the last outstanding failure dismisses the whole report; repairing one of several removes only that entry. A repair landing in a group the reader has hidden un-hides it (task 215/ADR-0131's mechanism), proved directly rather than assumed from the call site's signature being unchanged. - A second import replacing an unrepaired report is left as `ImportReportState.Show`'s existing behaviour — deliberate, recorded in the ADR, not an oversight. See ADR-0135 for the full reasoning and alternatives considered. ## Test plan - [x] `dotnet build PlaceMark.slnx` clean (SDK 10.0.100) - [x] `PlaceMark.WebUI.Tests`: 754 total, 750 passed, 4 skipped (pre-existing), stable across repeated full runs - [x] `PlaceMark.Architecture.Tests`: 123 total, all passed (ADR index, ButtonWeightCoverage, FieldErrors validation) - [x] Mutation-tested: partial-data carrying (CSV parser), `RemoveRepairedRow`, the banner's repairable/non-repairable gate, the `IsDirty` repair-seed baseline, and the hidden-group `EnsureGroupVisible` unhide — each reddened under a targeted mutation and was restored - [x] The reseed test's `WaitForAssertion` is ADR-0119's own class of gap (`OverlayFocusTrap.ExitAsync` scheduled asynchronously after a panel closes), confirmed empirically — reddens 9/10 in isolation without it, and an isolated no-typing/no-discard repro reddens 8/8 on nothing but "close a panel, open a new one straight after" — not a `StartRepair` ordering defect
Repair a failed import row from the place form
All checks were successful
CI / build (pull_request) Successful in 3m11s
CI / container-images (pull_request) Has been skipped
CI / e2e (pull_request) Successful in 2m1s
22901ac3c8
rob force-pushed feature/task-219-repair-failed-import-rows from 22901ac3c8
All checks were successful
CI / build (pull_request) Successful in 3m11s
CI / container-images (pull_request) Has been skipped
CI / e2e (pull_request) Successful in 2m1s
to 36c98efc6d
All checks were successful
CI / build (pull_request) Successful in 4m6s
CI / container-images (pull_request) Has been skipped
CI / e2e (pull_request) Successful in 2m8s
2026-08-14 08:15:48 +00:00
Compare
rob force-pushed feature/task-219-repair-failed-import-rows from 36c98efc6d
All checks were successful
CI / build (pull_request) Successful in 4m6s
CI / container-images (pull_request) Has been skipped
CI / e2e (pull_request) Successful in 2m8s
to 625ff4b067
All checks were successful
CI / build (pull_request) Successful in 2m55s
CI / container-images (pull_request) Has been skipped
CI / e2e (pull_request) Successful in 1m51s
2026-08-14 08:34:23 +00:00
Compare
rob left a comment

Verdict: mergeable

Verified independently, from a clean clone of the PR head (not the author's worktree), with the pinned SDK: dotnet build clean; PlaceMark.WebUI.Tests 754/750/4, PlaceMark.Architecture.Tests 123/123 — matches the PR body exactly.

Mutation-tested five of the claimed guards myself rather than trusting the ADR's Consequences section: nulling CsvPlaceImportParser.BuildRow's PartialPlace construction reddens exactly 4 tests; widening the banner's repair-button gate reddens ImportReportBanner_FailureWithNoPartialData_OffersNoRepairButton; dropping the _seededInitialValues term from IsDirty reddens PlaceForm_CreateWithInitialValues_IsNotDirty; removing EnsureGroupVisible from PlaceFormPanel.HandleSavedAsync reddens the hidden-group repair test. All confirmed, then reverted.

Checked the invariant directly: PlaceImportRunner.RunAsync's API-refusal and systemic-abort branches never pass Partial, only the parser-rejection branch does — Request/Error/Partial stay correctly exclusive, nothing downstream re-derives validity from Partial. PlaceForm.SubmitAsync still builds CreatePlaceRequest/UpdatePlaceRequest fresh from typed state, never from PartialPlace. No PlaceMark.Api/PlaceMark.Contracts file is touched by this PR at all.

Also independently checked the WaitForAssertion finding, since it's the most interesting claim here. Removed it and ran the reseed test 10/10 in isolation — all 10 failed identically: #place-form-name simply absent, not present with a stale/wrong value. That failure shape corroborates a render-timing gap (ADR-0119's own documented mechanism: OverlayFocusTrap.ExitAsync only fires from ModalOverlay.DisposeAsync, scheduled after the removing render) rather than a data-ordering defect in StartRepair — a real StartRepair bug would more likely show the wrong value, not no element at all. Also confirmed CallerGroupsState.EnsureLoadedAsync's guard is checked before any await, so it returns synchronously once loaded and can't be the intermittent cause either — consistent with the author's own disproof of their first explanation. Separately, I mutated CloseRepair to also call RemoveRepairedRow (modelling the ADR's own rejected "infer save from close" alternative) and the reseed test caught it immediately — so the test isn't purely tautological-by-construction, it does guard a real regression class.

ADR-0135 and docs/adr/README.md check out: correctly slotted between 0134/0140, README diff is exactly one added row, otherwise byte-identical.

One doc-only nit, not blocking: PartialPlace.cs's cref="Contracts.Places.CreatePlaceRequest" won't actually resolve (no using PlaceMark.Contracts.Places, and the project has no GenerateDocumentationFile so nothing catches it at build time) — harmless today since crefs aren't validated here, but worth cref="PlaceMark.Contracts.Places.CreatePlaceRequest" in a follow-up.

Verdict: mergeable Verified independently, from a clean clone of the PR head (not the author's worktree), with the pinned SDK: `dotnet build` clean; `PlaceMark.WebUI.Tests` 754/750/4, `PlaceMark.Architecture.Tests` 123/123 — matches the PR body exactly. Mutation-tested five of the claimed guards myself rather than trusting the ADR's Consequences section: nulling `CsvPlaceImportParser.BuildRow`'s `PartialPlace` construction reddens exactly 4 tests; widening the banner's repair-button gate reddens `ImportReportBanner_FailureWithNoPartialData_OffersNoRepairButton`; dropping the `_seededInitialValues` term from `IsDirty` reddens `PlaceForm_CreateWithInitialValues_IsNotDirty`; removing `EnsureGroupVisible` from `PlaceFormPanel.HandleSavedAsync` reddens the hidden-group repair test. All confirmed, then reverted. Checked the invariant directly: `PlaceImportRunner.RunAsync`'s API-refusal and systemic-abort branches never pass `Partial`, only the parser-rejection branch does — `Request`/`Error`/`Partial` stay correctly exclusive, nothing downstream re-derives validity from `Partial`. `PlaceForm.SubmitAsync` still builds `CreatePlaceRequest`/`UpdatePlaceRequest` fresh from typed state, never from `PartialPlace`. No `PlaceMark.Api`/`PlaceMark.Contracts` file is touched by this PR at all. Also independently checked the `WaitForAssertion` finding, since it's the most interesting claim here. Removed it and ran the reseed test 10/10 in isolation — all 10 failed identically: `#place-form-name` simply absent, not present with a stale/wrong value. That failure shape corroborates a render-timing gap (ADR-0119's own documented mechanism: `OverlayFocusTrap.ExitAsync` only fires from `ModalOverlay.DisposeAsync`, scheduled after the removing render) rather than a data-ordering defect in `StartRepair` — a real `StartRepair` bug would more likely show the wrong *value*, not no element at all. Also confirmed `CallerGroupsState.EnsureLoadedAsync`'s guard is checked before any `await`, so it returns synchronously once loaded and can't be the intermittent cause either — consistent with the author's own disproof of their first explanation. Separately, I mutated `CloseRepair` to also call `RemoveRepairedRow` (modelling the ADR's own rejected "infer save from close" alternative) and the reseed test caught it immediately — so the test isn't purely tautological-by-construction, it does guard a real regression class. ADR-0135 and `docs/adr/README.md` check out: correctly slotted between 0134/0140, README diff is exactly one added row, otherwise byte-identical. One doc-only nit, not blocking: `PartialPlace.cs`'s `cref="Contracts.Places.CreatePlaceRequest"` won't actually resolve (no `using PlaceMark.Contracts.Places`, and the project has no `GenerateDocumentationFile` so nothing catches it at build time) — harmless today since crefs aren't validated here, but worth `cref="PlaceMark.Contracts.Places.CreatePlaceRequest"` in a follow-up.
rob merged commit 1e08bbd692 into main 2026-08-14 08:52:25 +00:00
rob deleted branch feature/task-219-repair-failed-import-rows 2026-08-14 08:52:25 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/PlaceMark!158
No description provided.