From b6b8acdf07056b96c1889e06fe298eb80fadb135 Mon Sep 17 00:00:00 2001 From: Brad Groux <3053586+BradGroux@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:45:43 -0500 Subject: [PATCH] fix: make scoring profile creation visible --- CHANGELOG.md | 3 + .../governance-surfaces-mantine.test.tsx | 33 ++++++- .../components/scoring/ScoringProfiles.tsx | 87 ++++++++++++++----- 3 files changed, 102 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eb15590..7fc76a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 preventing undefined length crashes and providing a recoverable load error with Retry instead of replacing the task surface with an error boundary (#936). +- Made **New Profile** open a focused, validated scoring draft from both Profiles + and Score Explorer, with cancel returning to the originating tab without + creating an orphan profile (#943). ## [6.0.0] - 2026-07-24 diff --git a/web/src/__tests__/governance-surfaces-mantine.test.tsx b/web/src/__tests__/governance-surfaces-mantine.test.tsx index ab025007..f96fc20f 100644 --- a/web/src/__tests__/governance-surfaces-mantine.test.tsx +++ b/web/src/__tests__/governance-surfaces-mantine.test.tsx @@ -528,6 +528,35 @@ describe('governance surfaces Mantine migration', () => { expectNoLegacySlots(baseElement); }); + it('opens and cancels a focused scoring profile draft from Score Explorer', async () => { + const user = userEvent.setup(); + const confirmDiscard = vi.spyOn(window, 'confirm').mockReturnValue(true); + renderWithProviders(); + + await user.click(screen.getByRole('tab', { name: /score explorer/i })); + expect(await screen.findByText('Composite Score Trend')).toBeDefined(); + + await user.click(screen.getByRole('button', { name: 'New Profile' })); + + expect(screen.getByRole('tab', { name: 'Profiles' }).getAttribute('aria-selected')).toBe( + 'true' + ); + expect(screen.getByRole('heading', { name: 'New scoring profile' })).toBeDefined(); + const name = screen.getByRole('textbox', { name: 'Profile name' }); + const save = screen.getByRole('button', { name: 'Save Profile' }); + expect(document.activeElement).toBe(name); + expect((save as HTMLButtonElement).disabled).toBe(true); + expect(screen.getByText('Profile name is required')).toBeDefined(); + + await user.type(name, 'Explorer profile'); + expect((save as HTMLButtonElement).disabled).toBe(false); + await user.click(screen.getByRole('button', { name: 'Cancel' })); + + expect(await screen.findByText('Composite Score Trend')).toBeDefined(); + expect(mocks.createScoringProfile).not.toHaveBeenCalled(); + confirmDiscard.mockRestore(); + }); + it('supports compact scoring list and detail flows with unsaved-change protection', async () => { const user = userEvent.setup(); const confirmDiscard = vi.spyOn(window, 'confirm').mockReturnValue(false); @@ -565,7 +594,9 @@ describe('governance surfaces Mantine migration', () => { await user.click(screen.getByRole('button', { name: 'New Profile' })); expect(screen.getByRole('heading', { name: 'New scoring profile' })).toBeDefined(); - await user.type(screen.getByRole('textbox', { name: 'Profile name' }), 'Phone profile'); + const profileName = screen.getByRole('textbox', { name: 'Profile name' }); + expect(document.activeElement).toBe(profileName); + await user.type(profileName, 'Phone profile'); await user.click(screen.getByRole('button', { name: 'Save Profile' })); expect(mocks.createScoringProfile).toHaveBeenCalledWith( expect.objectContaining({ name: 'Phone profile' }) diff --git a/web/src/components/scoring/ScoringProfiles.tsx b/web/src/components/scoring/ScoringProfiles.tsx index bdda8903..0d847a23 100644 --- a/web/src/components/scoring/ScoringProfiles.tsx +++ b/web/src/components/scoring/ScoringProfiles.tsx @@ -115,8 +115,14 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) { const [draftMode, setDraftMode] = useState('edit'); const [mobileView, setMobileView] = useState('list'); const detailHeadingRef = useRef(null); + const nameInputRef = useRef(null); const selectedProfileButtonRef = useRef(null); const shouldFocusListRef = useRef(false); + const shouldFocusNameRef = useRef(false); + const createOriginRef = useRef<{ activeTab: string; mobileView: MobileView }>({ + activeTab: 'profiles', + mobileView: 'list', + }); const [evaluationForm, setEvaluationForm] = useState({ profileId: '', action: '', @@ -151,6 +157,12 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) { }, [profiles, selectedProfileId]); useEffect(() => { + if (activeTab === 'profiles' && shouldFocusNameRef.current) { + shouldFocusNameRef.current = false; + nameInputRef.current?.focus(); + return; + } + if (mobileView === 'detail') { detailHeadingRef.current?.focus(); return; @@ -160,7 +172,7 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) { shouldFocusListRef.current = false; selectedProfileButtonRef.current?.focus(); } - }, [mobileView, selectedProfileId]); + }, [activeTab, mobileView, selectedProfileId]); const loadProfileIntoDraft = (profile: ScoringProfile) => { if (!confirmDiscardChanges()) return; @@ -175,13 +187,35 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) { const handleCreateNew = () => { if (!confirmDiscardChanges()) return; + createOriginRef.current = { activeTab, mobileView }; const nextDraft = createEmptyDraft(); setDraft(nextDraft); setCleanDraft(nextDraft); setDraftMode('create'); + shouldFocusNameRef.current = true; + setActiveTab('profiles'); setMobileView('detail'); }; + const handleCancelCreate = () => { + if (!confirmDiscardChanges()) return; + + if (selectedProfile) { + const nextDraft = profileToDraft(selectedProfile); + setDraft(nextDraft); + setCleanDraft(nextDraft); + setDraftMode('edit'); + } else { + const nextDraft = createEmptyDraft(); + setDraft(nextDraft); + setCleanDraft(nextDraft); + setDraftMode('create'); + } + + setActiveTab(createOriginRef.current.activeTab); + setMobileView(createOriginRef.current.mobileView); + }; + const handleDuplicate = (profile: ScoringProfile) => { if (!confirmDiscardChanges()) return; setDraft({ @@ -351,7 +385,12 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) { h={48} className="w-full flex-1 sm:w-auto sm:flex-none" onClick={handleSave} - disabled={draftReadOnly || createProfile.isPending || updateProfile.isPending} + disabled={ + !draft.name.trim() || + draftReadOnly || + createProfile.isPending || + updateProfile.isPending + } > Save Profile @@ -462,31 +501,37 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) { Define weighted scorers and a composite strategy.

- {draftMode === 'edit' && selectedProfile && ( -
- - {!selectedProfile.builtIn && ( + {draftMode === 'create' ? ( + + ) : ( + selectedProfile && ( +
- )} -
+ {!selectedProfile.builtIn && ( + + )} +
+ ) )} @@ -494,11 +539,13 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) {
setDraft((current) => ({ ...current, name: event.target.value })) } + error={!draft.name.trim() ? 'Profile name is required' : undefined} disabled={draftReadOnly} />