diff --git a/packages/common-ui/lib/api-client/useQuery.tsx b/packages/common-ui/lib/api-client/useQuery.tsx index 907496189..f0ae23dc9 100644 --- a/packages/common-ui/lib/api-client/useQuery.tsx +++ b/packages/common-ui/lib/api-client/useQuery.tsx @@ -93,8 +93,6 @@ export function useQuery( ); } - await onSuccess?.(response); - if (joinSpecs) { const { data } = response; const resources = isArray(data) ? data : [data]; @@ -104,6 +102,8 @@ export function useQuery( } } + await onSuccess?.(response); + return response; } diff --git a/packages/dina-ui/components/button-bar/nav/nav.tsx b/packages/dina-ui/components/button-bar/nav/nav.tsx index e4e3106ae..46f08b76e 100644 --- a/packages/dina-ui/components/button-bar/nav/nav.tsx +++ b/packages/dina-ui/components/button-bar/nav/nav.tsx @@ -380,6 +380,11 @@ function NavSequenceDropdown({ formatMessage }) { + + + + + diff --git a/packages/dina-ui/components/collection/VocabularySelectField.tsx b/packages/dina-ui/components/collection/VocabularySelectField.tsx index b42511acc..853c3d349 100644 --- a/packages/dina-ui/components/collection/VocabularySelectField.tsx +++ b/packages/dina-ui/components/collection/VocabularySelectField.tsx @@ -6,6 +6,7 @@ import { useDinaIntl } from "../../intl/dina-ui-intl"; import useVocabularyOptions from "./useVocabularyOptions"; import { IdentifierType } from "packages/dina-ui/types/collection-api/resources/IdentifierType"; import { startCase } from "lodash"; +import { boolean } from "zod"; export interface VocabularySelectFieldProps extends FieldWrapperProps { path: string; @@ -13,6 +14,7 @@ export interface VocabularySelectFieldProps extends FieldWrapperProps { selectProps?: Partial< CreatableProps> >; + isDisabled?: boolean; } export interface VocabularyOption { @@ -28,6 +30,7 @@ export function VocabularySelectField({ path, selectProps, isMulti, + isDisabled, ...labelWrapperProps }: VocabularySelectFieldProps) { const { formatMessage } = useDinaIntl(); @@ -60,6 +63,7 @@ export function VocabularySelectField({ return (
+ isDisabled={isDisabled} isClearable={true} options={vocabOptions} isLoading={loading} diff --git a/packages/dina-ui/components/molecular-analysis/useMetagenomicsWorkflowMolecularAnalysisRun.tsx b/packages/dina-ui/components/molecular-analysis/useMetagenomicsWorkflowMolecularAnalysisRun.tsx new file mode 100644 index 000000000..44400eebe --- /dev/null +++ b/packages/dina-ui/components/molecular-analysis/useMetagenomicsWorkflowMolecularAnalysisRun.tsx @@ -0,0 +1,541 @@ +import { PcrBatchItem, SeqReaction } from "../../types/seqdb-api"; +import { MolecularAnalysisRunItem } from "../../types/seqdb-api/resources/MolecularAnalysisRunItem"; +import { useEffect, useState } from "react"; +import { + BulkGetOptions, + FieldHeader, + filterBy, + rsql, + SaveArgs, + useApiClient, + useQuery, + useStringComparator +} from "common-ui"; +import { StorageUnitUsage } from "../../types/collection-api/resources/StorageUnitUsage"; +import { MolecularAnalysisRun } from "../../types/seqdb-api/resources/MolecularAnalysisRun"; +import { KitsuResource, PersistedResource } from "kitsu"; +import { MaterialSampleSummary } from "../../types/collection-api"; +import { useDinaIntl } from "../../intl/dina-ui-intl"; +import { ColumnDef } from "@tanstack/react-table"; +import Link from "next/link"; +import { attachGenericMolecularAnalysisItems } from "../seqdb/molecular-analysis-workflow/useGenericMolecularAnalysisRun"; +import { GenericMolecularAnalysisItem } from "packages/dina-ui/types/seqdb-api/resources/GenericMolecularAnalysisItem"; +import { MetagenomicsBatchItem } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem"; +import { MetagenomicsBatch } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatch"; +import { getMolecularAnalysisRunColumns } from "./useMolecularAnalysisRun"; + +export interface UseMetagenomicsWorkflowMolecularAnalysisRunProps { + metagenomicsBatchId: string; + metagenomicsBatch: MetagenomicsBatch; + editMode: boolean; + setEditMode: (newValue: boolean) => void; + performSave: boolean; + setPerformSave: (newValue: boolean) => void; +} + +/** + * Represents data to be displayed in the table. + */ +export interface SequencingRunItem { + metagenomicsBatchItem?: MetagenomicsBatchItem; + metagenomicsBatchItemId?: string; + + storageUnitUsage?: StorageUnitUsage; + storageUnitUsageId?: string; + + molecularAnalysisRunItem?: MolecularAnalysisRunItem; + molecularAnalysisRunItemId?: string; + + pcrBatchItem?: PcrBatchItem; + pcrBatchItemId?: string; + + materialSampleSummary?: MaterialSampleSummary; + materialSampleId?: string; +} + +export interface UseMolecularAnalysisRunReturn { + /** + * Used to display if the network calls are still in progress. + */ + loading: boolean; + + /** + * Error message, undefined if no error has occurred. + */ + errorMessage?: string; + + /** + * Only 1 MolecularAnalysisRun should be present for each SeqBatch, if multiple are found this + * will return true and a warning can be displayed in the UI. + */ + multipleRunWarning: boolean; + + /** + * If a sequencing run exists, a name will be returned. Otherwise it will be undefined if not + * created yet. + */ + sequencingRunName?: string; + + /** + * Locally sets the sequencing run name. Changing this does not automatically update the run + * name. Once a save is performed, then it's saved/created. + * + * @param newName New name to use. + */ + setSequencingRunName: (newName: string) => void; + + /** + * Once all the data is loaded in, the contents will be returned to be displayed in the table. + * + * Undefined if no data is available yet. + */ + sequencingRunItems?: SequencingRunItem[]; + + columns: ColumnDef[]; +} + +/** + * Takes an array of MetagenomicsBatchItem, then turns it into the SequencingRunItem which will be + * used to generate more data. + * @param metagenomicsBatchItem + * @returns The initial structure of SequencingRunItem. + */ +export function attachMetagenomicsBatchItem( + metagenomicsBatchItem: PersistedResource[] +): SequencingRunItem[] { + return metagenomicsBatchItem.map((reaction) => { + return { + metagenomicsBatchItem: reaction, + metagenomicsBatchItemId: reaction.id, + molecularAnalysisRunItem: reaction?.molecularAnalysisRunItem, + molecularAnalysisRunItemId: reaction?.molecularAnalysisRunItem?.id, + pcrBatchItemId: reaction?.pcrBatchItem?.id, + pcrBatchItem: reaction?.pcrBatchItem as PcrBatchItem, + materialSampleId: reaction?.pcrBatchItem?.materialSample?.id + }; + }); +} + +/** + * Fetch StorageUnitUsage linked to each SeqReactions. This will perform the API request + * to retrieve the full storage unit since it's stored in the collection-api. + * @returns The updated SeqReactionSample with storage unit attached. + */ +export async function attachStorageUnitUsageMetagenomics( + sequencingRunItem: SequencingRunItem[], + bulkGet: ( + paths: readonly string[], + options?: BulkGetOptions + ) => Promise< + (TReturnNull extends true + ? PersistedResource | null + : PersistedResource)[] + > +): Promise { + const storageUnitUsageQuery = await bulkGet( + sequencingRunItem + .filter((item) => item?.pcrBatchItem?.storageUnitUsage?.id) + .map( + (item) => + "/storage-unit-usage/" + item?.pcrBatchItem?.storageUnitUsage?.id + ), + { apiBaseUrl: "/collection-api" } + ); + + return sequencingRunItem.map((runItem) => { + const queryStorageUnitUsage = storageUnitUsageQuery.find( + (storageUnitUsage) => + storageUnitUsage?.id === runItem?.pcrBatchItem?.storageUnitUsage?.id + ); + return { + ...runItem, + storageUnitUsage: queryStorageUnitUsage as StorageUnitUsage, + storageUnitUsageId: queryStorageUnitUsage?.id + }; + }); +} + +/** + * Fetch MaterialSampleSummary from each PcrBatchItem. + */ +export async function attachMaterialSampleSummaryMetagenomics( + sequencingRunItem: SequencingRunItem[], + bulkGet: ( + paths: readonly string[], + options?: BulkGetOptions + ) => Promise< + (TReturnNull extends true + ? PersistedResource | null + : PersistedResource)[] + > +): Promise { + const materialSampleSummaryQuery = await bulkGet( + sequencingRunItem + .filter((item) => item?.materialSampleId) + .map((item) => "/material-sample-summary/" + item?.materialSampleId), + { apiBaseUrl: "/collection-api" } + ); + + return sequencingRunItem.map((runItem) => { + const queryMaterialSampleSummary = materialSampleSummaryQuery.find( + (materialSample) => materialSample?.id === runItem?.materialSampleId + ); + return { + ...runItem, + materialSampleSummary: queryMaterialSampleSummary as MaterialSampleSummary + }; + }); +} + +/** + * Fetch PcrBatchItem linked to each SeqReactions. + */ +export async function attachPcrBatchItemMetagenomics( + sequencingRunItem: SequencingRunItem[], + bulkGet: ( + paths: readonly string[], + options?: BulkGetOptions + ) => Promise< + (TReturnNull extends true + ? PersistedResource | null + : PersistedResource)[] + > +): Promise { + const pcrBatchItemQuery = await bulkGet( + sequencingRunItem + .filter((item) => item?.pcrBatchItemId) + .map( + (item) => + "/pcr-batch-item/" + + item?.pcrBatchItemId + + "?include=materialSample,storageUnitUsage" + ), + { apiBaseUrl: "/seqdb-api" } + ); + + return sequencingRunItem.map((runItem) => { + const queryPcrBatchItem = pcrBatchItemQuery.find( + (pcrBatchItem) => pcrBatchItem?.id === runItem?.pcrBatchItemId + ); + return { + ...runItem, + pcrBatchItem: queryPcrBatchItem as PcrBatchItem, + materialSampleId: queryPcrBatchItem?.materialSample?.id, + storageUnitUsageId: queryPcrBatchItem?.storageUnitUsage?.id + }; + }); +} + +export function useMetagenomicsWorkflowMolecularAnalysisRun({ + metagenomicsBatchId, + metagenomicsBatch, + editMode, + setEditMode, + performSave, + setPerformSave +}: UseMetagenomicsWorkflowMolecularAnalysisRunProps): UseMolecularAnalysisRunReturn { + const { bulkGet, save } = useApiClient(); + const { formatMessage } = useDinaIntl(); + const { compareByStringAndNumber } = useStringComparator(); + const columns = getMolecularAnalysisRunColumns( + compareByStringAndNumber, + "metagenomics-batch-item" + ); + + // Used to display if the network calls are still in progress. + const [loading, setLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(); + const [automaticEditMode, setAutomaticEditMode] = useState(false); + const [multipleRunWarning, setMultipleRunWarning] = useState(false); + const [sequencingRunName, setSequencingRunName] = useState(); + + // Sequencing run, contains the name. + const [sequencingRun, setSequencingRun] = useState(); + + // Run Items + const [sequencingRunItems, setSequencingRunItems] = + useState(); + + // Network Requests, starting with the SeqReaction + useQuery( + { + filter: filterBy([], { + extraFilters: [ + { + selector: "metagenomicsBatch.uuid", + comparison: "==", + arguments: metagenomicsBatchId + } + ] + })(""), + page: { limit: 1000 }, + path: `seqdb-api/metagenomics-batch-item`, + include: + "indexI5,indexI7,pcrBatchItem,molecularAnalysisRunItem,molecularAnalysisRunItem.run" + }, + { + onSuccess: async ({ data: metagenomicsBatchItems }) => { + /** + * Go through each of the MetagenomicsBatchItems and retrieve the Molecular Analysis Run. There + * should only be one for a set of MetagenomicsBatchItems + * + * If multiple are found, the first found is returned and a warning will be displayed + * to the user. + */ + async function findMolecularAnalysisRun( + sequencingRunItem: SequencingRunItem[] + ) { + if ( + !sequencingRunItem.some( + (item) => item?.molecularAnalysisRunItem?.run?.id + ) + ) { + // Nothing to attach. + return; + } + + // Extract unique run IDs + const uniqueRunIds = new Set( + sequencingRunItem + .filter((item) => item?.molecularAnalysisRunItem?.run?.id) + .map((item) => item?.molecularAnalysisRunItem?.run?.id) + ); + if (uniqueRunIds.size === 0) { + // Nothing to attach. + return; + } + + // Multiple exist, display the warning. + if (uniqueRunIds.size > 1) { + setMultipleRunWarning(true); + } + + const firstSequencingRun = sequencingRunItem.find( + (item) => + item?.molecularAnalysisRunItem?.run?.id === [...uniqueRunIds][0] + )?.molecularAnalysisRunItem?.run; + if (firstSequencingRun) { + setSequencingRun(firstSequencingRun); + setSequencingRunName(firstSequencingRun.name); + } + } + + // Chain it all together to create one object. + let sequencingRunItemsChain = attachMetagenomicsBatchItem( + metagenomicsBatchItems + ); + sequencingRunItemsChain = await attachPcrBatchItemMetagenomics( + sequencingRunItemsChain, + bulkGet + ); + sequencingRunItemsChain = await attachStorageUnitUsageMetagenomics( + sequencingRunItemsChain, + bulkGet + ); + + sequencingRunItemsChain = await attachMaterialSampleSummaryMetagenomics( + sequencingRunItemsChain, + bulkGet + ); + await findMolecularAnalysisRun(sequencingRunItemsChain); + + // All finished loading. + setSequencingRunItems(sequencingRunItemsChain); + setLoading(false); + } + } + ); + + // After loaded, check if we should automatically switch to edit mode. + useEffect(() => { + if ( + loading === false && + !sequencingRunItems?.some((item) => item.molecularAnalysisRunItemId) && + !editMode && + !automaticEditMode + ) { + setEditMode(true); + setAutomaticEditMode(true); + } + }, [sequencingRunItems, loading, editMode]); + + // Reset error messages between edit modes. + useEffect(() => { + setErrorMessage(undefined); + }, [editMode]); + + async function createNewRun() { + if (!sequencingRunItems || sequencingRunItems.length === 0) { + return; + } + + try { + // Retrieve the group name from the seqReaction. + const groupName = metagenomicsBatch?.group; + + // Create a new molecular analysis run. + const molecularAnalysisRunSaveArg: SaveArgs[] = [ + { + type: "molecular-analysis-run", + resource: { + type: "molecular-analysis-run", + name: sequencingRunName, + group: groupName + } + } + ]; + const savedMolecularAnalysisRun = await save( + molecularAnalysisRunSaveArg, + { + apiBaseUrl: "/seqdb-api" + } + ); + + // Create a MolecularAnalysisRunitem for each MetagenomicsBatchItem. + const molecularAnalysisRunItemSaveArgs: SaveArgs[] = + sequencingRunItems.map(() => ({ + type: "molecular-analysis-run-item", + resource: { + type: "molecular-analysis-run-item", + usageType: "metagenomics-batch-item", + relationships: { + run: { + data: { + id: savedMolecularAnalysisRun[0].id, + type: "molecular-analysis-run" + } + } + } + } as any + })); + const savedMolecularAnalysisRunItem = await save( + molecularAnalysisRunItemSaveArgs, + { apiBaseUrl: "/seqdb-api" } + ); + + // Update the existing MetagenomicsBatchItems. + const metagenomicsBatchItemsSaveArgs: SaveArgs[] = + sequencingRunItems.map((item, index) => ({ + type: "metagenomics-batch-item", + resource: { + type: "metagenomics-batch-item", + id: item.metagenomicsBatchItemId, + relationships: { + molecularAnalysisRunItem: { + data: { + id: savedMolecularAnalysisRunItem[index].id, + type: "molecular-analysis-run-item" + } + } + } + } + })); + await save(metagenomicsBatchItemsSaveArgs, { + apiBaseUrl: "/seqdb-api" + }); + + // Update the sequencing run items state. + setSequencingRunItems( + sequencingRunItems.map((item, index) => ({ + ...item, + molecularAnalysisRunItemId: savedMolecularAnalysisRunItem[index].id, + molecularAnalysisRunItem: savedMolecularAnalysisRunItem[ + index + ] as MolecularAnalysisRunItem + })) + ); + + // Go back to view mode once completed. + setPerformSave(false); + setEditMode(false); + setLoading(false); + } catch (error) { + console.error("Error creating a new sequencing run: ", error); + setPerformSave(false); + setLoading(false); + setErrorMessage( + "Error creating a new sequencing run: " + error.toString() + ); + } + } + + async function updateSequencingName() { + // Sequencing run needs an id to update. + if (!sequencingRun?.id) { + setPerformSave(false); + setLoading(false); + setErrorMessage(formatMessage("sangerRunStep_missingSequencingRunID")); + return; + } + + try { + // Update the existing molecular analysis run. + const molecularAnalysisRunSaveArg: SaveArgs[] = [ + { + type: "molecular-analysis-run", + resource: { + id: sequencingRun.id, + type: "molecular-analysis-run", + name: sequencingRunName + } + } + ]; + await save(molecularAnalysisRunSaveArg, { + apiBaseUrl: "/seqdb-api" + }); + + // Go back to view mode once completed. + setPerformSave(false); + setEditMode(false); + setLoading(false); + } catch (error) { + console.error("Error updating sequencing run: ", error); + setPerformSave(false); + setLoading(false); + setErrorMessage("Error updating sequencing run: " + error.toString()); + } + } + + // Handle saving + useEffect(() => { + if (performSave && !loading && editMode) { + setLoading(true); + setErrorMessage(undefined); + + // There must be sequencingRunItems to generate. + if (!sequencingRunItems || sequencingRunItems.length === 0) { + setPerformSave(false); + setLoading(false); + setErrorMessage( + formatMessage("sangerRunStep_missingSequenceReactions") + ); + return; + } + + // Ensure the sequencing name is valid. + if (!sequencingRunName || sequencingRunName.length === 0) { + setPerformSave(false); + setLoading(false); + setErrorMessage(formatMessage("sangerRunStep_invalidRunName")); + return; + } + + // Determine if a new run should be created or update the existing one. + if (sequencingRun) { + updateSequencingName(); + } else { + createNewRun(); + } + } + }, [performSave, loading]); + + return { + loading, + errorMessage, + multipleRunWarning, + sequencingRunName, + setSequencingRunName, + sequencingRunItems, + columns + }; +} diff --git a/packages/dina-ui/components/molecular-analysis/useMolecularAnalysisRun.tsx b/packages/dina-ui/components/molecular-analysis/useMolecularAnalysisRun.tsx index db4b880ca..6da116172 100644 --- a/packages/dina-ui/components/molecular-analysis/useMolecularAnalysisRun.tsx +++ b/packages/dina-ui/components/molecular-analysis/useMolecularAnalysisRun.tsx @@ -20,6 +20,13 @@ import { ColumnDef } from "@tanstack/react-table"; import Link from "next/link"; import { attachGenericMolecularAnalysisItems } from "../seqdb/molecular-analysis-workflow/useGenericMolecularAnalysisRun"; import { GenericMolecularAnalysisItem } from "packages/dina-ui/types/seqdb-api/resources/GenericMolecularAnalysisItem"; +import { MetagenomicsBatchItem } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem"; +import { + attachMaterialSampleSummaryMetagenomics, + attachMetagenomicsBatchItem, + attachPcrBatchItemMetagenomics, + attachStorageUnitUsageMetagenomics +} from "./useMetagenomicsWorkflowMolecularAnalysisRun"; export interface UseMolecularAnalysisRunProps { seqBatchId: string; @@ -574,6 +581,22 @@ export function useMolecularAnalysisRunView({ } return genericMolecularAnalysisItems; } + async function fetchMetagenomicsBatchItems() { + const fetchPaths = molecularAnalysisRunItems.map( + (molecularAnalysisRunItem) => + `seqdb-api/metagenomics-batch-item?include=pcrBatchItem&filter[rsql]=molecularAnalysisRunItem.uuid==${molecularAnalysisRunItem.id}` + ); + const metagenomicsBatchItems: PersistedResource[] = + []; + for (const path of fetchPaths) { + const metagenomicsBatchItem = await apiClient.get< + MetagenomicsBatchItem[] + >(path, {}); + metagenomicsBatchItems.push(metagenomicsBatchItem.data[0]); + } + return metagenomicsBatchItems; + } + const usageType = molecularAnalysisRunItems?.[0].usageType; setColumns( getMolecularAnalysisRunColumns(compareByStringAndNumber, usageType) @@ -617,6 +640,30 @@ export function useMolecularAnalysisRunView({ // All finished loading. setSequencingRunItems(sequencingRunItemsChain); setLoading(false); + } else if (usageType === "metagenomics-batch-item") { + const metagenomicsBatchItems = await fetchMetagenomicsBatchItems(); + + // Chain it all together to create one object. + let sequencingRunItemsChain = attachMetagenomicsBatchItem( + metagenomicsBatchItems + ); + sequencingRunItemsChain = await attachPcrBatchItemMetagenomics( + sequencingRunItemsChain, + bulkGet + ); + sequencingRunItemsChain = await attachStorageUnitUsageMetagenomics( + sequencingRunItemsChain, + bulkGet + ); + sequencingRunItemsChain = + await attachMaterialSampleSummaryMetagenomics( + sequencingRunItemsChain, + bulkGet + ); + + // All finished loading. + setSequencingRunItems(sequencingRunItemsChain); + setLoading(false); } } } @@ -782,7 +829,8 @@ export function getMolecularAnalysisRunColumns(compareByStringAndNumber, type) { ]; const MOLECULAR_ANALYSIS_RUN_COLUMNS_MAP = { "seq-reaction": SEQ_REACTION_COLUMNS, - "generic-molecular-analysis-item": GENERIC_MOLECULAR_ANALYSIS_COLUMNS + "generic-molecular-analysis-item": GENERIC_MOLECULAR_ANALYSIS_COLUMNS, + "metagenomics-batch-item": GENERIC_MOLECULAR_ANALYSIS_COLUMNS }; return MOLECULAR_ANALYSIS_RUN_COLUMNS_MAP[type]; } diff --git a/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsBatchDetailsStep.tsx b/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsBatchDetailsStep.tsx new file mode 100644 index 000000000..cc6df6c70 --- /dev/null +++ b/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsBatchDetailsStep.tsx @@ -0,0 +1,217 @@ +import { + DateField, + DinaForm, + DinaFormSubmitParams, + filterBy, + ResourceSelectField, + SaveArgs, + SubmitButton, + TextField, + useAccount, + useApiClient, + useDinaFormContext +} from "common-ui"; +import { PersistedResource } from "kitsu"; +import { useEffect, useState } from "react"; +import { GroupSelectField } from "../../group-select/GroupSelectField"; +import { Protocol } from "../../../types/collection-api"; +import { MetagenomicsBatch } from "../../../types/seqdb-api/resources/metagenomics/MetagenomicsBatch"; +import { IndexSet, PcrBatch, PcrBatchItem } from "../../../types/seqdb-api"; +import { MetagenomicsBatchItem } from "../../../types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem"; + +export interface MetagenomicsBatchDetailsStepProps { + metagenomicsBatchId?: string; + metagenomicsBatch?: MetagenomicsBatch; + onSaved: ( + nextStep: number, + metagenomicsBatch?: PersistedResource + ) => Promise; + editMode: boolean; + setEditMode: (newValue: boolean) => void; + performSave: boolean; + setPerformSave: (newValue: boolean) => void; + pcrBatch?: PcrBatch; +} + +export function MetagenomicsBatchDetailsStep({ + metagenomicsBatchId, + metagenomicsBatch, + onSaved, + editMode, + setEditMode, + performSave, + setPerformSave, + pcrBatch +}: MetagenomicsBatchDetailsStepProps) { + const { username } = useAccount(); + const { apiClient } = useApiClient(); + const [pcrBatchItems, setPcrBatchItems] = useState([]); + + /** + * Retrieve all of the PCR Batch Items that are associated with the PCR Batch from step 1. + */ + async function fetchPcrBatchItems() { + await apiClient + .get("/seqdb-api/pcr-batch-item", { + filter: filterBy([], { + extraFilters: [ + { + selector: "pcrBatch.uuid", + comparison: "==", + arguments: pcrBatch!.id! + } + ] + })("") + }) + .then((response) => { + setPcrBatchItems(response?.data); + }); + } + + // If no Metagenomics Batch has been created, automatically go to edit mode. + useEffect(() => { + if (!metagenomicsBatchId) { + setEditMode(true); + } + }, [metagenomicsBatchId]); + + /** + * When the page is first loaded, get PcrBatchItems + */ + useEffect(() => { + fetchPcrBatchItems(); + }, [editMode]); + + async function onSavedInternal( + resource: PersistedResource + ) { + setPerformSave(false); + await onSaved(5, resource); + } + + const initialValues = metagenomicsBatch || { + createdBy: username, + type: "metagenomics-batch" + }; + + const buttonBar = ( + <> + + + ); + + async function onSubmit({ + submittedValues, + api: { save } + }: DinaFormSubmitParams) { + const inputResource = { + ...submittedValues + }; + // Save MetagenomicsBatch + const [savedMetagenomicsBatch] = await save( + [ + { + resource: inputResource, + type: "metagenomics-batch" + } + ], + { apiBaseUrl: "/seqdb-api" } + ); + + // Save MetagenomicsBatchItems + const metagenomicsBatchItemSaveArgs: SaveArgs[] = + pcrBatchItems.map((pcrBatchItem) => { + return { + type: "metagenomics-batch-item", + resource: { + type: "metagenomics-batch-item", + relationships: { + metagenomicsBatch: { + data: { + id: savedMetagenomicsBatch.id, + type: "metagenomics-batch" + } + }, + pcrBatchItem: { + data: { + id: pcrBatchItem.id, + type: "pcr-batch-item" + } + } + } + } + }; + }); + + const saveMetagenomicsBatchItems = await save( + metagenomicsBatchItemSaveArgs, + { apiBaseUrl: "/seqdb-api" } + ); + + await onSavedInternal(savedMetagenomicsBatch); + } + + return ( + > + initialValues={initialValues} + onSubmit={onSubmit} + readOnly={!editMode} + > + {buttonBar} + + + ); +} + +export function MetagenomicsBatchForm() { + const { readOnly } = useDinaFormContext(); + + return ( +
+
+ + +
+
+ + className="col-md-6" + name="protocol" + filter={filterBy(["name"], { + extraFilters: [ + { + selector: "protocolType", + comparison: "==", + arguments: "molecular_analysis" + } + ] + })} + model="collection-api/protocol" + optionLabel={(protocol) => protocol.name} + readOnlyLink="/collection/protocol/view?id=" + /> + + className="col-md-6" + name="indexSet" + filter={filterBy(["name"])} + model="seqdb-api/index-set" + optionLabel={(set) => set.name} + readOnlyLink="/seqdb/index-set/view?id=" + /> +
+ {readOnly && ( +
+ + +
+ )} +
+ ); +} diff --git a/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsIndexAssignmentStep.tsx b/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsIndexAssignmentStep.tsx new file mode 100644 index 000000000..3721e491b --- /dev/null +++ b/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsIndexAssignmentStep.tsx @@ -0,0 +1,100 @@ +import { PersistedResource } from "kitsu"; +import { PcrBatch } from "packages/dina-ui/types/seqdb-api"; +import { Dispatch, SetStateAction } from "react"; +import Col from "react-bootstrap/Col"; +import Nav from "react-bootstrap/Nav"; +import Row from "react-bootstrap/Row"; +import Tab from "react-bootstrap/Tab"; +import { useSeqdbIntl } from "packages/dina-ui/intl/seqdb-intl"; +import { useLocalStorage } from "@rehooks/local-storage"; +import { MetagenomicsBatch } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatch"; +import { useMetagenomicsIndexAssignmentAPI } from "../ngs-workflow/useMetagenomicsIndexAssignmentAPI"; +import { MetagenomicsIndexGrid } from "../ngs-workflow/index-grid/MetagenomicsIndexGrid"; +import { MetagenomicsIndexAssignmentTable } from "../ngs-workflow/MetagenomicsIndexAssignmentTable"; + +export interface MetagenomicsIndexAssignmentStepProps { + pcrBatchId: string; + pcrBatch: PcrBatch; + metagenomicsBatchId: string; + metagenomicsBatch: MetagenomicsBatch; + onSaved: ( + nextStep: number, + batchSaved?: PersistedResource + ) => Promise; + editMode: boolean; + setEditMode: Dispatch>; + performSave: boolean; + setPerformSave: (newValue: boolean) => void; +} + +export function MetagenomicsIndexAssignmentStep( + props: MetagenomicsIndexAssignmentStepProps +) { + const { formatMessage } = useSeqdbIntl(); + // Get the last active tab from local storage (defaults to "assignByGrid") + const [activeKey, setActiveKey] = useLocalStorage( + "metagenomicsIndexAssignmentStep_activeTab", + "assignByGrid" + ); + + // Data required for both options is pretty much the same so share the data between both. + const metagenomicsIndexAssignmentApiProps = + useMetagenomicsIndexAssignmentAPI(props); + + const handleSelect = (eventKey) => { + // Do not switch modes if in edit mode. This is used to prevent data from being mixed up. + if (props.editMode) { + return; + } + + setActiveKey(eventKey); + }; + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsRunStep.tsx b/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsRunStep.tsx new file mode 100644 index 000000000..94d8eebbe --- /dev/null +++ b/packages/dina-ui/components/seqdb/metagenomics-workflow/MetagenomicsRunStep.tsx @@ -0,0 +1,129 @@ +import Link from "next/link"; +import { SeqBatch } from "../../../types/seqdb-api"; +import { + SequencingRunItem, + useMolecularAnalysisRun +} from "../../molecular-analysis/useMolecularAnalysisRun"; +import { LoadingSpinner, ReactTable } from "common-ui"; +import { Alert } from "react-bootstrap"; +import { DinaMessage } from "../../../intl/dina-ui-intl"; +import { useMetagenomicsWorkflowMolecularAnalysisRun } from "../../molecular-analysis/useMetagenomicsWorkflowMolecularAnalysisRun"; +import { MetagenomicsBatch } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatch"; + +export interface SequencingRunStepProps { + metagenomicsBatchId: string; + metagenomicsBatch: MetagenomicsBatch; + editMode: boolean; + setEditMode: (newValue: boolean) => void; + performSave: boolean; + setPerformSave: (newValue: boolean) => void; +} + +export function MetagenomicsRunStep({ + metagenomicsBatchId, + metagenomicsBatch, + editMode, + setEditMode, + performSave, + setPerformSave +}: SequencingRunStepProps) { + const { + loading, + errorMessage, + multipleRunWarning, + setSequencingRunName, + sequencingRunName, + sequencingRunItems, + columns + } = useMetagenomicsWorkflowMolecularAnalysisRun({ + editMode, + setEditMode, + performSave, + setPerformSave, + metagenomicsBatchId, + metagenomicsBatch + }); + + // Display loading if network requests from hook are still loading in... + if (loading) { + return ( +
+ +
+ ); + } + + return ( + <> + {/* Multiple Runs Exist Warning */} + {multipleRunWarning && ( +
+
+ + + + +

+ +

+
+
+
+ )} + + {/* Error Message */} + {errorMessage && ( +
+
+ + {errorMessage} + +
+
+ )} + + {/* Run Information */} + {editMode || + sequencingRunItems?.some((item) => item.molecularAnalysisRunItemId) ? ( +
+
+ + + + {editMode ? ( + + setSequencingRunName(newValue.target.value ?? "") + } + /> + ) : ( +

{sequencingRunName}

+ )} +
+
+ + + + + className="-striped mt-2" + columns={columns} + data={sequencingRunItems ?? []} + sort={[{ id: "wellCoordinates", desc: false }]} + /> +
+
+ ) : ( +
+
+ + + +
+
+ )} + + ); +} diff --git a/packages/dina-ui/components/seqdb/metagenomics-workflow/useMetagenomicsBatchQuery.tsx b/packages/dina-ui/components/seqdb/metagenomics-workflow/useMetagenomicsBatchQuery.tsx new file mode 100644 index 000000000..048cbb705 --- /dev/null +++ b/packages/dina-ui/components/seqdb/metagenomics-workflow/useMetagenomicsBatchQuery.tsx @@ -0,0 +1,88 @@ +import { filterBy, useApiClient, useQuery } from "common-ui"; +import { MetagenomicsBatch } from "../../../types/seqdb-api/resources/metagenomics/MetagenomicsBatch"; +import { PcrBatchItem } from "../../../types/seqdb-api"; +import { MetagenomicsBatchItem } from "../../../types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem"; +import { PersistedResource } from "kitsu"; +import { useEffect, useState } from "react"; + +export function useMetagenomicsBatchQuery( + metagenomicsBatchId?: string, + pcrBatchId?: string, + setMetagenomicsBatchId?: React.Dispatch< + React.SetStateAction + >, + deps?: any[] +) { + const { apiClient } = useApiClient(); + /** + * Retrieve all of the PCR Batch Items that are associated with the PCR Batch from step 1. + */ + async function fetchPcrBatchItems() { + const pcrBatchItemsResp = await apiClient.get( + "/seqdb-api/pcr-batch-item", + { + filter: filterBy([], { + extraFilters: [ + { + selector: "pcrBatch.uuid", + comparison: "==", + arguments: pcrBatchId! + } + ] + })("") + } + ); + return pcrBatchItemsResp.data; + } + + async function getMetagenomicsBatchId( + pcrBatchItems: PersistedResource[] + ) { + const metagenomicsBatchItem = await apiClient.get( + "/seqdb-api/metagenomics-batch-item", + { + include: "metagenomicsBatch", + filter: filterBy([], { + extraFilters: [ + { + selector: "pcrBatchItem.uuid", + comparison: "==", + arguments: pcrBatchItems[0].id! + } + ] + })("") + } + ); + return metagenomicsBatchItem?.data?.[0]?.metagenomicsBatch?.id; + } + async function fetchMetagenomicsBatch() { + const pcrBatchItems = await fetchPcrBatchItems(); + + // Only reverse look up metagenomicsBatch if pcrBatchItems exist + if (pcrBatchItems?.length > 0) { + const fetchedMetagenomicsBatchId = await getMetagenomicsBatchId( + pcrBatchItems + ); + setMetagenomicsBatchId?.(fetchedMetagenomicsBatchId); + } + } + useEffect(() => { + if (!metagenomicsBatchId) { + if (pcrBatchId) { + fetchMetagenomicsBatch(); + } + } + }, [metagenomicsBatchId]); + + const metagenomicsBatch = useQuery( + { + path: `seqdb-api/metagenomics-batch/${metagenomicsBatchId}`, + include: "protocol,indexSet" + }, + { + disabled: !metagenomicsBatchId, + deps + } + ); + return metagenomicsBatch; +} diff --git a/packages/dina-ui/components/seqdb/ngs-workflow/MetagenomicsIndexAssignmentTable.tsx b/packages/dina-ui/components/seqdb/ngs-workflow/MetagenomicsIndexAssignmentTable.tsx new file mode 100644 index 000000000..eb3520caf --- /dev/null +++ b/packages/dina-ui/components/seqdb/ngs-workflow/MetagenomicsIndexAssignmentTable.tsx @@ -0,0 +1,213 @@ +import { + DinaForm, + LoadingSpinner, + ReactTable, + SelectField, + SelectOption, + SubmitButton, + useStringComparator +} from "packages/common-ui/lib"; +import { IndexAssignmentStepProps } from "./IndexAssignmentStep"; +import { useMemo } from "react"; +import { PcrBatchItem } from "packages/dina-ui/types/seqdb-api"; +import { ColumnDef } from "@tanstack/react-table"; +import { MaterialSampleSummary } from "packages/dina-ui/types/collection-api"; +import { UseIndexAssignmentReturn } from "./useIndexAssignmentAPI"; +import { useSeqdbIntl } from "packages/dina-ui/intl/seqdb-intl"; +import { MetagenomicsBatchItem } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem"; +import { MetagenomicsIndexAssignmentStepProps } from "../metagenomics-workflow/MetagenomicsIndexAssignmentStep"; +import { + MetagenomicsIndexAssignmentResource, + UseMetagenomicsIndexAssignmentReturn +} from "./useMetagenomicsIndexAssignmentAPI"; + +interface MetagenomicsIndexAssignmentRow { + materialSampleSummary?: MaterialSampleSummary; + metagenomicsIndexAssignmentResource?: MetagenomicsIndexAssignmentResource; + pcrBatchItem?: PcrBatchItem; +} + +interface MetagenomicsIndexAssignmentTableProps + extends MetagenomicsIndexAssignmentStepProps, + UseMetagenomicsIndexAssignmentReturn {} + +export function MetagenomicsIndexAssignmentTable( + props: MetagenomicsIndexAssignmentTableProps +) { + const { + editMode, + metagenomicsBatch, + performSave, + setPerformSave, + loading, + metagenomicsIndexAssignmentResources, + materialSampleSummaries, + ngsIndexes, + onSubmitTable + } = props; + + const { compareByStringAndNumber } = useStringComparator(); + const { formatMessage } = useSeqdbIntl(); + + // Hidden button bar is used to submit the page from the button bar in a parent component. + const hiddenButtonBar = ( + + ); + + const COLUMNS: ColumnDef[] = [ + { + cell: ({ row: { original: sr } }) => { + const { metagenomicsIndexAssignmentResource } = + sr as MetagenomicsIndexAssignmentRow; + if ( + !metagenomicsIndexAssignmentResource || + !metagenomicsIndexAssignmentResource.storageUnitUsage + ) { + return ""; + } + + const { wellRow, wellColumn } = + metagenomicsIndexAssignmentResource.storageUnitUsage; + const wellCoordinates = + wellColumn === null || !wellRow ? "" : `${wellRow}${wellColumn}`; + + return wellCoordinates; + }, + header: "Well Coordinates", + accessorKey: "wellCoordinates", + sortingFn: (a: any, b: any): number => { + const aStorageUnit = + a.original?.metagenomicsIndexAssignmentResource?.storageUnitUsage; + const bStorageUnit = + b.original?.metagenomicsIndexAssignmentResource?.storageUnitUsage; + + const aString = + !aStorageUnit || + aStorageUnit?.wellRow === null || + aStorageUnit?.wellColumn === null + ? "" + : `${aStorageUnit?.wellRow}${aStorageUnit?.wellColumn}`; + const bString = + !bStorageUnit || + bStorageUnit?.wellRow === null || + bStorageUnit?.wellColumn === null + ? "" + : `${bStorageUnit?.wellRow}${bStorageUnit?.wellColumn}`; + return compareByStringAndNumber(aString, bString); + }, + enableSorting: true, + size: 150 + }, + { + header: "Material Sample Name", + accessorKey: "materialSampleSummary.materialSampleName" + }, + { + header: "Index i5", + cell: ({ row: { index } }) => + metagenomicsBatch.indexSet && ( + ngsIndex.direction === "I5") + ?.map>((ngsIndex) => ({ + label: ngsIndex.name, + value: ngsIndex.id + }))} + selectProps={{ + isClearable: true + }} + /> + ) + }, + { + header: "Index i7", + cell: ({ row: { index } }) => + metagenomicsBatch.indexSet && ( + ngsIndex.direction === "I7") + ?.map>((ngsIndex) => ({ + label: ngsIndex.name, + value: ngsIndex.id + }))} + selectProps={{ + isClearable: true + }} + /> + ) + } + ]; + + const tableData = useMemo( + () => + metagenomicsIndexAssignmentResources && + metagenomicsIndexAssignmentResources.length !== 0 + ? metagenomicsIndexAssignmentResources.map( + (prep) => ({ + metagenomicsIndexAssignmentResource: prep, + materialSampleSummary: materialSampleSummaries?.find( + (samp) => samp.id === prep?.materialSampleSummary?.id + ) + }) + ) + : [], + [metagenomicsIndexAssignmentResources, materialSampleSummaries] + ); + + const initialValues = useMemo(() => { + if ( + !metagenomicsIndexAssignmentResources || + metagenomicsIndexAssignmentResources.length === 0 + ) { + return {}; + } + + return { + indexAssignment: metagenomicsIndexAssignmentResources.map((resource) => ({ + ...(resource.indexI5 ? { indexI5: resource?.indexI5?.id } : {}), + ...(resource.indexI7 ? { indexI7: resource?.indexI7?.id } : {}) + })) + }; + }, [metagenomicsIndexAssignmentResources]); + + if (loading) { + return ; + } + + if (!metagenomicsBatch?.indexSet?.id) { + return ( +
+ {formatMessage("missingIndexForAssignment")} +
+ ); + } + + return ( + + {hiddenButtonBar} + + className="-striped react-table-overflow" + columns={COLUMNS} + data={tableData} + loading={loading} + manualPagination={true} + showPagination={false} + pageSize={tableData.length} + sort={[{ id: "wellCoordinates", desc: false }]} + /> + + ); +} diff --git a/packages/dina-ui/components/seqdb/ngs-workflow/index-grid/MetagenomicsIndexGrid.tsx b/packages/dina-ui/components/seqdb/ngs-workflow/index-grid/MetagenomicsIndexGrid.tsx new file mode 100644 index 000000000..f32cb358b --- /dev/null +++ b/packages/dina-ui/components/seqdb/ngs-workflow/index-grid/MetagenomicsIndexGrid.tsx @@ -0,0 +1,226 @@ +import { + LoadingSpinner, + ReactTable, + DinaForm, + SelectField, + SelectOption, + SubmitButton +} from "common-ui"; +import { LibraryPrep } from "../../../../types/seqdb-api"; +import { ColumnDef } from "@tanstack/react-table"; +import { useSeqdbIntl } from "packages/dina-ui/intl/seqdb-intl"; +import { MetagenomicsIndexAssignmentStepProps } from "../../metagenomics-workflow/MetagenomicsIndexAssignmentStep"; +import { + MetagenomicsIndexAssignmentResource, + UseMetagenomicsIndexAssignmentReturn +} from "../useMetagenomicsIndexAssignmentAPI"; + +interface CellData { + row: number; +} + +interface MetagenomicsIndexGridProps + extends MetagenomicsIndexAssignmentStepProps, + UseMetagenomicsIndexAssignmentReturn {} + +export function MetagenomicsIndexGrid(props: MetagenomicsIndexGridProps) { + const { + pcrBatch, + metagenomicsBatch, + editMode, + performSave, + setPerformSave, + loading, + metagenomicsIndexAssignmentResources, + materialSampleSummaries, + ngsIndexes, + storageUnitType, + onSubmitGrid + } = props; + + const { formatMessage } = useSeqdbIntl(); + const { indexSet } = metagenomicsBatch; + + // Hidden button bar is used to submit the page from the button bar in a parent component. + const hiddenButtonBar = ( + + ); + + if (loading) { + return ; + } + + if (!storageUnitType || !indexSet) { + return ( +
+ {formatMessage("missingContainerAndIndexForAssignment")} +
+ ); + } + + if (metagenomicsIndexAssignmentResources) { + const indexAssignmentResourcesWithCoords = + metagenomicsIndexAssignmentResources.filter( + (indexAssignmentResource) => + indexAssignmentResource.storageUnitUsage?.wellRow && + indexAssignmentResource.storageUnitUsage?.wellColumn + ); + + // Display an error if no coordinates have been selected yet, nothing to edit. + if (indexAssignmentResourcesWithCoords.length === 0) { + return ( +
+ {formatMessage("missingSelectedCoordinatesForAssignment")} +
+ ); + } + + const cellGrid: { [key: string]: MetagenomicsIndexAssignmentResource } = {}; + for (const resource of indexAssignmentResourcesWithCoords) { + cellGrid[ + `${resource.storageUnitUsage?.wellRow}_${resource.storageUnitUsage?.wellColumn}` + ] = resource; + } + + const columns: ColumnDef[] = []; + + // Add the primer column: + columns.push({ + cell: ({ row: { original } }) => { + const rowLetter = String.fromCharCode(original.row + 65); + + return ( + indexSet && ( +
+ index.direction === "I5") + ?.map>((index) => ({ + label: index.name, + value: index.id + }))} + selectProps={{ + isClearable: true, + placeholder: formatMessage("selectI5") + }} + removeBottomMargin={true} + /> +
+ ) + ); + }, + meta: { + style: { + background: "white", + boxShadow: "7px 0px 9px 0px rgba(0,0,0,0.1)" + } + }, + id: "rowNumber", + accessorKey: "", + enableResizing: false, + enableSorting: false, + size: 300 + }); + + // Generate the columns + for ( + let col = 0; + col < (storageUnitType?.gridLayoutDefinition?.numberOfColumns ?? 0); + col++ + ) { + const columnLabel = String(col + 1); + + columns.push({ + cell: ({ row: { original } }) => { + const rowLabel = String.fromCharCode(original.row + 65); + const coords = `${rowLabel}_${columnLabel}`; + const indexAssignmentResource: MetagenomicsIndexAssignmentResource = + cellGrid[coords]; + + return indexAssignmentResource ? ( +
+
+ {materialSampleSummaries?.find( + (sample) => + sample.id === + indexAssignmentResource?.materialSampleSummary?.id + )?.materialSampleName ?? ""} +
+
+ {indexAssignmentResource.indexI5 && ( +
+ i5: + {indexAssignmentResource.indexI5.name} +
+ )} + {indexAssignmentResource.indexI7 && ( +
+ i7: + {indexAssignmentResource.indexI7.name} +
+ )} +
+
+ ) : null; + }, + header: () => + indexSet && ( + index.direction === "I7") + ?.map>((index) => ({ + label: index.name, + value: index.id + }))} + selectProps={{ + isClearable: true, + placeholder: formatMessage("selectI7") + }} + removeBottomMargin={true} + className={"w-100"} + /> + ), + id: `${columnLabel}`, + accessorKey: `${columnLabel}`, + enableResizing: false, + enableSorting: false, + size: 300 + }); + } + + // Populate the table's rows using the number of rows. + const tableData: CellData[] = []; + const numberOfRows = + storageUnitType?.gridLayoutDefinition?.numberOfRows ?? 0; + for (let i = 0; i < numberOfRows; i++) { + tableData.push({ row: i }); + } + + return ( + + {hiddenButtonBar} + + className="-striped react-table-overflow" + columns={columns} + data={tableData} + showPagination={false} + manualPagination={true} + /> + + ); + } + + return null; +} diff --git a/packages/dina-ui/components/seqdb/ngs-workflow/useMetagenomicsIndexAssignmentAPI.ts b/packages/dina-ui/components/seqdb/ngs-workflow/useMetagenomicsIndexAssignmentAPI.ts new file mode 100644 index 000000000..618678cfd --- /dev/null +++ b/packages/dina-ui/components/seqdb/ngs-workflow/useMetagenomicsIndexAssignmentAPI.ts @@ -0,0 +1,367 @@ +import { + ApiClientContext, + DinaFormSubmitParams, + filterBy, + SaveArgs, + useQuery +} from "common-ui"; +import { Dictionary, toPairs } from "lodash"; +import { useContext, useState, useEffect } from "react"; +import { + MaterialSample, + MaterialSampleSummary, + Protocol, + StorageUnit, + StorageUnitType +} from "packages/dina-ui/types/collection-api"; +import { StorageUnitUsage } from "packages/dina-ui/types/collection-api/resources/StorageUnitUsage"; +import { + LibraryPrep, + NgsIndex, + PcrBatchItem +} from "packages/dina-ui/types/seqdb-api"; +import { isEqual } from "lodash"; +import { MetagenomicsIndexAssignmentStepProps } from "../metagenomics-workflow/MetagenomicsIndexAssignmentStep"; +import { MetagenomicsBatchItem } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem"; + +export interface UseMetagenomicsIndexAssignmentReturn { + loading: boolean; + metagenomicsIndexAssignmentResources?: MetagenomicsIndexAssignmentResource[]; + materialSampleSummaries?: MaterialSampleSummary[]; + ngsIndexes?: NgsIndex[]; + storageUnitType?: StorageUnitType; + protocol?: Protocol; + onSubmitGrid: ({ + submittedValues + }: DinaFormSubmitParams) => Promise; + onSubmitTable: ({ + submittedValues + }: DinaFormSubmitParams) => Promise; +} + +// UI-side only type that combines MetagenomicsBatchItem and other fields necessary for Index Assignment step +export type MetagenomicsIndexAssignmentResource = MetagenomicsBatchItem & { + materialSampleSummary?: MaterialSampleSummary; + storageUnitUsage?: StorageUnitUsage; +}; + +export function useMetagenomicsIndexAssignmentAPI({ + pcrBatch, + metagenomicsBatch, + editMode, + setEditMode, + setPerformSave, + onSaved +}: Partial): UseMetagenomicsIndexAssignmentReturn { + const { save, apiClient, bulkGet } = useContext(ApiClientContext); + + const [lastSave, setLastSave] = useState(); + + const [storageUnitType, setStorageUnitType] = useState(); + const [loading, setLoading] = useState(true); + const [ + metagenomicsIndexAssignmentResources, + setMetagenomicsIndexAssignmentResources + ] = useState([]); + const [materialSampleSummaries, setMaterialSamples] = + useState(); + const [ngsIndexes, setNgsIndexes] = useState(); + const [protocol, setProtocol] = useState(); + + useQuery( + { + include: "indexI5,indexI7,pcrBatchItem", + page: { limit: 1000 }, + filter: filterBy([], { + extraFilters: [ + { + selector: "metagenomicsBatch.uuid", + comparison: "==", + arguments: metagenomicsBatch?.id ?? "" + } + ] + })(""), + path: `seqdb-api/metagenomics-batch-item` + }, + { + deps: [lastSave], + joinSpecs: [ + { + apiBaseUrl: "/seqdb-api", + idField: "pcrBatchItem.id", + joinField: "pcrBatchItem", + path: (metagenomicsBatchItem: MetagenomicsBatchItem) => + `pcr-batch-item/${metagenomicsBatchItem.pcrBatchItem?.id}?include=materialSample,storageUnitUsage` + } + ], + async onSuccess(response) { + /** + * Fetch Storage Unit Usage linked to each Library Prep along with the material sample. + * @returns + */ + async function fetchStorageUnitUsage( + metagenomicsBatchItems: MetagenomicsBatchItem[] + ): Promise { + const paths = metagenomicsBatchItems + .filter((item) => item.pcrBatchItem?.storageUnitUsage?.id) + .map( + (item) => + "/storage-unit-usage/" + item.pcrBatchItem?.storageUnitUsage?.id + ); + const storageUnitUsageQuery = await bulkGet(paths, { + apiBaseUrl: "/collection-api" + }); + + return metagenomicsBatchItems.map((metagenomicsBatchItem) => { + const queryStorageUnitUsage = storageUnitUsageQuery.find( + (storageUnitUsage) => + storageUnitUsage?.id === + metagenomicsBatchItem.pcrBatchItem?.storageUnitUsage?.id + ); + return { + ...metagenomicsBatchItem, + storageUnitUsage: queryStorageUnitUsage as StorageUnitUsage + }; + }); + } + + async function fetchMaterialSamples( + metagenomicsBatchItems: MetagenomicsIndexAssignmentResource[] + ): Promise { + const materialSampleQuery = await bulkGet( + metagenomicsBatchItems + .filter((item) => item?.pcrBatchItem?.materialSample?.id) + .map( + (item) => + "/material-sample-summary/" + + item?.pcrBatchItem?.materialSample?.id + ), + { + apiBaseUrl: "/collection-api" + } + ); + return materialSampleQuery as MaterialSampleSummary[]; + } + let fetchedMetagenomicsIndexAssignmentResources = + await fetchStorageUnitUsage(response.data); + const materialSampleItems = await fetchMaterialSamples( + fetchedMetagenomicsIndexAssignmentResources + ); + + // Add materialSampleSummary to each resource + fetchedMetagenomicsIndexAssignmentResources = + fetchedMetagenomicsIndexAssignmentResources.map( + (metagenomicsBatchItem) => { + const materialSampleSummary = materialSampleItems.find( + (msSummary) => + msSummary.id === + metagenomicsBatchItem.pcrBatchItem?.materialSample?.id + ); + return { + ...metagenomicsBatchItem, + materialSampleSummary + }; + } + ); + setMetagenomicsIndexAssignmentResources( + fetchedMetagenomicsIndexAssignmentResources + ); + setMaterialSamples(materialSampleItems); + setLoading(false); + } + } + ); + + useQuery( + { + page: { limit: 1000 }, + filter: filterBy([], { + extraFilters: [ + { + selector: "indexSet.uuid", + comparison: "==", + arguments: metagenomicsBatch?.indexSet?.id ?? "" + } + ] + })(""), + path: `seqdb-api/ngs-index` + }, + { + deps: [lastSave], + async onSuccess(response) { + setNgsIndexes(response.data as NgsIndex[]); + }, + disabled: !metagenomicsBatch?.indexSet?.id + } + ); + + useQuery( + { + page: { limit: 1 }, + path: `collection-api/protocol/${metagenomicsBatch?.protocol?.id}` + }, + { + async onSuccess(response) { + setProtocol(response.data as Protocol); + }, + disabled: !metagenomicsBatch?.protocol?.id + } + ); + + useEffect(() => { + if (!pcrBatch || !pcrBatch.storageUnit) return; + + async function fetchStorageUnitTypeLayout() { + const storageUnitReponse = await apiClient.get( + `/collection-api/storage-unit/${pcrBatch?.storageUnit?.id}`, + { include: "storageUnitType" } + ); + if (storageUnitReponse?.data.storageUnitType?.gridLayoutDefinition) { + setStorageUnitType(storageUnitReponse?.data.storageUnitType); + } + } + fetchStorageUnitTypeLayout(); + }, [metagenomicsIndexAssignmentResources]); + + /** + * Index Grid Form Submit + * + * Columns can set the i7 for each cell in that column and rows can set the i5 index for each + * cell in that row. + * + * @param submittedValues Formik form data - Indicates the row/column and the index to set. + */ + async function onSubmitGrid({ submittedValues }: DinaFormSubmitParams) { + // Do not perform a submit if not in edit mode. + if (!editMode) { + return; + } + + const resourcesToSave = metagenomicsIndexAssignmentResources + ? metagenomicsIndexAssignmentResources + : []; + const { indexI5s, indexI7s } = submittedValues; + + const edits: Dictionary> = {}; + + // Get the new i7 values: + const colIndexes = toPairs(indexI7s); + for (const [col, index] of colIndexes) { + const colResources = resourcesToSave.filter( + (it) => String(it?.storageUnitUsage?.wellColumn) === col + ); + for (const metagenicsIndexAssignmentResource of colResources) { + if (metagenicsIndexAssignmentResource.id) { + const edit = edits[metagenicsIndexAssignmentResource.id] || {}; + edit.indexI7 = { id: index, type: "ngs-index" } as NgsIndex; + edits[metagenicsIndexAssignmentResource.id] = edit; + } + } + } + + // Get the new i5 values: + const rowIndexes = toPairs(indexI5s); + for (const [row, index] of rowIndexes) { + const rowPreps = resourcesToSave.filter( + (it) => it?.storageUnitUsage?.wellRow === row + ); + for (const prep of rowPreps) { + if (prep.id) { + const edit = edits[prep.id] || {}; + edit.indexI5 = { id: index, type: "ngs-index" } as NgsIndex; + edits[prep.id] = edit; + } + } + } + + const saveOps: SaveArgs[] = toPairs(edits).map(([id, prepEdit]) => ({ + resource: { id, type: "metagenomics-batch-item", ...prepEdit }, + type: "metagenomics-batch-item" + })); + + await save(saveOps, { apiBaseUrl: "/seqdb-api" }); + setLastSave(Date.now()); + setPerformSave?.(false); + setEditMode?.(false); + await onSaved?.(6); + } + + /** + * Table index assignment submit. This form lets you set the i5/i7 indexes for each library + * prep individually. + * + * @param submittedValues Formik form data + */ + async function onSubmitTable({ submittedValues }: DinaFormSubmitParams) { + // Do not perform a submit if not in edit mode. + if (!editMode) { + return; + } + + // Library preps must be loaded in. + if ( + !metagenomicsIndexAssignmentResources || + metagenomicsIndexAssignmentResources.length === 0 || + !submittedValues.indexAssignment || + submittedValues.indexAssignment.length === 0 + ) { + return; + } + + const indexAssignmentUpdates = (submittedValues?.indexAssignment as any[]) + ?.map( + (submittedValue: any, index: number) => + ({ + type: "metagenomics-batch-item", + id: metagenomicsIndexAssignmentResources[index].id, + ...(!isEqual( + metagenomicsIndexAssignmentResources[index]?.indexI5?.id, + submittedValue.indexI5 + ) && { + indexI5: { + type: "ngs-index", + id: submittedValue.indexI5 ? submittedValue.indexI5 : null + } + }), + ...(!isEqual( + metagenomicsIndexAssignmentResources[index]?.indexI7?.id, + submittedValue.indexI7 + ) && { + indexI7: { + type: "ngs-index", + id: submittedValue.indexI7 ? submittedValue.indexI7 : null + } + }) + } as MetagenomicsBatchItem) + ) + ?.filter( + (update: any) => + update.indexI5 !== undefined || update.indexI7 !== undefined + ); + + if (indexAssignmentUpdates.length !== 0) { + const saveArgs = indexAssignmentUpdates.map((resource) => ({ + resource, + type: "metagenomics-batch-item" + })); + + await save(saveArgs, { apiBaseUrl: "/seqdb-api" }); + setLastSave(Date.now()); + } + + setPerformSave?.(false); + setEditMode?.(false); + await onSaved?.(6); + } + + return { + loading, + metagenomicsIndexAssignmentResources, + materialSampleSummaries, + ngsIndexes, + storageUnitType, + protocol, + onSubmitGrid, + onSubmitTable + }; +} diff --git a/packages/dina-ui/components/seqdb/pcr-workflow/SangerPcrBatchStep.tsx b/packages/dina-ui/components/seqdb/pcr-workflow/SangerPcrBatchStep.tsx index 02ab856cc..c95d715b8 100644 --- a/packages/dina-ui/components/seqdb/pcr-workflow/SangerPcrBatchStep.tsx +++ b/packages/dina-ui/components/seqdb/pcr-workflow/SangerPcrBatchStep.tsx @@ -15,6 +15,7 @@ export interface SangerPcrBatchStepProps { setEditMode: (newValue: boolean) => void; performSave: boolean; setPerformSave: (newValue: boolean) => void; + isMetagenomicsWorkflow?: boolean; } export function SangerPcrBatchStep({ @@ -24,7 +25,8 @@ export function SangerPcrBatchStep({ editMode, setEditMode, performSave, - setPerformSave + setPerformSave, + isMetagenomicsWorkflow }: SangerPcrBatchStepProps) { // If no PCR Batch has been created, automatically go to edit mode. useEffect(() => { @@ -54,6 +56,7 @@ export function SangerPcrBatchStep({ onSaved={onSavedInternal} buttonBar={buttonBar} readOnlyOverride={!editMode} + isMetagenomicsWorkflow={isMetagenomicsWorkflow} /> ); } diff --git a/packages/dina-ui/components/seqdb/pcr-workflow/SangerPcrReactionStep.tsx b/packages/dina-ui/components/seqdb/pcr-workflow/SangerPcrReactionStep.tsx index 150a9c63b..df5453bdc 100644 --- a/packages/dina-ui/components/seqdb/pcr-workflow/SangerPcrReactionStep.tsx +++ b/packages/dina-ui/components/seqdb/pcr-workflow/SangerPcrReactionStep.tsx @@ -5,8 +5,8 @@ import { PcrBatch, PcrBatchItem } from "../../../types/seqdb-api"; import { PcrReactionTable, usePcrReactionData } from "./PcrReactionTable"; import Link from "next/link"; import { AttachmentsField } from "../../object-store/attachment-list/AttachmentsField"; -import { DinaMessage } from "packages/dina-ui/intl/dina-ui-intl"; -import { InputResource, KitsuResource } from "kitsu"; +import { DinaMessage } from "../../../intl/dina-ui-intl"; +import { InputResource, KitsuResource, PersistedResource } from "kitsu"; export interface SangerPcrReactionProps { pcrBatchId: string; @@ -18,6 +18,10 @@ export interface SangerPcrReactionProps { setPerformComplete: (newValue: boolean) => void; setEditMode: (newValue: boolean) => void; setReloadPcrBatch: (newValue: number) => void; + onSaved?: ( + nextStep: number, + pcrBatchSaved?: PersistedResource + ) => Promise; } export function SangerPcrReactionStep({ @@ -29,7 +33,8 @@ export function SangerPcrReactionStep({ performComplete, setPerformComplete, setEditMode, - setReloadPcrBatch + setReloadPcrBatch, + onSaved }: SangerPcrReactionProps) { const { doOperations, save } = useApiClient(); const formRef: Ref>> = useRef(null); @@ -125,6 +130,7 @@ export function SangerPcrReactionStep({ if (!!setPerformComplete) { setPerformComplete(false); } + await onSaved?.(4); } // Load the result based on the API request with the pcr-batch-item. diff --git a/packages/dina-ui/components/seqdb/pcr-workflow/SangerSampleSelectionStep.tsx b/packages/dina-ui/components/seqdb/pcr-workflow/SangerSampleSelectionStep.tsx index f27901e64..94c6e678b 100644 --- a/packages/dina-ui/components/seqdb/pcr-workflow/SangerSampleSelectionStep.tsx +++ b/packages/dina-ui/components/seqdb/pcr-workflow/SangerSampleSelectionStep.tsx @@ -3,11 +3,13 @@ import { DoOperationsError, LoadingSpinner, QueryPage, + SaveArgs, filterBy, useAccount, - useApiClient + useApiClient, + useQuery } from "common-ui"; -import { PersistedResource } from "kitsu"; +import { KitsuResponse, PersistedResource } from "kitsu"; import { compact, pick, uniq, difference, concat } from "lodash"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -18,6 +20,9 @@ import { import { SeqdbMessage } from "../../../intl/seqdb-intl"; import { PcrBatch, PcrBatchItem } from "../../../types/seqdb-api"; import { useMaterialSampleRelationshipColumns } from "../../collection/material-sample/useMaterialSampleRelationshipColumns"; +import { MetagenomicsBatch } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatch"; +import { MetagenomicsBatchItem } from "packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem"; +import { MolecularAnalysisRunItem } from "packages/dina-ui/types/seqdb-api/resources/MolecularAnalysisRunItem"; export interface SangerSampleSelectionStepProps { pcrBatchId: string; @@ -29,10 +34,12 @@ export interface SangerSampleSelectionStepProps { setEditMode: (newValue: boolean) => void; performSave: boolean; setPerformSave: (newValue: boolean) => void; + metagenomicsBatch?: MetagenomicsBatch; } export function SangerSampleSelectionStep({ pcrBatchId, + metagenomicsBatch, editMode, onSaved, setEditMode, @@ -182,7 +189,6 @@ export function SangerSampleSelectionStep({ `seqdb-api/pcr-batch/${pcrBatchId}`, {} ); - // Convert to UUID arrays to compare the two arrays. const selectedResourceUUIDs = compact( selectedResources?.map((material) => material.id) @@ -212,9 +218,39 @@ export function SangerSampleSelectionStep({ ) ); + let molecularAnalysisRunId: string | undefined; + let metagenomicsBatchItemsResp: KitsuResponse< + MetagenomicsBatchItem[], + undefined + >; + + // If a MetagenomicsBatch exists + if (metagenomicsBatch && metagenomicsBatch.id) { + // Check for existing MolecularAnalysisRun before creating new MetagenomicsBatchItems + metagenomicsBatchItemsResp = await apiClient.get< + MetagenomicsBatchItem[] + >(`seqdb-api/metagenomics-batch-item`, { + filter: filterBy([], { + extraFilters: [ + { + selector: "metagenomicsBatch.uuid", + comparison: "==", + arguments: metagenomicsBatch.id + } + ] + })(""), + page: { limit: 1000 }, + include: + "molecularAnalysisRunItem,molecularAnalysisRunItem.run,pcrBatchItem" + }); + molecularAnalysisRunId = metagenomicsBatchItemsResp?.data?.find( + (item) => item?.molecularAnalysisRunItem?.run?.id + )?.molecularAnalysisRunItem?.run?.id; + } + // Perform create if (itemsToCreate.length !== 0) { - await save( + const pcrBatchItems = await save( itemsToCreate.map((materialUUID) => ({ resource: { type: "pcr-batch-item", @@ -234,10 +270,136 @@ export function SangerSampleSelectionStep({ })), { apiBaseUrl: "/seqdb-api" } ); + + // If a MolecularAnalysisRun exists, then we need to create new + // MolecularAnalysisRunItems for new MetagenomicsBatchItems. + let molecularRunItemsCreated: MolecularAnalysisRunItem[] = []; + if (molecularAnalysisRunId) { + molecularRunItemsCreated = await save( + itemsToCreate.map((_) => ({ + resource: { + type: "molecular-analysis-run-item", + usageType: "seq-reaction", + relationships: { + run: { + data: { + type: "molecular-analysis-run", + id: molecularAnalysisRunId + } + } + } + }, + type: "molecular-analysis-run-item" + })), + { apiBaseUrl: "/seqdb-api" } + ); + if (metagenomicsBatch && metagenomicsBatch.id) { + // Create MetagenomicsBatchItems for new PcrBatchItems + const metagenomicsBatchItemSaveArgs: SaveArgs[] = + pcrBatchItems.map((pcrBatchItem, index) => { + return { + type: "metagenomics-batch-item", + resource: { + type: "metagenomics-batch-item", + relationships: { + // Link back to existing MetagenomicsBatch + metagenomicsBatch: { + data: { + id: metagenomicsBatch.id, + type: "metagenomics-batch" + } + }, + // Link to new PcrBatchItem + pcrBatchItem: { + data: { + id: pcrBatchItem.id, + type: "pcr-batch-item" + } + }, + // Included only if molecular run items were created. + molecularAnalysisRunItem: + molecularRunItemsCreated.length > 0 && + molecularRunItemsCreated[index] + ? { + data: { + type: "molecular-analysis-run-item", + id: molecularRunItemsCreated[index].id + } + } + : undefined + } + } + }; + }); + const saveMetagenomicsBatchItems = + await save(metagenomicsBatchItemSaveArgs, { + apiBaseUrl: "/seqdb-api" + }); + } + } } // Perform deletes if (itemsToDelete.length !== 0) { + // Check if molecular analysis items need to be deleted. + if (molecularAnalysisRunId) { + // Delete the MetagenomicsBatchItems + await save( + itemsToDelete.map((item) => { + const metagenomicsBatchItem = + metagenomicsBatchItemsResp?.data?.find( + (metagenBatchItem) => + metagenBatchItem?.pcrBatchItem?.id === item.pcrBatchItemUUID + ); + return { + delete: { + id: metagenomicsBatchItem?.id ?? "", + type: "metagenomics-batch-item" + } + }; + }), + { apiBaseUrl: "/seqdb-api" } + ); + // Delete the molecular analysis run items. + const deletedMolecularAnalysisRunItemsResp = await save( + itemsToDelete.map((itemToDelete) => { + const molecularAnalysisRunItem: + | MolecularAnalysisRunItem + | undefined = metagenomicsBatchItemsResp?.data?.find( + (metagenomicsBatchItem) => + metagenomicsBatchItem?.pcrBatchItem?.id === + itemToDelete.pcrBatchItemUUID + )?.molecularAnalysisRunItem; + return { + delete: { + id: molecularAnalysisRunItem?.id ?? "", + type: "molecular-analysis-run-item" + } + }; + }), + { apiBaseUrl: "/seqdb-api" } + ); + + // Delete the run if all seq-reactions are being deleted. + if ( + itemsToDelete.length === + previouslySelectedResourcesUUIDs.length + + selectedResourceUUIDs.length + ) { + await save( + [ + { + delete: { + id: molecularAnalysisRunId, + type: "molecular-analysis-run" + } + } + ], + { apiBaseUrl: "/seqdb-api" } + ); + } + } + await save( itemsToDelete.map((item) => ({ delete: { diff --git a/packages/dina-ui/intl/seqdb-en.ts b/packages/dina-ui/intl/seqdb-en.ts index 585656d92..483ecb42e 100644 --- a/packages/dina-ui/intl/seqdb-en.ts +++ b/packages/dina-ui/intl/seqdb-en.ts @@ -198,5 +198,8 @@ export const SEQDB_MESSAGES_ENGLISH = { molecularAnalysisRunItems: "Molecular Analysis Run Items", molecularAnalysisWorkflowTitle: "Molecular Analysis Workflow", molecularAnalysisName: "Molecular Analysis Name", - molecularAnalysis: "Molecular Analysis" + molecularAnalysis: "Molecular Analysis", + metagenomicsWorkflowTitle: "Metagenomics Workflow", + metagenomicsWorkflowName: "Metagenomics Workflow Name", + metagenomicsBatch: "Metagenomics Batch" }; diff --git a/packages/dina-ui/pages/index.tsx b/packages/dina-ui/pages/index.tsx index 5d6f9a7c6..359825e6d 100644 --- a/packages/dina-ui/pages/index.tsx +++ b/packages/dina-ui/pages/index.tsx @@ -216,6 +216,11 @@ export function Home() { + + + + + diff --git a/packages/dina-ui/pages/seqdb/metagenomics-workflow/list.tsx b/packages/dina-ui/pages/seqdb/metagenomics-workflow/list.tsx new file mode 100644 index 000000000..d215ce6e2 --- /dev/null +++ b/packages/dina-ui/pages/seqdb/metagenomics-workflow/list.tsx @@ -0,0 +1,100 @@ +import { + ButtonBar, + ColumnDefinition, + dateCell, + FilterAttribute, + ListPageLayout +} from "common-ui"; +import Link from "next/link"; +import { + Footer, + groupCell, + GroupSelectField, + Head, + Nav +} from "../../../components"; +import { SeqdbMessage } from "../../../intl/seqdb-intl"; +import { useDinaIntl } from "../../../intl/dina-ui-intl"; +import { MetagenomicsBatch } from "../../../types/seqdb-api/resources/metagenomics/MetagenomicsBatch"; + +const TABLE_COLUMNS: ColumnDefinition[] = [ + { + cell: ({ + row: { + original: { id, name } + } + }) => ( + + {name || id} + + ), + accessorKey: "name" + }, + "primerForward.name", + "primerReverse.name", + groupCell("group"), + "createdBy", + dateCell("createdOn") +]; + +const FILTER_ATTRIBUTES: FilterAttribute[] = [ + "name", + "primerForward.name", + "primerReverse.name", + { + name: "createdOn", + type: "DATE" + }, + "createdBy" +]; + +export default function MetagenomicsWorkflowListPage() { + const { formatMessage } = useDinaIntl(); + + return ( +
+ +
+ ); +} diff --git a/packages/dina-ui/pages/seqdb/metagenomics-workflow/run.tsx b/packages/dina-ui/pages/seqdb/metagenomics-workflow/run.tsx new file mode 100644 index 000000000..7fd9b5603 --- /dev/null +++ b/packages/dina-ui/pages/seqdb/metagenomics-workflow/run.tsx @@ -0,0 +1,330 @@ +import { BackToListButton, LoadingSpinner } from "common-ui"; +import { PersistedResource } from "kitsu"; +import { useRouter } from "next/router"; +import { Tab, TabList, TabPanel, Tabs } from "react-tabs"; +import { SeqdbMessage, useSeqdbIntl } from "../../../intl/seqdb-intl"; +import PageLayout from "../../../components/page/PageLayout"; +import { useState, useEffect } from "react"; +import { Button, Spinner } from "react-bootstrap"; +import { DinaMessage } from "../../../intl/dina-ui-intl"; +import React from "react"; +import { useMetagenomicsBatchQuery } from "../../../components/seqdb/metagenomics-workflow/useMetagenomicsBatchQuery"; +import { MetagenomicsBatch } from "../../../types/seqdb-api/resources/metagenomics/MetagenomicsBatch"; +import { MetagenomicsBatchDetailsStep } from "../../../components/seqdb/metagenomics-workflow/MetagenomicsBatchDetailsStep"; +import { MetagenomicsIndexAssignmentStep } from "../../../components/seqdb/metagenomics-workflow/MetagenomicsIndexAssignmentStep"; +import { MetagenomicsRunStep } from "../../../components/seqdb/metagenomics-workflow/MetagenomicsRunStep"; +import { usePcrBatchQuery } from "../pcr-batch/edit"; +import { SangerPcrBatchItemGridStep } from "../../../components/seqdb/pcr-workflow/pcr-batch-plating-step/SangerPcrBatchItemGridStep"; +import { SangerPcrBatchStep } from "../../../components/seqdb/pcr-workflow/SangerPcrBatchStep"; +import { SangerPcrReactionStep } from "../../../components/seqdb/pcr-workflow/SangerPcrReactionStep"; +import { SangerSampleSelectionStep } from "../../../components/seqdb/pcr-workflow/SangerSampleSelectionStep"; +import { PcrBatch } from "../../../types/seqdb-api"; + +export default function MetagenomicWorkflowRunPage() { + const router = useRouter(); + const { formatMessage } = useSeqdbIntl(); + + // Current step being used. + const [currentStep, setCurrentStep] = useState( + router.query.step ? Number(router.query.step) : 0 + ); + + // Global edit mode state. + const [editMode, setEditMode] = useState( + router.query.editMode === "true" + ); + + // Request saving to be performed. + const [performSave, setPerformSave] = useState(false); + + // Request completion to be performed. + const [performComplete, setPerformComplete] = useState(false); + + const [reloadPcrBatch, setReloadPcrBatch] = useState(Date.now()); + + // Loaded PCR Batch ID. + const [pcrBatchId, setPcrBatchId] = useState( + router.query.pcrBatchId?.toString() + ); + + // Loaded PCR Batch. + const pcrBatchQuery = usePcrBatchQuery(pcrBatchId, [ + pcrBatchId, + currentStep, + reloadPcrBatch + ]); + + // Used to determine if the resource needs to be reloaded. + const [reloadMetagenomicsBatch, setReloadMetagenomicsBatch] = + useState(Date.now()); + + // Loaded resource id + const [metagenomicsBatchId, setMetagenomicsBatchId] = useState< + string | undefined + >(router.query.metagenomicsBatchId?.toString()); + + // Loaded resource + const metagenomicsBatchQuery = useMetagenomicsBatchQuery( + metagenomicsBatchId, + pcrBatchId, + setMetagenomicsBatchId, + [metagenomicsBatchId, currentStep, reloadMetagenomicsBatch] + ); + + // Update the URL to contain the current step. + useEffect(() => { + router.push({ + pathname: router.pathname, + query: { ...router.query, step: currentStep } + }); + }, [currentStep]); + + async function onSavedMetagenomicsBatch( + nextStep: number, + metagenomicsBatchSaved?: PersistedResource + ) { + setCurrentStep(nextStep); + if (metagenomicsBatchSaved) { + setMetagenomicsBatchId(metagenomicsBatchSaved.id); + } + await router.push({ + pathname: router.pathname, + query: { + ...router.query, + metagenomicsBatchId: metagenomicsBatchSaved + ? metagenomicsBatchSaved.id + : metagenomicsBatchId, + step: "" + nextStep + } + }); + } + + async function onSavedPcrBatch( + nextStep: number, + pcrBatchSaved?: PersistedResource + ) { + setCurrentStep(nextStep); + if (pcrBatchSaved) { + setPcrBatchId(pcrBatchSaved.id); + } + await router.push({ + pathname: router.pathname, + query: { + ...router.query, + pcrBatchId: pcrBatchSaved ? pcrBatchSaved.id : pcrBatchId, + step: "" + nextStep + } + }); + } + + const buttonBarContent = ( + <> +
+ +
+ {editMode ? ( + <> + + + + + ) : ( + + )} + + ); + + // Helper function to determine if a step should be disabled. + const isDisabled = ( + stepNumber: number, + pcrBatchRequired: boolean, + metagenomicsBatchRequired: boolean + ) => { + // While in edit mode, other steps should be disabled. + if (editMode && stepNumber !== currentStep) { + return true; + } + + // If a metagenomics batch is required, and not provided then this step should be disabled. + if (metagenomicsBatchRequired && !metagenomicsBatchId) { + return true; + } + // If a PCR Batch is required, and not provided then this step should be disabled. + if (pcrBatchRequired && !pcrBatchId) { + return true; + } + // Not disabled. + return false; + }; + function isLoading() { + return metagenomicsBatchQuery.loading || pcrBatchQuery.loading; + } + return isLoading() ? ( + + ) : ( + + + + + {formatMessage("pcrBatch")} + + + {formatMessage("selectMaterialSamples")} + + + {formatMessage("selectCoordinates")} + + + {formatMessage("pcrReaction")} + + + {formatMessage("metagenomicsBatch")} + + + {formatMessage("indexAssignmentStep")} + + + {formatMessage("runStep")} + + + + + + + {pcrBatchId && ( + + )} + + + {pcrBatchQuery.response?.data && pcrBatchId && ( + + )} + + + {pcrBatchQuery.response?.data && pcrBatchId && ( + + )} + + + {pcrBatchQuery?.response?.data && ( + + )} + + + {metagenomicsBatchQuery.response?.data && + metagenomicsBatchId && + pcrBatchQuery?.response?.data && + pcrBatchId && ( + + )} + + + {metagenomicsBatchQuery.response?.data && metagenomicsBatchId && ( + + )} + + + + ); +} diff --git a/packages/dina-ui/pages/seqdb/pcr-batch/edit.tsx b/packages/dina-ui/pages/seqdb/pcr-batch/edit.tsx index ffa0b22c5..8b23e51e4 100644 --- a/packages/dina-ui/pages/seqdb/pcr-batch/edit.tsx +++ b/packages/dina-ui/pages/seqdb/pcr-batch/edit.tsx @@ -108,6 +108,7 @@ export interface PcrBatchFormProps { onSaved: (resource: PersistedResource) => Promise; buttonBar?: ReactNode; readOnlyOverride?: boolean; + isMetagenomicsWorkflow?: boolean; } export function PcrBatchForm({ @@ -123,7 +124,8 @@ export function PcrBatchForm({
), - readOnlyOverride + readOnlyOverride, + isMetagenomicsWorkflow }: PcrBatchFormProps) { const { username } = useAccount(); const { doOperations } = useApiClient(); @@ -131,7 +133,8 @@ export function PcrBatchForm({ const initialValues = pcrBatch || { // TODO let the back-end set this: createdBy: username, - type: "pcr-batch" + type: "pcr-batch", + batchType: isMetagenomicsWorkflow ? "illumina_metagenomics" : undefined }; async function savePcrReactionResults(submittedValues: any) { @@ -235,6 +238,7 @@ export function PcrBatchForm({ readOnly: readOnlyOverride }} buttonBar={buttonBar as any} + isMetagenomicsWorkflow={isMetagenomicsWorkflow} /> ); } @@ -242,11 +246,13 @@ export function PcrBatchForm({ interface LoadExternalDataForPcrBatchFormProps { dinaFormProps: DinaFormProps; buttonBar?: ReactNode; + isMetagenomicsWorkflow?: boolean; } export function LoadExternalDataForPcrBatchForm({ dinaFormProps, - buttonBar + buttonBar, + isMetagenomicsWorkflow }: LoadExternalDataForPcrBatchFormProps) { const { loading: loadingReactionData, @@ -275,6 +281,7 @@ export function LoadExternalDataForPcrBatchForm({ ); @@ -283,12 +290,14 @@ export function LoadExternalDataForPcrBatchForm({ interface PcrBatchFormFieldsProps { pcrBatchItems: PcrBatchItem[]; materialSamples: MaterialSampleSummary[]; + isMetagenomicsWorkflow?: boolean; } /** Re-usable field layout between edit and view pages. */ function PcrBatchFormFields({ pcrBatchItems, - materialSamples + materialSamples, + isMetagenomicsWorkflow }: PcrBatchFormFieldsProps) { const { readOnly, initialValues } = useDinaFormContext(); const { values } = useFormikContext(); @@ -359,6 +368,7 @@ function PcrBatchFormFields({ className="col-md-6" name="batchType" path="seqdb-api/vocabulary/pcrBatchType" + isDisabled={isMetagenomicsWorkflow} /> diff --git a/packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatch.ts b/packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatch.ts new file mode 100644 index 000000000..6a691d862 --- /dev/null +++ b/packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatch.ts @@ -0,0 +1,20 @@ +import { KitsuResource } from "kitsu"; +import { IndexSet } from "../ngs-workflow/IndexSet"; +import { Protocol } from "../../../collection-api"; + +export interface MetagenomicsBatchAttributes { + type: "metagenomics-batch"; + name: string; + group?: string; + createdBy?: string; + createdOn?: string; +} + +export interface MetagenomicsBatchRelationships { + protocol?: Protocol; + indexSet?: IndexSet; +} + +export type MetagenomicsBatch = KitsuResource & + MetagenomicsBatchAttributes & + MetagenomicsBatchRelationships; diff --git a/packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem.ts b/packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem.ts new file mode 100644 index 000000000..576ddfb85 --- /dev/null +++ b/packages/dina-ui/types/seqdb-api/resources/metagenomics/MetagenomicsBatchItem.ts @@ -0,0 +1,23 @@ +import { KitsuResource } from "kitsu"; +import { NgsIndex } from "../ngs-workflow/NgsIndex"; +import { MetagenomicsBatch } from "./MetagenomicsBatch"; +import { PcrBatchItem } from "../PcrBatchItem"; +import { MolecularAnalysisRunItem } from "../MolecularAnalysisRunItem"; + +export interface MetagenomicsBatchItemAttributes { + type: "metagenomics-batch-item"; + createdBy?: string; + createdOn?: string; +} + +export interface MetagenomicsBatchItemRelationships { + metagenomicsBatch?: MetagenomicsBatch; + indexI5?: NgsIndex; + indexI7?: NgsIndex; + pcrBatchItem?: PcrBatchItem; + molecularAnalysisRunItem?: MolecularAnalysisRunItem; +} + +export type MetagenomicsBatchItem = KitsuResource & + MetagenomicsBatchItemAttributes & + MetagenomicsBatchItemRelationships;