{finding.attributes.check_metadata.checktitle}
diff --git a/ui/components/findings/table/finding-note-modal.test.tsx b/ui/components/findings/table/finding-note-modal.test.tsx index 73b6a2f61c..4d40c382a3 100644 --- a/ui/components/findings/table/finding-note-modal.test.tsx +++ b/ui/components/findings/table/finding-note-modal.test.tsx @@ -9,6 +9,13 @@ vi.mock("@/components/icons/providers-badge/provider-type-icon", () => ({ ), })); +// CustomLink pulls the "@/lib" barrel (and next-auth with it) into the unit env. +vi.mock("@/components/ui/custom/custom-link", () => ({ + CustomLink: ({ href, children }: { href: string; children: ReactNode }) => ( + {children} + ), +})); + vi.mock("@/components/shadcn/modal", () => ({ Modal: ({ children, @@ -44,7 +51,6 @@ beforeAll(() => { import { FINDING_TRIAGE_DISABLED_REASON, - FINDING_TRIAGE_NOTE_PRIVACY_COPY, FINDING_TRIAGE_STATUS, type FindingTriageDetail, type UpdateFindingTriageInput, @@ -72,7 +78,6 @@ function makeTriageDetail( noteId: "note-1", noteBody: "Existing investigation note", maxNoteLength: 500, - privacyCopy: FINDING_TRIAGE_NOTE_PRIVACY_COPY, ...overrides, }; } @@ -246,7 +251,45 @@ describe("FindingNoteModal", () => { expect(onOpenChange).not.toHaveBeenCalledWith(false); }); - it("should render counter, privacy copy, and cancel/update actions", async () => { + it("should lock the status picker for resolved findings while keeping the note editable", async () => { + // Given + const user = userEvent.setup(); + const onTriageUpdateAction = vi.fn(); + renderNoteModal({ + triage: makeTriageDetail({ + status: FINDING_TRIAGE_STATUS.RESOLVED, + label: "Resolved", + }), + onTriageUpdateAction, + }); + + // Then — automation owns the transition out of Resolved. + expect( + screen.getByRole("combobox", { name: "Triage status" }), + ).toBeDisabled(); + expect( + screen.getByText( + "Triage status is managed automatically once the finding is resolved.", + ), + ).toBeVisible(); + expect(screen.getByLabelText("Note text")).toBeEnabled(); + + // When — the note itself can still be updated. + const textarea = screen.getByLabelText("Note text"); + await user.clear(textarea); + await user.type(textarea, "Documenting the resolution."); + await user.click(screen.getByRole("button", { name: "Save changes" })); + + // Then + expect(onTriageUpdateAction).toHaveBeenCalledWith( + expect.objectContaining({ note: "Documenting the resolution." }), + ); + expect(onTriageUpdateAction).toHaveBeenCalledWith( + expect.not.objectContaining({ status: expect.anything() }), + ); + }); + + it("should render counter and cancel/update actions without privacy copy", async () => { // Given const user = userEvent.setup(); const onOpenChange = vi.fn(); @@ -258,7 +301,9 @@ describe("FindingNoteModal", () => { // Then expect(screen.getByText("3/500")).toBeInTheDocument(); - expect(screen.getByText(FINDING_TRIAGE_NOTE_PRIVACY_COPY)).toBeVisible(); + expect( + screen.queryByText("This note is only visible to your team."), + ).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Cancel" })); expect(onOpenChange).toHaveBeenCalledWith(false); expect( @@ -305,7 +350,11 @@ describe("FindingNoteModal", () => { await user.click(screen.getByRole("option", { name: "Risk Accepted" })); // Then - expect(screen.getByText(/will be muted/i)).toBeVisible(); + expect( + screen.getByText( + "Changing triage to Risk Accepted will mute the finding", + ), + ).toBeVisible(); await waitFor(() => expect(screen.queryByRole("listbox")).not.toBeInTheDocument(), ); diff --git a/ui/components/findings/table/finding-note-modal.tsx b/ui/components/findings/table/finding-note-modal.tsx index 02f1557579..90c9241017 100644 --- a/ui/components/findings/table/finding-note-modal.tsx +++ b/ui/components/findings/table/finding-note-modal.tsx @@ -5,14 +5,24 @@ import { type FormEvent, useRef, useState } from "react"; import { ProviderTypeIcon } from "@/components/icons/providers-badge/provider-type-icon"; import { Alert, AlertDescription, Button, Textarea } from "@/components/shadcn"; import { Modal } from "@/components/shadcn/modal"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip"; import { CloudFeatureBadgeLink } from "@/components/shared/cloud-feature-badge"; +import { CustomLink } from "@/components/ui/custom/custom-link"; +import { DOCS_URLS } from "@/lib/external-urls"; import { FINDING_TRIAGE_DISABLED_REASON, FINDING_TRIAGE_ORIGIN, + FINDING_TRIAGE_RESOLVED_LOCKED_COPY, FINDING_TRIAGE_STATUS, type FindingTriageDetail, type FindingTriageStatus, + getFindingTriageMuteInfoCopy, isMutelistShortcutStatus, + isTriageStatusLocked, } from "@/types/findings-triage"; import type { ProviderType } from "@/types/providers"; @@ -37,10 +47,8 @@ interface FindingNoteModalProps { onTriageUpdateAction?: FindingTriageUpdateHandler; } -const MUTELIST_INFO_COPY = - "This finding will be muted through the existing Mutelist flow."; const REMEDIATING_INFO_COPY = - "Once this finding is fixed and passes in the next scan, it will be automatically changed to Resolved."; + "Once this finding is remediated, if in the following scan its status changes to Pass, it will be automatically changed to Resolved"; export function FindingNoteModal({ open, @@ -68,6 +76,7 @@ export function FindingNoteModal({ isMutelistShortcutStatus(selectedStatus); const shouldShowRemediatingInfo = selectedStatus === FINDING_TRIAGE_STATUS.REMEDIATING; + const isStatusLocked = isTriageStatusLocked(triage.status); // Opened from a dropdown item: move focus into the dialog on mount so Radix's // aria-hidden is not applied to the still-focused dropdown that opened it. const handleOpenAutoFocus = (event: Event) => { @@ -118,7 +127,10 @@ export function FindingNoteModal({ title="Add Triage Note" size="lg" > -