mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
fix: make scoring profile creation visible
This commit is contained in:
parent
53c5ba2df3
commit
b6b8acdf07
3 changed files with 102 additions and 21 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(<ScoringProfiles onBack={vi.fn()} />);
|
||||
|
||||
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' })
|
||||
|
|
|
|||
|
|
@ -115,8 +115,14 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) {
|
|||
const [draftMode, setDraftMode] = useState<DraftMode>('edit');
|
||||
const [mobileView, setMobileView] = useState<MobileView>('list');
|
||||
const detailHeadingRef = useRef<HTMLHeadingElement>(null);
|
||||
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||
const selectedProfileButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const shouldFocusListRef = useRef(false);
|
||||
const shouldFocusNameRef = useRef(false);
|
||||
const createOriginRef = useRef<{ activeTab: string; mobileView: MobileView }>({
|
||||
activeTab: 'profiles',
|
||||
mobileView: 'list',
|
||||
});
|
||||
const [evaluationForm, setEvaluationForm] = useState<EvaluationRequest>({
|
||||
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 className="mr-2 h-4 w-4" />
|
||||
Save Profile
|
||||
|
|
@ -462,31 +501,37 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) {
|
|||
Define weighted scorers and a composite strategy.
|
||||
</p>
|
||||
</div>
|
||||
{draftMode === 'edit' && selectedProfile && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
h={48}
|
||||
className="flex-1 sm:flex-none"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDuplicate(selectedProfile)}
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Duplicate
|
||||
</Button>
|
||||
{!selectedProfile.builtIn && (
|
||||
{draftMode === 'create' ? (
|
||||
<Button h={48} variant="outline" size="sm" onClick={handleCancelCreate}>
|
||||
Cancel
|
||||
</Button>
|
||||
) : (
|
||||
selectedProfile && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
h={48}
|
||||
className="flex-1 sm:flex-none"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(selectedProfile)}
|
||||
onClick={() => handleDuplicate(selectedProfile)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Duplicate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!selectedProfile.builtIn && (
|
||||
<Button
|
||||
h={48}
|
||||
className="flex-1 sm:flex-none"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(selectedProfile)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -494,11 +539,13 @@ export function ScoringProfiles({ onBack }: ScoringProfilesProps) {
|
|||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Name</label>
|
||||
<TextInput
|
||||
ref={nameInputRef}
|
||||
aria-label="Profile name"
|
||||
value={draft.name}
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({ ...current, name: event.target.value }))
|
||||
}
|
||||
error={!draft.name.trim() ? 'Profile name is required' : undefined}
|
||||
disabled={draftReadOnly}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue