fix(frontend): post-review fixes sprint 2
- MitreTechniquePicker: guard sync effect while dropdown open to prevent
mid-stroke wipe when onChange(null,null) propagates back as null props
- SimulationList: replace window.location.href with navigate() to keep
TanStack Query cache intact on row click
- SimulationFormPage: simplify redundant ternary on SOC form onSubmit
- SimulationFormPage: remove dead .replace(' ','T') on executed_at
(backend already returns ISO 8601 with T separator)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,12 +37,16 @@ export function MitreTechniquePicker({
|
|||||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const listRef = useRef<HTMLUListElement>(null);
|
const listRef = useRef<HTMLUListElement>(null);
|
||||||
|
// True once we've synced the first real techniqueId from props (parent/API load).
|
||||||
|
// After that we stop reacting to null, so keystrokes that emit onChange(null,null)
|
||||||
|
// don't propagate back and wipe the input mid-stroke.
|
||||||
|
const hasHydratedFromProps = useRef(false);
|
||||||
|
|
||||||
// Sync display when selection changes from outside (initial load)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (techniqueId && techniqueName) {
|
if (techniqueId && techniqueName) {
|
||||||
setInputValue(`${techniqueId} — ${techniqueName}`);
|
setInputValue(`${techniqueId} — ${techniqueName}`);
|
||||||
} else if (!techniqueId) {
|
hasHydratedFromProps.current = true;
|
||||||
|
} else if (!techniqueId && !hasHydratedFromProps.current) {
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
}
|
}
|
||||||
}, [techniqueId, techniqueName]);
|
}, [techniqueId, techniqueName]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { extractApiError } from '@/api/client';
|
import { extractApiError } from '@/api/client';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useEngagementSimulations } from '@/hooks/useSimulations';
|
import { useEngagementSimulations } from '@/hooks/useSimulations';
|
||||||
@@ -19,6 +19,7 @@ function formatDate(value: string | null): string {
|
|||||||
export function SimulationList({ engagementId }: SimulationListProps): JSX.Element {
|
export function SimulationList({ engagementId }: SimulationListProps): JSX.Element {
|
||||||
const { data, isLoading, isError, error, refetch } = useEngagementSimulations(engagementId);
|
const { data, isLoading, isError, error, refetch } = useEngagementSimulations(engagementId);
|
||||||
const { canEditEngagements } = useAuth();
|
const { canEditEngagements } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (isLoading) return <LoadingState label="Loading simulations…" />;
|
if (isLoading) return <LoadingState label="Loading simulations…" />;
|
||||||
|
|
||||||
@@ -82,14 +83,13 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
|
|||||||
key={sim.id}
|
key={sim.id}
|
||||||
className="border-b border-hairline last:border-0 hover:bg-cloud cursor-pointer"
|
className="border-b border-hairline last:border-0 hover:bg-cloud cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
(window.location.href = `/engagements/${engagementId}/simulations/${sim.id}/edit`)
|
navigate(`/engagements/${engagementId}/simulations/${sim.id}/edit`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<td className="px-xl py-md">
|
<td className="px-xl py-md">
|
||||||
<Link
|
<Link
|
||||||
to={`/engagements/${engagementId}/simulations/${sim.id}/edit`}
|
to={`/engagements/${engagementId}/simulations/${sim.id}/edit`}
|
||||||
className="text-ink font-medium hover:underline"
|
className="text-ink font-medium hover:underline"
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
{sim.name}
|
{sim.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function SimulationFormPage(): JSX.Element {
|
|||||||
description: s.description ?? '',
|
description: s.description ?? '',
|
||||||
commands: s.commands ?? '',
|
commands: s.commands ?? '',
|
||||||
prerequisites: s.prerequisites ?? '',
|
prerequisites: s.prerequisites ?? '',
|
||||||
executed_at: s.executed_at ? s.executed_at.replace(' ', 'T').slice(0, 16) : '',
|
executed_at: s.executed_at ? s.executed_at.slice(0, 16) : '',
|
||||||
execution_result: s.execution_result ?? '',
|
execution_result: s.execution_result ?? '',
|
||||||
});
|
});
|
||||||
setSoc({
|
setSoc({
|
||||||
@@ -117,6 +117,7 @@ export function SimulationFormPage(): JSX.Element {
|
|||||||
const socCanEdit = isSoc && (status === 'review_required' || status === 'done');
|
const socCanEdit = isSoc && (status === 'review_required' || status === 'done');
|
||||||
const socBlocked = isSoc && (status === 'pending' || status === 'in_progress');
|
const socBlocked = isSoc && (status === 'pending' || status === 'in_progress');
|
||||||
|
|
||||||
|
const canSaveSoc = socCanEdit || canEditEngagements;
|
||||||
const rtDisabled = !canEditRT;
|
const rtDisabled = !canEditRT;
|
||||||
const socDisabled = !canEditEngagements && !socCanEdit;
|
const socDisabled = !canEditEngagements && !socCanEdit;
|
||||||
|
|
||||||
@@ -395,7 +396,7 @@ export function SimulationFormPage(): JSX.Element {
|
|||||||
{/* SOC card */}
|
{/* SOC card */}
|
||||||
<form
|
<form
|
||||||
id="soc-form"
|
id="soc-form"
|
||||||
onSubmit={socCanEdit ? onSaveSOC : canEditEngagements ? onSaveSOC : (e) => e.preventDefault()}
|
onSubmit={canSaveSoc ? onSaveSOC : (e) => e.preventDefault()}
|
||||||
noValidate
|
noValidate
|
||||||
className="card-product flex flex-col gap-md"
|
className="card-product flex flex-col gap-md"
|
||||||
>
|
>
|
||||||
@@ -441,7 +442,7 @@ export function SimulationFormPage(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
{(socCanEdit || canEditEngagements) && (
|
{canSaveSoc && (
|
||||||
<div className="flex items-center gap-md pt-sm border-t border-hairline">
|
<div className="flex items-center gap-md pt-sm border-t border-hairline">
|
||||||
<button type="submit" form="soc-form" className="btn-primary" disabled={submitting}>
|
<button type="submit" form="soc-form" className="btn-primary" disabled={submitting}>
|
||||||
{updateMutation.isPending ? 'Saving…' : 'Save SOC'}
|
{updateMutation.isPending ? 'Saving…' : 'Save SOC'}
|
||||||
|
|||||||
Reference in New Issue
Block a user