From 42bfb1363d5abc102c364f50954dd7917ed142f8 Mon Sep 17 00:00:00 2001 From: Nafiz Islam Date: Mon, 31 Aug 2026 15:11:47 -0400 Subject: [PATCH 1/2] SampleAndLibrary to SampleAndLibraryAndIdentity --- .../components/individuals/IndividualAssociatedSamples.tsx | 4 ++-- .../src/components/individuals/IndividualsDetailContent.tsx | 4 ++-- frontend/src/components/labwork/step/LabworkSelection.tsx | 4 ++-- .../src/components/studySamples/StudyStepSamplesTable.tsx | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/individuals/IndividualAssociatedSamples.tsx b/frontend/src/components/individuals/IndividualAssociatedSamples.tsx index 0b6ee619e7..a8636b0c30 100644 --- a/frontend/src/components/individuals/IndividualAssociatedSamples.tsx +++ b/frontend/src/components/individuals/IndividualAssociatedSamples.tsx @@ -4,11 +4,11 @@ import { clearFilters, setFilter, setSortBy } from '../../modules/individualDeta import { FilterDescription, FilterValue, SortBy } from "../../models/paged_items"; import { useAppDispatch } from "../../hooks"; import { IndividualDetails } from "../../modules/individualDetails/models"; -import { SampleAndLibrary } from "../WorkflowSamplesTable/ColumnSets"; +import { SampleAndLibraryAndIdentity } from "../WorkflowSamplesTable/ColumnSets"; import WorkflowSamplesTable from '../WorkflowSamplesTable/WorkflowSamplesTable' interface IndividualAssociatedSamplesProps { - samples: SampleAndLibrary[], + samples: SampleAndLibraryAndIdentity[], individual: IndividualDetails } diff --git a/frontend/src/components/individuals/IndividualsDetailContent.tsx b/frontend/src/components/individuals/IndividualsDetailContent.tsx index 760eb3f786..7a79858b14 100644 --- a/frontend/src/components/individuals/IndividualsDetailContent.tsx +++ b/frontend/src/components/individuals/IndividualsDetailContent.tsx @@ -12,7 +12,7 @@ import IndividualAssociatedSamples from "./IndividualAssociatedSamples"; import { flushIndividualDetails, listTable } from "../../modules/individualDetails/actions"; import { IndividualDetails, IndividualDetailsById } from "../../modules/individualDetails/models"; import { Sample } from "../../models/frontend_models"; -import { SampleAndLibrary } from "../WorkflowSamplesTable/ColumnSets"; +import { SampleAndLibraryAndIdentity } from "../WorkflowSamplesTable/ColumnSets"; const IndividualsDetailContent = () => { const { id } = useParams(); @@ -50,7 +50,7 @@ const IndividualsDetailContent = () => { acc.push({ sample: sample as Sample }) } return acc - }, [] as SampleAndLibrary[]) + }, [] as SampleAndLibraryAndIdentity[]) const title = `Individual ${[id, (individual && individual.individual) ? individual.individual.name : undefined].filter(Boolean).join(' - ')}`; diff --git a/frontend/src/components/labwork/step/LabworkSelection.tsx b/frontend/src/components/labwork/step/LabworkSelection.tsx index aff75f7a53..813d88e15c 100644 --- a/frontend/src/components/labwork/step/LabworkSelection.tsx +++ b/frontend/src/components/labwork/step/LabworkSelection.tsx @@ -6,7 +6,7 @@ import { useAppDispatch, useAppSelector, useSampleAndLibraryList } from "../../. import { Protocol, Step } from "../../../models/frontend_models" import { updateSelectedSamplesAtStep, showSelectionChangedMessage, setSelectedSamples, unselectSamples } from "../../../modules/labworkSteps/actions" import { LabworkStepSamples } from "../../../modules/labworkSteps/models" -import { SampleAndLibrary, getColumnsForStep } from "../../WorkflowSamplesTable/ColumnSets" +import { SampleAndLibraryAndIdentity, getColumnsForStep } from "../../WorkflowSamplesTable/ColumnSets" import WorkflowSamplesTable, { WorkflowSamplesTableProps } from "../../WorkflowSamplesTable/WorkflowSamplesTable" import { SampleColumnID } from "../../samples/SampleTableColumns" import { FMSId } from "../../../models/fms_api_models" @@ -21,7 +21,7 @@ export interface LabworkSelectionProps { setSortByList: WorkflowSamplesTableProps['setSortByList'] } -function samplesAndLibrariesToSampleIDs(sampleAndLibraries: SampleAndLibrary[]) { +function samplesAndLibrariesToSampleIDs(sampleAndLibraries: SampleAndLibraryAndIdentity[]) { return sampleAndLibraries.reduce((sampleIDs, sampleAndLibrary) => { if (sampleAndLibrary.sample) { sampleIDs.push(sampleAndLibrary.sample.id) diff --git a/frontend/src/components/studySamples/StudyStepSamplesTable.tsx b/frontend/src/components/studySamples/StudyStepSamplesTable.tsx index 5a403cb18d..4b06376774 100644 --- a/frontend/src/components/studySamples/StudyStepSamplesTable.tsx +++ b/frontend/src/components/studySamples/StudyStepSamplesTable.tsx @@ -5,7 +5,7 @@ import { Protocol } from '../../models/frontend_models' import { clearFilters, refreshStudySamples, setStudyStepFilter, setStudyStepFilterOptions, setStudyStepPageNumber, setStudyStepPageSize, setStudyStepSortOrder } from '../../modules/studySamples/actions' import { StudySampleStep, StudyStepSamplesTableState, StudyUXStepSettings } from '../../modules/studySamples/models' import { selectProtocolsByID, selectStepsByID } from '../../selectors' -import { SampleAndLibrary, getColumnsForStudySamplesStep } from '../WorkflowSamplesTable/ColumnSets' +import { SampleAndLibraryAndIdentity, getColumnsForStudySamplesStep } from '../WorkflowSamplesTable/ColumnSets' import { LIBRARY_COLUMN_FILTERS, SAMPLE_NEXT_STEP_BY_STUDY_LIBRARY_FILTER_KEYS } from '../libraries/LibraryTableColumns' import { SAMPLE_COLUMN_FILTERS, SAMPLE_NEXT_STEP_BY_STUDY_FILTER_KEYS } from '../samples/SampleTableColumns' import WorkflowSamplesTable from '../WorkflowSamplesTable/WorkflowSamplesTable' @@ -60,7 +60,7 @@ function StudyStepSamplesTable({ studyID, step, tableState, settings }: StudySte title: 'Action', dataIndex: ['sample', 'id'], width: 100, - render: (_: any, { sample }: SampleAndLibrary) => { + render: (_: any, { sample }: SampleAndLibraryAndIdentity) => { return { @@ -82,7 +82,7 @@ function StudyStepSamplesTable({ studyID, step, tableState, settings }: StudySte } }), [dispatch, step.ready.sampleNextStepByID, step.stepID, step.stepName, studyID]) - const columns: IdentifiedTableColumnType[] = useMemo(() => { + const columns: IdentifiedTableColumnType[] = useMemo(() => { if (stepDefinition) { // missing protocol leads to default columns // Same columns as labwork, but we don't want the Project column, since the user // is already in the project details page. From 2e841e2bc549b804ab154dc33d609bdf7f7bf2de Mon Sep 17 00:00:00 2001 From: Nafiz Islam Date: Wed, 2 Sep 2026 09:36:57 -0400 Subject: [PATCH 2/2] npx prettier --write ./src/components/individuals/IndividualAssociatedSamples.tsx ./src/components/individuals/IndividualsDetailContent.tsx ./src/components/labwork/step/LabworkSelection.tsx ./src/components/studySamples/StudyStepSamplesTable.tsx --- .../IndividualAssociatedSamples.tsx | 163 +++++++------- .../individuals/IndividualsDetailContent.tsx | 138 ++++++------ .../labwork/step/LabworkSelection.tsx | 204 +++++++++++------- .../studySamples/StudyStepSamplesTable.tsx | 183 ++++++++++------ 4 files changed, 395 insertions(+), 293 deletions(-) diff --git a/frontend/src/components/individuals/IndividualAssociatedSamples.tsx b/frontend/src/components/individuals/IndividualAssociatedSamples.tsx index a8636b0c30..bbb3d7ad4c 100644 --- a/frontend/src/components/individuals/IndividualAssociatedSamples.tsx +++ b/frontend/src/components/individuals/IndividualAssociatedSamples.tsx @@ -1,94 +1,97 @@ import React, { useMemo, useCallback } from "react" -import { SAMPLE_COLUMN_DEFINITIONS as SAMPLE_COLUMNS, SAMPLE_COLUMN_FILTERS, SampleColumnID } from '../samples/SampleTableColumns'; -import { clearFilters, setFilter, setSortBy } from '../../modules/individualDetails/actions' -import { FilterDescription, FilterValue, SortBy } from "../../models/paged_items"; -import { useAppDispatch } from "../../hooks"; -import { IndividualDetails } from "../../modules/individualDetails/models"; -import { SampleAndLibraryAndIdentity } from "../WorkflowSamplesTable/ColumnSets"; -import WorkflowSamplesTable from '../WorkflowSamplesTable/WorkflowSamplesTable' +import { + SAMPLE_COLUMN_DEFINITIONS as SAMPLE_COLUMNS, + SAMPLE_COLUMN_FILTERS, + SampleColumnID, +} from "../samples/SampleTableColumns" +import { clearFilters, setFilter, setSortBy } from "../../modules/individualDetails/actions" +import { FilterDescription, FilterValue, SortBy } from "../../models/paged_items" +import { useAppDispatch } from "../../hooks" +import { IndividualDetails } from "../../modules/individualDetails/models" +import { SampleAndLibraryAndIdentity } from "../WorkflowSamplesTable/ColumnSets" +import WorkflowSamplesTable from "../WorkflowSamplesTable/WorkflowSamplesTable" interface IndividualAssociatedSamplesProps { - samples: SampleAndLibraryAndIdentity[], - individual: IndividualDetails + samples: SampleAndLibraryAndIdentity[] + individual: IndividualDetails } const IndividualAssociatedSamples = ({ samples, individual }: IndividualAssociatedSamplesProps) => { + const dispatch = useAppDispatch() - const dispatch = useAppDispatch(); + const handleSetFilter = useCallback( + (filterKey: string, value: FilterValue, description: FilterDescription) => { + if (typeof description === "undefined") { + return + } + if (individual.individual) dispatch(setFilter(individual.individual.id, description, value)) + }, + [dispatch], + ) + const handleSetSortBy = useCallback( + (sortBy: SortBy) => { + if (individual.individual) dispatch(setSortBy(individual.individual.id, sortBy)) + }, + [dispatch], + ) + const columnsForSelection = useMemo(() => { + const columns = [ + SAMPLE_COLUMNS.ID, + SAMPLE_COLUMNS.KIND, + SAMPLE_COLUMNS.NAME, + SAMPLE_COLUMNS.PROJECT, + SAMPLE_COLUMNS.CONTAINER_BARCODE, + SAMPLE_COLUMNS.COORDINATES, + SAMPLE_COLUMNS.VOLUME, + SAMPLE_COLUMNS.CONCENTRATION, + SAMPLE_COLUMNS.QC_FLAG, + SAMPLE_COLUMNS.CREATION_DATE, + SAMPLE_COLUMNS.DEPLETED, + ] + return columns + }, [samples]) - const handleSetFilter = useCallback( - (filterKey: string, value: FilterValue, description: FilterDescription) => { - if (typeof description === 'undefined') { - return - } - if (individual.individual) - dispatch(setFilter(individual.individual.id, description, value)) - }, [dispatch] - ) - const handleSetSortBy = useCallback( - (sortBy: SortBy) => { - if (individual.individual) - dispatch(setSortBy(individual.individual.id, sortBy)) - }, - [dispatch] - ) - const columnsForSelection = useMemo(() => { - const columns = [ - SAMPLE_COLUMNS.ID, - SAMPLE_COLUMNS.KIND, - SAMPLE_COLUMNS.NAME, - SAMPLE_COLUMNS.PROJECT, - SAMPLE_COLUMNS.CONTAINER_BARCODE, - SAMPLE_COLUMNS.COORDINATES, - SAMPLE_COLUMNS.VOLUME, - SAMPLE_COLUMNS.CONCENTRATION, - SAMPLE_COLUMNS.QC_FLAG, - SAMPLE_COLUMNS.CREATION_DATE, - SAMPLE_COLUMNS.DEPLETED - ] - return columns - }, [samples]) - - const filterKeys = useMemo(() => { - return { - [SampleColumnID.ID]: 'id', - [SampleColumnID.KIND]: 'derived_samples__sample_kind__name', - [SampleColumnID.NAME]: 'name', - [SampleColumnID.INDIVIDUAL]: 'derived_samples__biosample__individual__name', - [SampleColumnID.CONTAINER_NAME]: 'container__name', - [SampleColumnID.CONTAINER_BARCODE]: 'container__barcode', - [SampleColumnID.COORDINATES]: 'coordinate__name', - [SampleColumnID.VOLUME]: 'volume', - [SampleColumnID.CONCENTRATION]: 'concentration', - [SampleColumnID.CREATION_DATE]: 'creation_date', - [SampleColumnID.DEPLETED]: 'depleted', - [SampleColumnID.QC_FLAG]: 'qc_flag', - [SampleColumnID.PROJECT]: 'derived_by_samples__project__name', - } - }, []) + const filterKeys = useMemo(() => { + return { + [SampleColumnID.ID]: "id", + [SampleColumnID.KIND]: "derived_samples__sample_kind__name", + [SampleColumnID.NAME]: "name", + [SampleColumnID.INDIVIDUAL]: "derived_samples__biosample__individual__name", + [SampleColumnID.CONTAINER_NAME]: "container__name", + [SampleColumnID.CONTAINER_BARCODE]: "container__barcode", + [SampleColumnID.COORDINATES]: "coordinate__name", + [SampleColumnID.VOLUME]: "volume", + [SampleColumnID.CONCENTRATION]: "concentration", + [SampleColumnID.CREATION_DATE]: "creation_date", + [SampleColumnID.DEPLETED]: "depleted", + [SampleColumnID.QC_FLAG]: "qc_flag", + [SampleColumnID.PROJECT]: "derived_by_samples__project__name", + } + }, []) - const filterDefinitions = useMemo(() => { - return { ...SAMPLE_COLUMN_FILTERS } - }, []) + const filterDefinitions = useMemo(() => { + return { ...SAMPLE_COLUMN_FILTERS } + }, []) - const localClearFilters = () => { - if (clearFilters && individual.individual) - dispatch(clearFilters(individual.individual.id)) - } + const localClearFilters = () => { + if (clearFilters && individual.individual) dispatch(clearFilters(individual.individual.id)) + } - return <> - + return ( + <> + + ) } -export default IndividualAssociatedSamples \ No newline at end of file +export default IndividualAssociatedSamples diff --git a/frontend/src/components/individuals/IndividualsDetailContent.tsx b/frontend/src/components/individuals/IndividualsDetailContent.tsx index 7a79858b14..7783198e78 100644 --- a/frontend/src/components/individuals/IndividualsDetailContent.tsx +++ b/frontend/src/components/individuals/IndividualsDetailContent.tsx @@ -1,78 +1,78 @@ -import React, { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import { Tabs } from 'antd' -import useHashURL from '../../hooks/useHashURL' -import AppPageHeader from "../AppPageHeader"; -import PageContent from "../PageContent"; -import EditButton from "../EditButton"; -import IndividualOverview from "./IndividualOverview"; -import { useAppDispatch, useAppSelector } from "../../hooks"; -import { selectIndividualsDetailsById } from "../../selectors"; -import IndividualAssociatedSamples from "./IndividualAssociatedSamples"; -import { flushIndividualDetails, listTable } from "../../modules/individualDetails/actions"; -import { IndividualDetails, IndividualDetailsById } from "../../modules/individualDetails/models"; -import { Sample } from "../../models/frontend_models"; -import { SampleAndLibraryAndIdentity } from "../WorkflowSamplesTable/ColumnSets"; +import React, { useEffect, useState } from "react" +import { useParams } from "react-router-dom" +import { Tabs } from "antd" +import useHashURL from "../../hooks/useHashURL" +import AppPageHeader from "../AppPageHeader" +import PageContent from "../PageContent" +import EditButton from "../EditButton" +import IndividualOverview from "./IndividualOverview" +import { useAppDispatch, useAppSelector } from "../../hooks" +import { selectIndividualsDetailsById } from "../../selectors" +import IndividualAssociatedSamples from "./IndividualAssociatedSamples" +import { flushIndividualDetails, listTable } from "../../modules/individualDetails/actions" +import { IndividualDetails, IndividualDetailsById } from "../../modules/individualDetails/models" +import { Sample } from "../../models/frontend_models" +import { SampleAndLibraryAndIdentity } from "../WorkflowSamplesTable/ColumnSets" const IndividualsDetailContent = () => { - const { id } = useParams(); - const dispatch = useAppDispatch() - const individualDetailsById: IndividualDetailsById = useAppSelector(selectIndividualsDetailsById) + const { id } = useParams() + const dispatch = useAppDispatch() + const individualDetailsById: IndividualDetailsById = useAppSelector(selectIndividualsDetailsById) - const [activeKey, setActiveKey] = useHashURL('overview') - const [individual, setIndividual] = useState() + const [activeKey, setActiveKey] = useHashURL("overview") + const [individual, setIndividual] = useState() - useEffect(() => { - dispatch(listTable(Number(id))); - }, [id, dispatch]) + useEffect(() => { + dispatch(listTable(Number(id))) + }, [id, dispatch]) - useEffect(() => { - // Flush the labwork state when the user navigates away from the - // the labwork section. - return () => { - dispatch(flushIndividualDetails(Number(id))) - } - }, [dispatch, id]) - useEffect(() => { - if (individualDetailsById[Number(id)]) { - const individualInstance: IndividualDetails = { - ...individualDetailsById[Number(id)] - } - if (individualInstance) { - setIndividual(individualInstance) - } - } - }, [individualDetailsById, id]) + useEffect(() => { + // Flush the labwork state when the user navigates away from the + // the labwork section. + return () => { + dispatch(flushIndividualDetails(Number(id))) + } + }, [dispatch, id]) + useEffect(() => { + if (individualDetailsById[Number(id)]) { + const individualInstance: IndividualDetails = { + ...individualDetailsById[Number(id)], + } + if (individualInstance) { + setIndividual(individualInstance) + } + } + }, [individualDetailsById, id]) - const samples = individual?.samplesByIndividual.items.reduce((acc, sampleID) => { - const sample = individual?.samplesByIndividual.itemsByID[sampleID] - if (sample) { - acc.push({ sample: sample as Sample }) - } - return acc - }, [] as SampleAndLibraryAndIdentity[]) + const samples = individual?.samplesByIndividual.items.reduce((acc, sampleID) => { + const sample = individual?.samplesByIndividual.itemsByID[sampleID] + if (sample) { + acc.push({ sample: sample as Sample }) + } + return acc + }, [] as SampleAndLibraryAndIdentity[]) - const title = - `Individual ${[id, (individual && individual.individual) ? individual.individual.name : undefined].filter(Boolean).join(' - ')}`; + const title = `Individual ${[id, individual && individual.individual ? individual.individual.name : undefined].filter(Boolean).join(" - ")}` - return <> - } /> - - - - - - - { - individual && individual.individual.id && - < IndividualAssociatedSamples samples={samples ?? []} individual={individual} /> - } - - - - ; + return ( + <> + } /> + + + + + + + {individual && individual.individual.id && ( + + )} + + + + + ) +} - -}; - -export default IndividualsDetailContent; +export default IndividualsDetailContent diff --git a/frontend/src/components/labwork/step/LabworkSelection.tsx b/frontend/src/components/labwork/step/LabworkSelection.tsx index 813d88e15c..8fefe2624f 100644 --- a/frontend/src/components/labwork/step/LabworkSelection.tsx +++ b/frontend/src/components/labwork/step/LabworkSelection.tsx @@ -4,10 +4,20 @@ import React, { useState, useCallback, useEffect, useMemo } from "react" import { DEFAULT_SMALL_PAGE_SIZE } from "../../../constants" import { useAppDispatch, useAppSelector, useSampleAndLibraryList } from "../../../hooks" import { Protocol, Step } from "../../../models/frontend_models" -import { updateSelectedSamplesAtStep, showSelectionChangedMessage, setSelectedSamples, unselectSamples } from "../../../modules/labworkSteps/actions" +import { + updateSelectedSamplesAtStep, + showSelectionChangedMessage, + setSelectedSamples, + unselectSamples, +} from "../../../modules/labworkSteps/actions" import { LabworkStepSamples } from "../../../modules/labworkSteps/models" -import { SampleAndLibraryAndIdentity, getColumnsForStep } from "../../WorkflowSamplesTable/ColumnSets" -import WorkflowSamplesTable, { WorkflowSamplesTableProps } from "../../WorkflowSamplesTable/WorkflowSamplesTable" +import { + SampleAndLibraryAndIdentity, + getColumnsForStep, +} from "../../WorkflowSamplesTable/ColumnSets" +import WorkflowSamplesTable, { + WorkflowSamplesTableProps, +} from "../../WorkflowSamplesTable/WorkflowSamplesTable" import { SampleColumnID } from "../../samples/SampleTableColumns" import { FMSId } from "../../../models/fms_api_models" import { selectCurrentPreferences } from "../../../modules/profiles/selectors" @@ -15,97 +25,129 @@ import { selectCurrentPreferences } from "../../../modules/profiles/selectors" const { Text } = Typography export interface LabworkSelectionProps { - stepSamples: LabworkStepSamples - step: Step - protocol: Protocol | undefined - setSortByList: WorkflowSamplesTableProps['setSortByList'] + stepSamples: LabworkStepSamples + step: Step + protocol: Protocol | undefined + setSortByList: WorkflowSamplesTableProps["setSortByList"] } function samplesAndLibrariesToSampleIDs(sampleAndLibraries: SampleAndLibraryAndIdentity[]) { - return sampleAndLibraries.reduce((sampleIDs, sampleAndLibrary) => { - if (sampleAndLibrary.sample) { - sampleIDs.push(sampleAndLibrary.sample.id) - } - return sampleIDs - }, [] as FMSId[]) + return sampleAndLibraries.reduce((sampleIDs, sampleAndLibrary) => { + if (sampleAndLibrary.sample) { + sampleIDs.push(sampleAndLibrary.sample.id) + } + return sampleIDs + }, [] as FMSId[]) } -export function LabworkSelection({stepSamples, step, protocol, setSortByList}: LabworkSelectionProps) { - const dispatch = useAppDispatch() - - const [pageSize, setPageSize] = useState(DEFAULT_SMALL_PAGE_SIZE) - const defaultPageSize = useAppSelector(selectCurrentPreferences)['table.sample.page-limit'] - useEffect(() => { - setPageSize(defaultPageSize) - }, [defaultPageSize]) +export function LabworkSelection({ + stepSamples, + step, + protocol, + setSortByList, +}: LabworkSelectionProps) { + const dispatch = useAppDispatch() - const [pageNumber, setPageNumber] = useState(1) - const totalCount = stepSamples.selectedSamples.items.length + const [pageSize, setPageSize] = useState(DEFAULT_SMALL_PAGE_SIZE) + const defaultPageSize = useAppSelector(selectCurrentPreferences)["table.sample.page-limit"] + useEffect(() => { + setPageSize(defaultPageSize) + }, [defaultPageSize]) - const [samples, loading] = useSampleAndLibraryList(stepSamples.selectedSamples.items, pageSize * (pageNumber - 1), pageSize) + const [pageNumber, setPageNumber] = useState(1) + const totalCount = stepSamples.selectedSamples.items.length - const onChangePageNumber = useCallback((pageNumber: number) => { setPageNumber(pageNumber) }, []) - const onChangePageSize = useCallback((pageSize: number) => { setPageSize(pageSize) }, []) + const [samples, loading] = useSampleAndLibraryList( + stepSamples.selectedSamples.items, + pageSize * (pageNumber - 1), + pageSize, + ) - useEffect(() => { - // order checked automatically - dispatch(updateSelectedSamplesAtStep(step.id, stepSamples.selectedSamples.items)) - }, [dispatch, step.id, stepSamples.selectedSamples.items]) + const onChangePageNumber = useCallback((pageNumber: number) => { + setPageNumber(pageNumber) + }, []) + const onChangePageSize = useCallback((pageSize: number) => { + setPageSize(pageSize) + }, []) - // Columns for selected samples table - const columnsForSelection = useMemo(() => { - const columns = getColumnsForStep(step, protocol) - // Make the Coordinates column sortable. We have to force the sorter to appear since - // the selection table doesn't use column filters - otherwise, WorkflowSamplesTable would - // take care of setting the column sortable. - const coordsColumn = columns.find(col => col.columnID === SampleColumnID.COORDINATES) - if (coordsColumn) { - coordsColumn.sorter = true - coordsColumn.key = SampleColumnID.COORDINATES - coordsColumn.defaultSortOrder = 'ascend' - coordsColumn.sortDirections = ['ascend', 'descend', 'ascend'] - } - return columns - }, [step, protocol]) + useEffect(() => { + // order checked automatically + dispatch(updateSelectedSamplesAtStep(step.id, stepSamples.selectedSamples.items)) + }, [dispatch, step.id, stepSamples.selectedSamples.items]) - const selection: WorkflowSamplesTableProps['selection'] = useMemo(() => ({ - selectedSampleIDs: stepSamples.selectedSamples.items, - onSelectionChanged(selectedSamples) { - const selectedSamplesInPage = samplesAndLibrariesToSampleIDs(selectedSamples) - const unselectedSamples = samplesAndLibrariesToSampleIDs(samples).filter((id) => !selectedSamplesInPage.includes(id)) - dispatch(unselectSamples(step.id, unselectedSamples)) - } - }), [dispatch, samples, step.id, stepSamples.selectedSamples.items]) + // Columns for selected samples table + const columnsForSelection = useMemo(() => { + const columns = getColumnsForStep(step, protocol) + // Make the Coordinates column sortable. We have to force the sorter to appear since + // the selection table doesn't use column filters - otherwise, WorkflowSamplesTable would + // take care of setting the column sortable. + const coordsColumn = columns.find((col) => col.columnID === SampleColumnID.COORDINATES) + if (coordsColumn) { + coordsColumn.sorter = true + coordsColumn.key = SampleColumnID.COORDINATES + coordsColumn.defaultSortOrder = "ascend" + coordsColumn.sortDirections = ["ascend", "descend", "ascend"] + } + return columns + }, [step, protocol]) - return <> - {stepSamples.showSelectionChangedWarning && - dispatch(showSelectionChangedMessage(step.id, false))} - style={{ marginBottom: '1em' }} - /> - } - {/* Selection table does not allow filtering or sorting.*/} - {/* Also, we don't handle pagination for selected samples so we are required to + const selection: WorkflowSamplesTableProps["selection"] = useMemo( + () => ({ + selectedSampleIDs: stepSamples.selectedSamples.items, + onSelectionChanged(selectedSamples) { + const selectedSamplesInPage = samplesAndLibrariesToSampleIDs(selectedSamples) + const unselectedSamples = samplesAndLibrariesToSampleIDs(samples).filter( + (id) => !selectedSamplesInPage.includes(id), + ) + dispatch(unselectSamples(step.id, unselectedSamples)) + }, + }), + [dispatch, samples, step.id, stepSamples.selectedSamples.items], + ) + + return ( + <> + {stepSamples.showSelectionChangedWarning && ( + dispatch(showSelectionChangedMessage(step.id, false))} + style={{ marginBottom: "1em" }} + /> + )} + {/* Selection table does not allow filtering or sorting.*/} + {/* Also, we don't handle pagination for selected samples so we are required to load all of the selected samples and libraries for the table to work. */} - - Samples are automatically sorted by container name and then by coordinate. - - + + + + + Samples are automatically sorted by{" "} + + container name + {" "} + and then by{" "} + + coordinate + + . + + + + ) } -export default LabworkSelection \ No newline at end of file +export default LabworkSelection diff --git a/frontend/src/components/studySamples/StudyStepSamplesTable.tsx b/frontend/src/components/studySamples/StudyStepSamplesTable.tsx index 4b06376774..31206e8cc8 100644 --- a/frontend/src/components/studySamples/StudyStepSamplesTable.tsx +++ b/frontend/src/components/studySamples/StudyStepSamplesTable.tsx @@ -1,19 +1,40 @@ -import React, { useCallback, useMemo } from 'react' -import { useAppDispatch, useAppSelector } from '../../hooks' -import { FMSId } from '../../models/fms_api_models' -import { Protocol } from '../../models/frontend_models' -import { clearFilters, refreshStudySamples, setStudyStepFilter, setStudyStepFilterOptions, setStudyStepPageNumber, setStudyStepPageSize, setStudyStepSortOrder } from '../../modules/studySamples/actions' -import { StudySampleStep, StudyStepSamplesTableState, StudyUXStepSettings } from '../../modules/studySamples/models' -import { selectProtocolsByID, selectStepsByID } from '../../selectors' -import { SampleAndLibraryAndIdentity, getColumnsForStudySamplesStep } from '../WorkflowSamplesTable/ColumnSets' -import { LIBRARY_COLUMN_FILTERS, SAMPLE_NEXT_STEP_BY_STUDY_LIBRARY_FILTER_KEYS } from '../libraries/LibraryTableColumns' -import { SAMPLE_COLUMN_FILTERS, SAMPLE_NEXT_STEP_BY_STUDY_FILTER_KEYS } from '../samples/SampleTableColumns' -import WorkflowSamplesTable from '../WorkflowSamplesTable/WorkflowSamplesTable' -import { FilterDescription, FilterValue, SortBy } from '../../models/paged_items' -import { Popconfirm, Button, notification } from 'antd' -import api from '../../utils/api' -import { IdentifiedTableColumnType } from '../pagedItemsTable/PagedItemsColumns' -import { DEFAULT_SMALL_PAGINATION_LIMIT } from '../../config' +import React, { useCallback, useMemo } from "react" +import { useAppDispatch, useAppSelector } from "../../hooks" +import { FMSId } from "../../models/fms_api_models" +import { Protocol } from "../../models/frontend_models" +import { + clearFilters, + refreshStudySamples, + setStudyStepFilter, + setStudyStepFilterOptions, + setStudyStepPageNumber, + setStudyStepPageSize, + setStudyStepSortOrder, +} from "../../modules/studySamples/actions" +import { + StudySampleStep, + StudyStepSamplesTableState, + StudyUXStepSettings, +} from "../../modules/studySamples/models" +import { selectProtocolsByID, selectStepsByID } from "../../selectors" +import { + SampleAndLibraryAndIdentity, + getColumnsForStudySamplesStep, +} from "../WorkflowSamplesTable/ColumnSets" +import { + LIBRARY_COLUMN_FILTERS, + SAMPLE_NEXT_STEP_BY_STUDY_LIBRARY_FILTER_KEYS, +} from "../libraries/LibraryTableColumns" +import { + SAMPLE_COLUMN_FILTERS, + SAMPLE_NEXT_STEP_BY_STUDY_FILTER_KEYS, +} from "../samples/SampleTableColumns" +import WorkflowSamplesTable from "../WorkflowSamplesTable/WorkflowSamplesTable" +import { FilterDescription, FilterValue, SortBy } from "../../models/paged_items" +import { Popconfirm, Button, notification } from "antd" +import api from "../../utils/api" +import { IdentifiedTableColumnType } from "../pagedItemsTable/PagedItemsColumns" +import { DEFAULT_SMALL_PAGINATION_LIMIT } from "../../config" interface StudyStepSamplesTableProps { studyID: FMSId @@ -22,8 +43,12 @@ interface StudyStepSamplesTableProps { settings?: StudyUXStepSettings } -function StudyStepSamplesTable({ studyID, step, tableState, settings }: StudyStepSamplesTableProps) { - +function StudyStepSamplesTable({ + studyID, + step, + tableState, + settings, +}: StudyStepSamplesTableProps) { const dispatch = useAppDispatch() const protocolsByID = useAppSelector(selectProtocolsByID) const stepsByID = useAppSelector(selectStepsByID) @@ -31,73 +56,96 @@ function StudyStepSamplesTable({ studyID, step, tableState, settings }: StudySte const pageSize = settings?.pageSize ?? DEFAULT_SMALL_PAGINATION_LIMIT const pageNumber = tableState?.pageNumber ?? 1 - const onChangePageNumber = useCallback((pageNumber: number) => { dispatch(setStudyStepPageNumber(studyID, step.stepOrderID, 'ready', pageNumber)) }, [dispatch, studyID, step.stepOrderID]) - const onChangePageSize = useCallback((pageSize: number) => { dispatch(setStudyStepPageSize(studyID, step.stepOrderID, pageSize)) }, [dispatch, studyID, step.stepOrderID]) + const onChangePageNumber = useCallback( + (pageNumber: number) => { + dispatch(setStudyStepPageNumber(studyID, step.stepOrderID, "ready", pageNumber)) + }, + [dispatch, studyID, step.stepOrderID], + ) + const onChangePageSize = useCallback( + (pageSize: number) => { + dispatch(setStudyStepPageSize(studyID, step.stepOrderID, pageSize)) + }, + [dispatch, studyID, step.stepOrderID], + ) const setFilter = useCallback( (filterKey: string, value: FilterValue, description: FilterDescription) => { dispatch(setStudyStepFilter(studyID, step.stepOrderID, description, value)) - } - , [studyID, step, dispatch]) + }, + [studyID, step, dispatch], + ) const setFilterOptions = useCallback( (filterKey: string, propertyName: string, value: boolean, description: FilterDescription) => { - dispatch(setStudyStepFilterOptions(studyID, step.stepOrderID, description, { [propertyName]: value })) - } - , [dispatch, studyID, step]) + dispatch( + setStudyStepFilterOptions(studyID, step.stepOrderID, description, { + [propertyName]: value, + }), + ) + }, + [dispatch, studyID, step], + ) const setSortBy = useCallback( (sortBy: SortBy) => { dispatch(setStudyStepSortOrder(studyID, step.stepOrderID, sortBy)) - } - , [studyID, step, dispatch]) + }, + [studyID, step, dispatch], + ) const protocol: Protocol | undefined = protocolsByID[step.protocolID] const stepDefinition = stepsByID[step.stepID] - const actionColumn = useMemo(() => ({ - columnID: 'Action', - title: 'Action', - dataIndex: ['sample', 'id'], - width: 100, - render: (_: any, { sample }: SampleAndLibraryAndIdentity) => { - return { - if (!sample) return; - const REMOVE_NOTIFICATION_KEY = `StudyStepSamplesTable.remove-${studyID}-${step.stepID}-${sample.id}` - notification.info({ - message: `Removing sample '${sample?.name}' from step '${step.stepName}'`, - key: REMOVE_NOTIFICATION_KEY - }) - await dispatch(api.sampleNextStepByStudy.remove(step.ready.sampleNextStepByID[sample.id])) - await dispatch(refreshStudySamples(studyID)) - notification.destroy(REMOVE_NOTIFICATION_KEY) - }} - disabled={!sample} - placement={'topLeft'} - > - - - } - }), [dispatch, step.ready.sampleNextStepByID, step.stepID, step.stepName, studyID]) + const actionColumn = useMemo( + () => ({ + columnID: "Action", + title: "Action", + dataIndex: ["sample", "id"], + width: 100, + render: (_: any, { sample }: SampleAndLibraryAndIdentity) => { + return ( + { + if (!sample) return + const REMOVE_NOTIFICATION_KEY = `StudyStepSamplesTable.remove-${studyID}-${step.stepID}-${sample.id}` + notification.info({ + message: `Removing sample '${sample?.name}' from step '${step.stepName}'`, + key: REMOVE_NOTIFICATION_KEY, + }) + await dispatch( + api.sampleNextStepByStudy.remove(step.ready.sampleNextStepByID[sample.id]), + ) + await dispatch(refreshStudySamples(studyID)) + notification.destroy(REMOVE_NOTIFICATION_KEY) + }} + disabled={!sample} + placement={"topLeft"} + > + + + ) + }, + }), + [dispatch, step.ready.sampleNextStepByID, step.stepID, step.stepName, studyID], + ) const columns: IdentifiedTableColumnType[] = useMemo(() => { - if (stepDefinition) { // missing protocol leads to default columns + if (stepDefinition) { + // missing protocol leads to default columns // Same columns as labwork, but we don't want the Project column, since the user // is already in the project details page. - return [ - ...getColumnsForStudySamplesStep(stepDefinition, protocol), - actionColumn, - ] + return [...getColumnsForStudySamplesStep(stepDefinition, protocol), actionColumn] } else { return [] } }, [actionColumn, protocol, stepDefinition]) const localClearFilters = useCallback(() => { - if (clearFilters) - dispatch(clearFilters(studyID, step.stepOrderID)) + if (clearFilters) dispatch(clearFilters(studyID, step.stepOrderID)) }, [dispatch, step.stepOrderID, studyID]) return ( @@ -107,15 +155,24 @@ function StudyStepSamplesTable({ studyID, step, tableState, settings }: StudySte samples={step.ready.samples} columns={columns} filterDefinitions={{ ...SAMPLE_COLUMN_FILTERS, ...LIBRARY_COLUMN_FILTERS }} - filterKeys={{ ...SAMPLE_NEXT_STEP_BY_STUDY_FILTER_KEYS, ...SAMPLE_NEXT_STEP_BY_STUDY_LIBRARY_FILTER_KEYS }} + filterKeys={{ + ...SAMPLE_NEXT_STEP_BY_STUDY_FILTER_KEYS, + ...SAMPLE_NEXT_STEP_BY_STUDY_LIBRARY_FILTER_KEYS, + }} filters={settings?.filters ?? {}} setFilter={setFilter} setFilterOptions={setFilterOptions} setSortBy={setSortBy} - pagination={{ pageNumber, pageSize, totalCount: step.ready.count, onChangePageNumber, onChangePageSize }} + pagination={{ + pageNumber, + pageSize, + totalCount: step.ready.count, + onChangePageNumber, + onChangePageSize, + }} loading={tableState?.isFetching ?? true} /> ) } -export default StudyStepSamplesTable \ No newline at end of file +export default StudyStepSamplesTable