From 707d8de4fbcc4ba4e065ed3afffd9360d04dfaaf Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Sun, 19 Apr 2026 14:49:04 +0000 Subject: [PATCH] UX-001: sidebar re-entry + inline team/owner creation in wizard Closes UX-001 (OnboardingWizard CertificateStep dead-end): users no longer have to navigate away from the wizard and lose their in-flight state when the required Owner/Team dropdowns are empty. Layout.tsx - Adds persistent 'Setup guide' button in the left sidebar. - Clears localStorage 'certctl:onboarding-dismissed' then navigates to /?onboarding=1 as a re-entry signal that overrides dismissal. - localStorage.removeItem wrapped in try/catch to tolerate storage access errors (private browsing, quota, etc.). DashboardPage.tsx - Reads ?onboarding=1 via useSearchParams as a forceOnboarding flag. - forceOnboarding bypasses the latched first-run gate so the wizard reopens even after dismissal or with certs/issuers already present. - onDismiss now also strips ?onboarding=1 via setSearchParams(next, { replace: true }) so a page refresh does not relaunch the wizard. OnboardingWizard.tsx - Adds CreateTeamModalInline and CreateOwnerModalInline inside CertificateStep. Both wire through React Query: createTeam / createOwner mutation on success invalidates ['teams'] / ['owners'] and calls onCreated(id) so the parent select auto-selects the new row as soon as the refetch lands. - '+ New team' and '+ New owner' buttons placed next to the select labels; empty-state copy replaced with inline 'create one now' buttons (no more Link back to /owners /teams). - CreateOwner coerces empty teamId to undefined before mutation so the server contract matches OwnersPage. Tests (12 new, all green; total suite 252 passed / 0 failed): - Layout.test.tsx (4): Setup guide button renders, clicking it clears the dismissal key and navigates to /?onboarding=1, tolerates localStorage.removeItem throwing. - DashboardPage.test.tsx (4): first-run auto-open, ?onboarding=1 re-entry after dismissal, onDismiss writes localStorage + strips the query param, dismissed-with-no-param stays closed. - OnboardingWizard.test.tsx (4): Skip-Skip reaches CertificateStep with '+ New team' / '+ New owner' buttons visible; '+ New team' happy path with React Query invalidation + parent-select auto-select via option-parent traversal (label is a sibling, not htmlFor-linked); '+ New owner' happy path pins team_id: undefined coercion; Cancel abort never mutates. Test infrastructure notes: - Closure-driven vi.fn().mockImplementation pattern drives the post-invalidation refetch: the mutation mock mutates a closure variable that the getTeams/getOwners mock reads, so the parent select's new