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}
/>