fix(admin): clear unsaved-changes guard before redirect on groups/new and users/new #567
Open
marcel
wants to merge 4 commits from
feat/issue-527-unsaved-warning-new-pages into main
pull from: feat/issue-527-unsaved-warning-new-pages
merge into: marcel:main
marcel:main
marcel:feat/issue-533-mass-import-ux
marcel:worktree-feat+issue-557-upload-artifact-v3-pin
marcel:worktree-chore+issue-556-drop-client-branches-coverage-gate
marcel:fix/issue-514-prerender-crawl-bakes-redirects
marcel:fix/issue-472-prerender-entries
marcel:feat/issue-395-readme
marcel:feat/issue-345-bulk-mark-reviewed
marcel:feat/issue-344-bell-tooltip
marcel:feat/issue-341-annotieren-contrast
marcel:feat/issue-225-bulk-metadata-edit
marcel:feat/issue-317-bulk-upload
marcel:feat/issue-271-dashboard-redesign
marcel:docs/issue-240-mission-control-spec
marcel:refactor/issues-193-200
marcel:feat/issue-162-korrespondenz-redesign
marcel:feature/68-new-document-file-first
marcel:feat/81-discussion-discoverability
marcel:feat/62-document-bottom-panel
marcel:feature/56-backfill-file-hashes
marcel:feat/38-document-edit-history
marcel:fix/svelte5-test-delegation-and-login-auth
No Reviewers
Labels
Clear labels
P0-critical
P1-high
P2-medium
P3-later
audit
bug
cleanup
collaboration
conversation
descoped
devops
documentation
epic
feature
file-upload
legibility
notification
person
phase-1: security
phase-2: container-images
phase-3: prod-compose
phase-4: spring-prod-profile
phase-5: backups
phase-6: deployment-docs
phase-7: monitoring
refactor
security
test
ui
user
Blocks a core user journey, causes data loss, or is a security/accessibility barrier. Work on this before P1+.
Significant friction on a main user journey; workaround exists but is non-obvious. Next up after P0.
Noticeable improvement; doesn't block anything; schedule opportunistically.
Cosmetic or parking-lot work; fine to stay open indefinitely.
Read-only audit / assessment work; produces a report rather than changing code
Something isn't working
Removal of dead code, vague comments, naming violations, and scratch artifacts
We want to extend the family archive to have more collaboration tools
We will do that later
README, ARCHITECTURE, GLOSSARY, CONTRIBUTING, per-domain docs
Marker for epic-level issues that group multiple child issues
Codebase Legibility Refactor — preparing the codebase for human evaluation and bus-factor reduction
Security hygiene — must be done first
Production-ready multi-stage Docker images
Production compose overlay + Caddy reverse proxy
Spring Boot production configuration profile
Database and object storage backup strategy
.env.example, DEPLOYMENT.md, runbook
Prometheus, Loki, Grafana, Alertmanager
Code restructuring without behaviour change
UI/UX and accessibility issues
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: marcel/familienarchiv#567
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feat/issue-527-unsaved-warning-new-pages"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #527
Summary
use:enhanceon both/newpages had no callback, so on aredirectresult SvelteKit internally calledgoto()whileisDirtywas stilltrue— causingbeforeNavigateto cancel the navigation and show the amber "unsaved changes" banner even though the record had been saved successfully.unsaved.clearOnSuccess()beforeawait update(), so the guard is disarmed before SvelteKit's internalgoto()fires.isDirty/showUnsavedWarning/discardTarget/beforeNavigateblock and inline banner withcreateUnsavedWarning()+<UnsavedWarningBanner>— matching the existing pattern on the edit pages ([id]/+page.svelte).Changed files
frontend/src/routes/admin/groups/new/+page.sveltefrontend/src/routes/admin/users/new/+page.sveltefrontend/src/routes/admin/groups/new/page.svelte.spec.tsfrontend/src/routes/admin/users/new/page.svelte.spec.ts$app/navigationmock + 6 unsaved-warning testsTest plan
groups/new/page.svelte.spec.ts— 6 new tests all greenusers/new/page.svelte.spec.ts— 6 new tests + 7 pre-existing tests all greengroups/new/page.svelte.test.ts— all pre-existing rendering tests still greenusers/new/page.svelte.test.ts— all pre-existing rendering tests still greennpm run check— no new type errors introduced (pre-existing{ url: URL }pattern shared withgroups/[id],users/[id],tags/[id]specs)🤖 Generated with Claude Code
👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
Clean, focused fix. The root cause diagnosis is correct —
beforeNavigatewas cancelling SvelteKit's internal redirectgoto()becauseisDirtywas still true at the time it fired. CallingclearOnSuccess()beforeawait update()is the right ordering. I checked the hook API and this is exactly how you defuse the guard before SvelteKit takes over.What works well
unsaved.clearOnSuccess()runs beforeawait update(), so the guard is disarmed before SvelteKit's internal navigation fires. If the order were reversed, the bug would persist.vi.hoisted()pattern: Correctly used to capture the enhance function reference beforevi.mock()hoisting executes. The pattern is consistent with the TipTap workarounds established in this codebase.oninput={unsaved.markDirty}instead ofoninput={() => { isDirty = true; ... }}: Passing the handler reference directly rather than wrapping it is cleaner and avoids a closure that captured the now-deleted inline state.Suggestions (non-blocking)
SubmitFntype is defined inline four times — twice ingroups/new/page.svelte.spec.ts, twice inusers/new/page.svelte.spec.ts. It's identical each time. A sharedtestHelpers.tsor a localtype SubmitFn = ...at the top of each spec file would eliminate the duplication. Minor, but noticeable.No blockers. The fix is minimal and correct. The refactor aligns these pages with the pattern already used on the
[id]/editpages.🏛️ Markus Keller — Application Architect
Verdict: ✅ Approved
Pure frontend bugfix and refactor. I checked this against every row in my doc-update table — none are triggered:
ErrorCodeorPermissionPattern consistency: This PR correctly brings
admin/groups/newandadmin/users/newinto alignment with theadmin/groups/[id]andadmin/users/[id]edit pages, which already usecreateUnsavedWarning()+<UnsavedWarningBanner>. This is exactly the right abstraction reuse — the hook encapsulates theisDirty,showUnsavedWarning,discardTargetstate triangle that was duplicated across at least four pages. This PR eliminates two of those duplicates.Layer boundaries: No boundary leaks. This is a component-level change with no cross-domain coupling introduced.
Nothing to flag architecturally. The change reduces complexity.
🔧 Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
Nothing in my domain is touched by this PR — no CI workflow changes, no Compose file modifications, no Docker image updates, no secrets, no infrastructure configuration.
I checked:
The test additions are Vitest browser-mode tests — they run in the existing frontend unit test job, no new CI infrastructure needed.
LGTM from an infra perspective.
📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
Reviewing against the implied acceptance criteria for issue #527.
Requirements satisfied
The fix addresses two observable behaviors:
FR-1 (implicit): After a successful form submission that results in a redirect, the unsaved-changes banner MUST NOT appear.
→ Tested: "clears banner when enhance callback receives a redirect result" ✓
FR-2 (implicit): After a failed form submission (
failureresult), the unsaved-changes banner MUST remain visible if the form was dirty.→ Tested: "keeps banner when enhance callback receives a failure result" ✓
FR-3 (implicit): A clean form (no user input) MUST NOT trigger the unsaved-changes guard during navigation.
→ Tested: "does not cancel navigation when form is clean" ✓
FR-4 (implicit): A dirty form MUST cancel navigation and show the banner.
→ Tested: "cancels navigation and shows banner when form is dirty" ✓
Open question (informational, not blocking)
SvelteKit distinguishes three result types beyond
redirectandfailure:error,success, andredirect. The enhance callback only callsclearOnSuccess()forredirect. What aboutsuccess(a 2xx response without redirect, which would keep the user on the same page)? In this form's current+page.server.ts, a 2xx success path presumably doesn't exist — the action either redirects or fails — so this is not a present gap. Worth noting if the action contract changes in future.No blockers from a requirements perspective. The fix is well-scoped to the reported issue.
🔐 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
No security-relevant changes in this PR. I reviewed it through my standard lens:
m.admin_unsaved_warning()(Paraglide i18n key), no user input reflected@RequirePermission)<UnsavedWarningBanner>renders only static i18n text and a buttonresult.type, a locally-typed value from SvelteKit's form action frameworklocalStorage,sessionStorage, or cookies touchedThe
createUnsavedWarning()hook is pure client-side state management. The enhance callback receives aresultobject from SvelteKit's action layer — it's not derived from untrusted input.Nothing to flag.
🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
Overall the test coverage is solid and correctly verifies the bug fix. I have one suggestion and one informational note.
What works well
beforeEachresetsvi.clearAllMocks()andenhanceCaptureRef.submitFn— prevents test bleed across runs.vitest-browser-svelteused throughout — real DOM, not JSDOM. ThedispatchEvent(new InputEvent('input', { bubbles: true }))pattern for triggeringoninputis the established workaround for this project.Suggestion (non-blocking)
SubmitFntype defined inline 4 times across 2 spec files. Each occurrence is identical. Extract it to a shared utility or at minimum define it once per file:This doesn't affect test reliability, but it reduces maintenance surface.
Informational note (not a blocker for this PR)
There is no test for
result.type === 'error'(thrown by SvelteKit'serror()helper, distinct fromfailure()). In the current action implementation this path may be unreachable, but if a future maintainer adds anerror()call, the guard behavior for that case would be untested. Since the existingfailuretest already covers the "don't clear on non-redirect" invariant and these share the same code path (theif (result.type === 'redirect')check), this is informational only.The pre-existing tests (
groups/new/page.svelte.test.tsandusers/new/page.svelte.test.ts) still pass — no regressions in rendering behavior.🎨 Leonie Voss — UX Designer & Accessibility Strategist
Verdict: ✅ Approved
This is a UX regression fix — and a meaningful one. The original bug caused the amber "unsaved changes" banner to appear after a successful save, right before the redirect. For a 60+ user who just filled out a "New Group" form and submitted it, seeing an amber warning saying their changes are unsaved — immediately after saving — is genuinely confusing. It undermines trust in the interface ("did my save work?") and violates Nielsen Heuristic #1 (Visibility of system status) and Heuristic #4 (Consistency and standards — the edit pages don't have this problem).
What I verified
<UnsavedWarningBanner>— the same component already used on the[id]/editpages. Visual consistency is maintained. The amber banner style, the "Verwerfen" discard button, and the i18n keys are unchanged.border-amber-200 bg-amber-50 text-amber-800). The replacement<UnsavedWarningBanner>component encapsulates those — correct either way, but the component is the right abstraction.UnsavedWarningBannershould already meet the 44px minimum. This PR doesn't change that./ungespeicherte Änderungen/iwhich confirms theadmin_unsaved_warningkey is in use — no new raw German strings introduced in the component.One note for future work (not this PR)
The amber banner currently has no
aria-liveattribute to announce itself to screen readers when it appears. This pre-exists this PR and would be a separate accessibility issue to file againstUnsavedWarningBanner. Not a blocker here since the banner behavior itself is unchanged.This fix makes the admin create flows consistent with the edit flows. Good housekeeping.
07ae9b61fbto6fffc06c28Review concerns addressed
Concern raised by @felix and @sara (non-blocking):
SubmitFntype defined inline 4 times — twice in each spec file.Fix (commit
2ca8428b): ExtractedSubmitFnto a file-leveltypealias at the top of each spec file, above thedescribeblocks. Removed the 4 inline definitions. The type itself is unchanged — pure structural deduplication.All other reviewer notes (Elicit's
success-result observation, Sara'serror-result note, Leonie'saria-livenote) were explicitly flagged as informational / future work and are not addressed in this PR.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.