From 84f1d33245ba110d66472fcef11ae5ae94d4013d Mon Sep 17 00:00:00 2001 From: Ian Boyes Date: Wed, 8 Jul 2026 17:24:39 -0700 Subject: [PATCH 1/5] fix: scope quick analyze to the clicked sample Quick analyzing a single sample row unioned that sample into the checkbox selection and opened the dialog over the whole selection. The dialog's samples are now held in their own state, set to `[item]` from a row and to the selected samples from the toolbar, so the two paths no longer share `selected`. Collapses the `selectOnQuickAnalyze` / `setOpenQuickAnalyze` prop pair on SampleItem and SampleSelectionToolbar into a single `onQuickAnalyze` callback, and drops the now-dead `onClear` prop and empty-samples effect from QuickAnalyze. Row checkboxes and quick analyze buttons get sample-scoped accessible names, since every row previously exposed the same "checkbox" and "quick analyze" labels. --- .../components/Create/QuickAnalyze.tsx | 14 +--- apps/web/src/base/IconButton.tsx | 5 +- .../src/samples/components/Item/EndIcon.tsx | 5 ++ .../samples/components/Item/SampleItem.tsx | 15 ++-- .../components/SampleSelectionToolbar.tsx | 9 ++- .../src/samples/components/SamplesList.tsx | 45 +++++------ .../src/samples/components/SamplesToolbar.tsx | 25 +++++- .../components/__tests__/SamplesList.test.tsx | 78 ++++++++++++++++++- 8 files changed, 141 insertions(+), 55 deletions(-) diff --git a/apps/web/src/analyses/components/Create/QuickAnalyze.tsx b/apps/web/src/analyses/components/Create/QuickAnalyze.tsx index 1d70f7b36..5377ae966 100644 --- a/apps/web/src/analyses/components/Create/QuickAnalyze.tsx +++ b/apps/web/src/analyses/components/Create/QuickAnalyze.tsx @@ -2,7 +2,6 @@ import { Dialog, DialogTitle } from "@base/Dialog"; import QueryError from "@base/QueryError"; import { useListHmms } from "@hmm/queries"; import type { SampleMinimal } from "@samples/types"; -import { useEffect } from "react"; import HMMAlert from "../HMMAlert"; import CreateAnalysisDialogContent from "./CreateAnalysisDialogContent"; import CreateAnalysisForm from "./CreateAnalysisForm"; @@ -11,16 +10,14 @@ import { getCompatibleWorkflows } from "./workflows"; type QuickAnalyzeProps = { open: boolean; - /** A callback function to clear selected samples */ - onClear: () => void; setOpen: (open: boolean) => void; - /** The selected samples */ + /** The samples to analyze */ samples: SampleMinimal[]; }; /** - * A form for triggering quick analyses on selected samples + * A form for triggering quick analyses on the passed samples */ export default function QuickAnalyze({ open, @@ -29,13 +26,6 @@ export default function QuickAnalyze({ }: QuickAnalyzeProps) { const { data: hmms, isPending, isError } = useListHmms(1, 1, ""); - // The dialog should close when all selected samples have been analyzed and deselected. - useEffect(() => { - if (open && samples.length === 0) { - setOpen(false); - } - }, [open, samples, setOpen]); - if (isError && !hmms) { return ( diff --git a/apps/web/src/base/IconButton.tsx b/apps/web/src/base/IconButton.tsx index 61192830b..fc772fa72 100644 --- a/apps/web/src/base/IconButton.tsx +++ b/apps/web/src/base/IconButton.tsx @@ -4,6 +4,8 @@ import Tooltip from "./Tooltip"; import type { IconColor } from "./types"; export type IconButtonProps = { + /** Accessible name for the button. Defaults to ``tip``. */ + ariaLabel?: string; className?: string; color?: IconColor; IconComponent: LucideIcon; @@ -17,6 +19,7 @@ export type IconButtonProps = { * A styled clickable icon with tooltip describing its action */ export default function IconButton({ + ariaLabel, className, color = "black", IconComponent, @@ -55,7 +58,7 @@ export default function IconButton({ }, className, )} - aria-label={tip} + aria-label={ariaLabel ?? tip} type="button" onClick={onClick} > diff --git a/apps/web/src/samples/components/Item/EndIcon.tsx b/apps/web/src/samples/components/Item/EndIcon.tsx index fbd9933bc..aecc05a22 100644 --- a/apps/web/src/samples/components/Item/EndIcon.tsx +++ b/apps/web/src/samples/components/Item/EndIcon.tsx @@ -5,6 +5,9 @@ import { ChartArea } from "lucide-react"; import { cn } from "@/app/utils"; type SampleItemEndIconProps = { + /** Accessible name for the quick analyze button */ + ariaLabel: string; + /** Progress of the job responsible for creating the sample */ progress: number; @@ -25,6 +28,7 @@ type SampleItemEndIconProps = { * Icon indicating the status of sample */ export default function SampleItemEndIcon({ + ariaLabel, onClick, ready, progress, @@ -40,6 +44,7 @@ export default function SampleItemEndIcon({ return (
void; - /** Callback to handle sample selection on end icon quick analysis */ - selectOnQuickAnalyze: () => void; - setOpenQuickAnalyze: (open: boolean) => void; + /** Callback to open a quick analysis scoped to this sample */ + onQuickAnalyze: () => void; }; /** @@ -31,20 +30,15 @@ export default function SampleItem({ sample, checked, handleSelect, - selectOnQuickAnalyze, - setOpenQuickAnalyze, + onQuickAnalyze, }: SampleItemProps) { - function onQuickAnalyze() { - selectOnQuickAnalyze(); - setOpenQuickAnalyze(true); - } - const { data: job } = useFetchJob(sample.job?.id ?? Number.NaN, sample.job); return (
)} void; + + /** A callback to open a quick analysis scoped to the selected samples */ + onQuickAnalyze: () => void; + /** A list of selected samples */ selected: string[]; - setOpenQuickAnalyze: (open: boolean) => void; }; /** @@ -15,15 +18,15 @@ type SampleSelectionToolbarProps = { */ export default function SampleSelectionToolbar({ onClear, + onQuickAnalyze, selected, - setOpenQuickAnalyze, }: SampleSelectionToolbarProps) { return (
-
diff --git a/apps/web/src/samples/components/SamplesList.tsx b/apps/web/src/samples/components/SamplesList.tsx index 6b790927d..4f521952e 100644 --- a/apps/web/src/samples/components/SamplesList.tsx +++ b/apps/web/src/samples/components/SamplesList.tsx @@ -11,7 +11,7 @@ import { useListIndexes } from "@indexes/queries"; import type { Label } from "@labels/types"; import { useListSamples } from "@samples/queries"; import type { SampleMinimal } from "@samples/types"; -import { intersectionWith, union, xor } from "es-toolkit/array"; +import { intersectionWith, xor } from "es-toolkit/array"; import { FlaskConical, SearchX } from "lucide-react"; import { useState } from "react"; import SampleFilters from "./Filter/SampleFilters"; @@ -54,6 +54,16 @@ export default function SamplesList({ const [selected, setSelected] = useState([]); const [openQuickAnalyze, setOpenQuickAnalyze] = useState(false); + const [quickAnalyzeSamples, setQuickAnalyzeSamples] = useState< + SampleMinimal[] + >([]); + + // Held separately from ``selected`` so a row's quick analyze can ignore the + // checkbox selection, and so the samples outlive the dialog's exit animation. + function openQuickAnalyzeFor(samples: SampleMinimal[]) { + setQuickAnalyzeSamples(samples); + setOpenQuickAnalyze(true); + } if ((isErrorSamples || isErrorIndexes) && !samples) { return ; @@ -65,25 +75,24 @@ export default function SamplesList({ const { items, page, page_count, total_count } = samples; + const selectedSamples = intersectionWith( + items, + selected, + (item, id) => item.id === id, + ); + function renderRow(item: SampleMinimal) { function handleSelect() { setSelected(xor(selected, [item.id])); } - function selectOnQuickAnalyze() { - if (!selected.includes(item.id)) { - setSelected(union(selected, [item.id])); - } - } - return ( openQuickAnalyzeFor([item])} /> ); } @@ -92,13 +101,8 @@ export default function SamplesList({ <> setSelected([])} setOpen={setOpenQuickAnalyze} - samples={intersectionWith( - items, - selected, - (item, id) => item.id === id, - )} + samples={quickAnalyzeSamples} />
setSelected([])} - setOpenQuickAnalyze={setOpenQuickAnalyze} + onQuickAnalyze={() => openQuickAnalyzeFor(selectedSamples)} term={term} onChange={(e) => setSearch({ term: e.target.value })} /> @@ -151,14 +155,7 @@ export default function SamplesList({ )}
{selected.length ? ( - item.id === id, - )} - /> + ) : ( ) => void; + term: string; +}; + +function SampleSearchToolbar({ onChange, term }: SampleSearchToolbarProps) { const { hasPermission: canCreate } = useCheckAdminRoleOrPermission("create_sample"); @@ -26,6 +32,17 @@ function SampleSearchToolbar({ onChange, term }) { ); } +type SampleToolbarProps = SampleSearchToolbarProps & { + /** A callback function to clear selected samples */ + onClear: () => void; + + /** A callback to open a quick analysis scoped to the selected samples */ + onQuickAnalyze: () => void; + + /** A list of selected samples */ + selected: string[]; +}; + /** * A toolbar allowing samples to be filtered by name and to create an analysis for selected samples */ @@ -33,14 +50,14 @@ export default function SampleToolbar({ selected, onClear, onChange, - setOpenQuickAnalyze, + onQuickAnalyze, term, -}) { +}: SampleToolbarProps) { return selected.length ? ( ) : ( diff --git a/apps/web/src/samples/components/__tests__/SamplesList.test.tsx b/apps/web/src/samples/components/__tests__/SamplesList.test.tsx index b2cbe0889..f68171ac7 100644 --- a/apps/web/src/samples/components/__tests__/SamplesList.test.tsx +++ b/apps/web/src/samples/components/__tests__/SamplesList.test.tsx @@ -1,4 +1,4 @@ -import { screen } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { createFakeAccount, mockApiGetAccount } from "@tests/fake/account"; import { createFakeHmmSearchResults, mockApiGetHmms } from "@tests/fake/hmm"; @@ -100,4 +100,80 @@ describe("", () => { screen.queryByRole("link", { name: "Create" }), ).not.toBeInTheDocument(); }); + + describe("quick analyze", () => { + it("should scope to the clicked sample, ignoring the selection", async () => { + await renderWithRouter(, path); + expect(await screen.findByText("Samples")).toBeInTheDocument(); + + const selectedSample = at(samples, 0); + const clickedSample = at(samples, 1); + + await userEvent.click( + screen.getByRole("checkbox", { name: `Select ${selectedSample.name}` }), + ); + await userEvent.click( + screen.getByRole("button", { + name: `Quick analyze ${clickedSample.name}`, + }), + ); + + const dialog = await screen.findByRole("dialog"); + + expect(within(dialog).getByText(clickedSample.name)).toBeInTheDocument(); + expect( + within(dialog).queryByText(selectedSample.name), + ).not.toBeInTheDocument(); + }); + + it("should leave the existing selection intact after a row is analyzed", async () => { + await renderWithRouter(, path); + expect(await screen.findByText("Samples")).toBeInTheDocument(); + + const selectedSample = at(samples, 0); + const clickedSample = at(samples, 1); + + await userEvent.click( + screen.getByRole("checkbox", { name: `Select ${selectedSample.name}` }), + ); + await userEvent.click( + screen.getByRole("button", { + name: `Quick analyze ${clickedSample.name}`, + }), + ); + + expect(await screen.findByRole("dialog")).toBeInTheDocument(); + + await userEvent.keyboard("{Escape}"); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + + expect( + screen.getByRole("checkbox", { name: `Select ${selectedSample.name}` }), + ).toBeChecked(); + expect( + screen.getByRole("checkbox", { name: `Select ${clickedSample.name}` }), + ).not.toBeChecked(); + }); + + it("should include every selected sample when triggered from the toolbar", async () => { + await renderWithRouter(, path); + expect(await screen.findByText("Samples")).toBeInTheDocument(); + + for (const sample of samples) { + await userEvent.click( + screen.getByRole("checkbox", { name: `Select ${sample.name}` }), + ); + } + + await userEvent.click( + screen.getByRole("button", { name: "Quick Analyze" }), + ); + + const dialog = await screen.findByRole("dialog"); + + for (const sample of samples) { + expect(within(dialog).getByText(sample.name)).toBeInTheDocument(); + } + }); + }); }); From a1b84de005b441af9f226af0aa14971fde40e6d8 Mon Sep 17 00:00:00 2001 From: Ian Boyes Date: Wed, 8 Jul 2026 17:30:50 -0700 Subject: [PATCH 2/5] fix: title quick analyze dialog for a single sample A row's quick analyze showed "Selected Samples 1". It now reads "Selected Sample" with no count, while the selection toolbar keeps the plural title and count badge even when only one sample is selected. The two cases are indistinguishable from the sample count alone, so SamplesList tags the dialog's samples with whether they came from the selection. --- .../components/Create/QuickAnalyze.tsx | 6 ++- .../components/Create/SelectedSamples.tsx | 20 +++++++++- .../src/samples/components/SamplesList.tsx | 30 +++++++++++---- .../components/__tests__/SamplesList.test.tsx | 38 +++++++++++++++++++ 4 files changed, 83 insertions(+), 11 deletions(-) diff --git a/apps/web/src/analyses/components/Create/QuickAnalyze.tsx b/apps/web/src/analyses/components/Create/QuickAnalyze.tsx index 5377ae966..c55f041d5 100644 --- a/apps/web/src/analyses/components/Create/QuickAnalyze.tsx +++ b/apps/web/src/analyses/components/Create/QuickAnalyze.tsx @@ -9,6 +9,9 @@ import { SelectedSamples } from "./SelectedSamples"; import { getCompatibleWorkflows } from "./workflows"; type QuickAnalyzeProps = { + /** Whether the samples came from the list selection rather than a single sample */ + fromSelection: boolean; + open: boolean; setOpen: (open: boolean) => void; @@ -20,6 +23,7 @@ type QuickAnalyzeProps = { * A form for triggering quick analyses on the passed samples */ export default function QuickAnalyze({ + fromSelection, open, samples, setOpen, @@ -51,7 +55,7 @@ export default function QuickAnalyze({ Quick Analyze - + - Selected Samples {samples.length} + {fromSelection ? ( + <> + Selected Samples {samples.length} + + ) : ( + "Selected Sample" + )}
([]); const [openQuickAnalyze, setOpenQuickAnalyze] = useState(false); - const [quickAnalyzeSamples, setQuickAnalyzeSamples] = useState< - SampleMinimal[] - >([]); + const [quickAnalyzeTarget, setQuickAnalyzeTarget] = + useState({ fromSelection: false, samples: [] }); // Held separately from ``selected`` so a row's quick analyze can ignore the // checkbox selection, and so the samples outlive the dialog's exit animation. - function openQuickAnalyzeFor(samples: SampleMinimal[]) { - setQuickAnalyzeSamples(samples); + function openQuickAnalyzeFor(target: QuickAnalyzeTarget) { + setQuickAnalyzeTarget(target); setOpenQuickAnalyze(true); } @@ -92,7 +98,9 @@ export default function SamplesList({ sample={item} checked={selected.includes(item.id)} handleSelect={handleSelect} - onQuickAnalyze={() => openQuickAnalyzeFor([item])} + onQuickAnalyze={() => + openQuickAnalyzeFor({ fromSelection: false, samples: [item] }) + } /> ); } @@ -100,9 +108,10 @@ export default function SamplesList({ return ( <>
setSelected([])} - onQuickAnalyze={() => openQuickAnalyzeFor(selectedSamples)} + onQuickAnalyze={() => + openQuickAnalyzeFor({ + fromSelection: true, + samples: selectedSamples, + }) + } term={term} onChange={(e) => setSearch({ term: e.target.value })} /> diff --git a/apps/web/src/samples/components/__tests__/SamplesList.test.tsx b/apps/web/src/samples/components/__tests__/SamplesList.test.tsx index f68171ac7..7e3d6b187 100644 --- a/apps/web/src/samples/components/__tests__/SamplesList.test.tsx +++ b/apps/web/src/samples/components/__tests__/SamplesList.test.tsx @@ -126,6 +126,41 @@ describe("", () => { ).not.toBeInTheDocument(); }); + it("should title a single clicked sample without a count", async () => { + await renderWithRouter(, path); + expect(await screen.findByText("Samples")).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole("button", { + name: `Quick analyze ${at(samples, 0).name}`, + }), + ); + + const dialog = await screen.findByRole("dialog"); + + expect(within(dialog).getByText("Selected Sample")).toBeInTheDocument(); + expect( + within(dialog).queryByText("Selected Samples"), + ).not.toBeInTheDocument(); + }); + + it("should count a single-sample selection from the toolbar", async () => { + await renderWithRouter(, path); + expect(await screen.findByText("Samples")).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole("checkbox", { name: `Select ${at(samples, 0).name}` }), + ); + await userEvent.click( + screen.getByRole("button", { name: "Quick Analyze" }), + ); + + const dialog = await screen.findByRole("dialog"); + + const title = within(dialog).getByText("Selected Samples"); + expect(within(title).getByText("1")).toBeInTheDocument(); + }); + it("should leave the existing selection intact after a row is analyzed", async () => { await renderWithRouter(, path); expect(await screen.findByText("Samples")).toBeInTheDocument(); @@ -171,6 +206,9 @@ describe("", () => { const dialog = await screen.findByRole("dialog"); + const title = within(dialog).getByText("Selected Samples"); + expect(within(title).getByText("2")).toBeInTheDocument(); + for (const sample of samples) { expect(within(dialog).getByText(sample.name)).toBeInTheDocument(); } From 867f358e1e542e4df1961e1220e2b59ae5f3ecd5 Mon Sep 17 00:00:00 2001 From: Ian Boyes Date: Wed, 8 Jul 2026 17:32:25 -0700 Subject: [PATCH 3/5] fix: remove scrollbar from single-sample quick analyze The sample list capped its height and always showed a vertical scrollbar, even when the dialog held one sample. The cap and scrollbar now apply only to a selection, which can outgrow the dialog. --- .../components/Create/SelectedSamples.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/web/src/analyses/components/Create/SelectedSamples.tsx b/apps/web/src/analyses/components/Create/SelectedSamples.tsx index fb9f0d964..09ad8f354 100644 --- a/apps/web/src/analyses/components/Create/SelectedSamples.tsx +++ b/apps/web/src/analyses/components/Create/SelectedSamples.tsx @@ -10,8 +10,8 @@ type SelectedSamplesProps = { /** * Whether the samples came from the list selection rather than a single - * sample. A selection is titled in the plural and counted, even when only one - * sample is in it. + * sample. A selection is titled in the plural, counted even when only one + * sample is in it, and scrolls once it outgrows the dialog. */ fromSelection: boolean; }; @@ -36,14 +36,9 @@ export function SelectedSamples({ )}
{samples.map(({ id, name }) => ( From 160fc859164d68694449dbd3ddff049a1f4db74c Mon Sep 17 00:00:00 2001 From: Ian Boyes Date: Wed, 8 Jul 2026 17:37:40 -0700 Subject: [PATCH 4/5] fix: even out spacing between quick analyze dialog fields The four fields cleared four different bottom margins: 0.5rem under the sample list, 1.5rem under the workflow radios (from Box, which bakes in its own mb-6), 2rem under the subtractions, and a 2rem margin on the reference field clawed back by a -1.5rem margin on the error message. Every field wrapper now owns its spacing and clears 1.5rem. Box's margin is zeroed where it would stack, the reference error hugs its select, and the footer supplies its own padding. Drops the footer's `[&_button]:ml-auto`, which reached for a button that justify-between already right-aligns. --- .../src/analyses/components/Create/CreateAnalysisForm.tsx | 4 ++-- apps/web/src/analyses/components/Create/IndexSelector.tsx | 4 ++-- apps/web/src/analyses/components/Create/SelectedSamples.tsx | 6 +++--- .../src/analyses/components/Create/SubtractionSelector.tsx | 2 +- .../web/src/analyses/components/Create/WorkflowSelector.tsx | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/web/src/analyses/components/Create/CreateAnalysisForm.tsx b/apps/web/src/analyses/components/Create/CreateAnalysisForm.tsx index 51f277cf0..669968086 100644 --- a/apps/web/src/analyses/components/Create/CreateAnalysisForm.tsx +++ b/apps/web/src/analyses/components/Create/CreateAnalysisForm.tsx @@ -134,11 +134,11 @@ export default function CreateAnalysisForm({ rules={{ required: true }} /> - + {errors.indexId && "A reference must be selected"} - + +
Reference {indexes.length ? ( ) : ( - + diff --git a/apps/web/src/analyses/components/Create/SelectedSamples.tsx b/apps/web/src/analyses/components/Create/SelectedSamples.tsx index 09ad8f354..506abc6c7 100644 --- a/apps/web/src/analyses/components/Create/SelectedSamples.tsx +++ b/apps/web/src/analyses/components/Create/SelectedSamples.tsx @@ -25,7 +25,7 @@ export function SelectedSamples({ samples, }: SelectedSamplesProps) { return ( - <> +
{fromSelection ? ( <> @@ -36,7 +36,7 @@ export function SelectedSamples({ )}
@@ -46,6 +46,6 @@ export function SelectedSamples({ ))}
- +
); } diff --git a/apps/web/src/analyses/components/Create/SubtractionSelector.tsx b/apps/web/src/analyses/components/Create/SubtractionSelector.tsx index 82ec6f0d4..f08f2379e 100644 --- a/apps/web/src/analyses/components/Create/SubtractionSelector.tsx +++ b/apps/web/src/analyses/components/Create/SubtractionSelector.tsx @@ -30,7 +30,7 @@ export default function SubtractionSelector({ } return ( -
+
label="Subtractions" items={results} diff --git a/apps/web/src/analyses/components/Create/WorkflowSelector.tsx b/apps/web/src/analyses/components/Create/WorkflowSelector.tsx index 4780e1734..4e2da3974 100644 --- a/apps/web/src/analyses/components/Create/WorkflowSelector.tsx +++ b/apps/web/src/analyses/components/Create/WorkflowSelector.tsx @@ -24,14 +24,14 @@ export default function WorkflowSelector({ onChange, }: WorkflowSelectorProps) { return ( -
+
Workflow - + {workflows.map((workflow) => ( Date: Wed, 8 Jul 2026 17:42:52 -0700 Subject: [PATCH 5/5] feat: describe each workflow in the analysis dialogs Replaces the workflow radio list with the boxed SelectBox picker already used to choose a reference creation method, and gives each workflow a line saying what it finds: Pathoscope finds known viruses, NuVs novel ones. SelectBox no longer bakes in a bottom margin, so a caller can place it without fighting a stacked margin. CreateReference keeps its old spacing through a wrapper. --- .../components/Create/WorkflowSelector.tsx | 42 ++++++------------- .../__tests__/WorkflowSelector.test.tsx | 28 +++++++++---- .../analyses/components/Create/workflows.ts | 3 ++ apps/web/src/base/SelectBox.tsx | 2 +- .../references/components/CreateReference.tsx | 38 +++++++++-------- 5 files changed, 57 insertions(+), 56 deletions(-) diff --git a/apps/web/src/analyses/components/Create/WorkflowSelector.tsx b/apps/web/src/analyses/components/Create/WorkflowSelector.tsx index 4e2da3974..46440d4aa 100644 --- a/apps/web/src/analyses/components/Create/WorkflowSelector.tsx +++ b/apps/web/src/analyses/components/Create/WorkflowSelector.tsx @@ -1,7 +1,4 @@ -import BoxGroup from "@base/BoxGroup"; -import BoxGroupSection from "@base/BoxGroupSection"; -import { RadioGroup, RadioGroupItem } from "@base/RadioGroup"; -import CreateAnalysisFieldTitle from "./CreateAnalysisFieldTitle"; +import { SelectBox, SelectBoxItem } from "@base/SelectBox"; import type { workflow } from "./workflows"; type WorkflowSelectorProps = { @@ -16,7 +13,7 @@ type WorkflowSelectorProps = { }; /** - * A radio group for choosing which analysis workflow to run. + * A boxed picker for choosing which analysis workflow to run. */ export default function WorkflowSelector({ workflows, @@ -25,32 +22,19 @@ export default function WorkflowSelector({ }: WorkflowSelectorProps) { return (
- Workflow - - - {workflows.map((workflow) => ( - - - - - ))} - - + {workflows.map(({ description, id, name }) => ( + +
{name}
+ {description} +
+ ))} +
); } diff --git a/apps/web/src/analyses/components/Create/__tests__/WorkflowSelector.test.tsx b/apps/web/src/analyses/components/Create/__tests__/WorkflowSelector.test.tsx index caeda75a7..329cd10ce 100644 --- a/apps/web/src/analyses/components/Create/__tests__/WorkflowSelector.test.tsx +++ b/apps/web/src/analyses/components/Create/__tests__/WorkflowSelector.test.tsx @@ -6,6 +6,9 @@ import { describe, expect, it } from "vitest"; import WorkflowSelector from "../WorkflowSelector"; import { nuvsWorkflow, pathoscopeWorkflow } from "../workflows"; +const pathoscopeName = `${pathoscopeWorkflow.name} ${pathoscopeWorkflow.description}`; +const nuvsName = `${nuvsWorkflow.name} ${nuvsWorkflow.description}`; + function Harness() { const [selected, setSelected] = useState(pathoscopeWorkflow.id); @@ -23,24 +26,33 @@ describe("", () => { renderWithProviders(); expect(screen.getByRole("radiogroup", { name: "Workflow" })).toBeVisible(); - expect(screen.getByRole("radio", { name: "Pathoscope" })).toBeChecked(); - expect(screen.getByRole("radio", { name: "NuVs" })).not.toBeChecked(); + expect(screen.getByRole("radio", { name: pathoscopeName })).toBeChecked(); + expect(screen.getByRole("radio", { name: nuvsName })).not.toBeChecked(); + }); + + it("describes what each workflow finds", () => { + renderWithProviders(); + + expect(screen.getByText("Find known viruses.")).toBeVisible(); + expect(screen.getByText("Find novel viruses.")).toBeVisible(); }); it("selects a workflow when its option is chosen", async () => { renderWithProviders(); - await userEvent.click(screen.getByRole("radio", { name: "NuVs" })); + await userEvent.click(screen.getByRole("radio", { name: nuvsName })); - expect(screen.getByRole("radio", { name: "NuVs" })).toBeChecked(); - expect(screen.getByRole("radio", { name: "Pathoscope" })).not.toBeChecked(); + expect(screen.getByRole("radio", { name: nuvsName })).toBeChecked(); + expect( + screen.getByRole("radio", { name: pathoscopeName }), + ).not.toBeChecked(); }); - it("selects a workflow when its label is clicked", async () => { + it("selects a workflow when its name is clicked", async () => { renderWithProviders(); - await userEvent.click(screen.getByText("NuVs")); + await userEvent.click(screen.getByText(nuvsWorkflow.name)); - expect(screen.getByRole("radio", { name: "NuVs" })).toBeChecked(); + expect(screen.getByRole("radio", { name: nuvsName })).toBeChecked(); }); }); diff --git a/apps/web/src/analyses/components/Create/workflows.ts b/apps/web/src/analyses/components/Create/workflows.ts index 570c1f34f..4dbb9ba1a 100644 --- a/apps/web/src/analyses/components/Create/workflows.ts +++ b/apps/web/src/analyses/components/Create/workflows.ts @@ -1,14 +1,17 @@ export type workflow = { + description: string; id: string; name: string; }; export const pathoscopeWorkflow = { + description: "Find known viruses.", id: "pathoscope", name: "Pathoscope", }; export const nuvsWorkflow = { + description: "Find novel viruses.", id: "nuvs", name: "NuVs", }; diff --git a/apps/web/src/base/SelectBox.tsx b/apps/web/src/base/SelectBox.tsx index db8943821..0fbdcf687 100644 --- a/apps/web/src/base/SelectBox.tsx +++ b/apps/web/src/base/SelectBox.tsx @@ -21,7 +21,7 @@ export function SelectBox({ const labelId = useId(); return ( -
+
{label} Create Reference - setMode(value as "empty" | "import")} - value={mode} - > - -
Empty
- Start from a blank reference. -
- -
Import
- - Create a reference from a file previously exported from another - Virtool reference. - -
-
+
+ setMode(value as "empty" | "import")} + value={mode} + > + +
Empty
+ Start from a blank reference. +
+ +
Import
+ + Create a reference from a file previously exported from another + Virtool reference. + +
+
+