Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding a new operation visium QC operation : qPcr results #536

Merged
merged 12 commits into from
Jan 25, 2024
73 changes: 73 additions & 0 deletions cypress/e2e/pages/visiumQC.cy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
RecordOpWithSlotCommentsMutation,
RecordOpWithSlotCommentsMutationVariables,
RecordOpWithSlotMeasurementsMutation,
RecordOpWithSlotMeasurementsMutationVariables,
RecordVisiumQcMutation,
RecordVisiumQcMutationVariables,
SlideCosting
Expand Down Expand Up @@ -574,6 +576,77 @@ describe('Visium QC Page', () => {
});
});

describe('When selecting qPCR Results for QCType', () => {
before(() => {
cy.reload();
selectOption('qcType', 'qPCR results');
cy.get('#labwareScanInput').type('STAN-2100{enter}');
});

context('When a user enters a global CQ value', () => {
before(() => {
cy.findByTestId('all-Cq value').type('5');
});
it('updates all the Cq value inputs inside the table related to the slots', () => {
cy.findAllByTestId('Cq value-input').should('have.value', 5);
});
});

describe('On Save', () => {
it('Save button should be disabled when there is no SGP number', () => {
selectSGPNumber('');
cy.findByRole('button', { name: /Save/i }).should('be.disabled');
});

context('When there is no server error', () => {
before(() => {
selectSGPNumber('SGP1008');
cy.findByRole('button', { name: /Save/i }).should('not.be.disabled').click();
});

it('shows a success message', () => {
cy.findByText('qPCR results complete').should('be.visible');
});
after(() => {
cy.findByRole('button', { name: /Reset/i }).click();
});
});
context('With server failure', () => {
before(() => {
cy.msw().then(({ worker, graphql }) => {
worker.use(
graphql.mutation<RecordOpWithSlotMeasurementsMutation, RecordOpWithSlotMeasurementsMutationVariables>(
'RecordOpWithSlotMeasurements',
(req, res, ctx) => {
return res.once(
ctx.errors([
{
message: 'Exception while fetching data : The operation could not be validated.',
extensions: {
problems: ['Labware is discarded: [STAN-2100]']
}
}
])
);
}
)
);
});
cy.reload();
selectSGPNumber('SGP1008');
selectOption('qcType', 'qPCR results');
cy.get('#labwareScanInput').type('STAN-2100{enter}');
cy.findByTestId('all-Cq value').type('5');
cy.findByRole('button', { name: /Save/i }).click();
});

it('shows an error', () => {
cy.findByText('Failed to record qPCR results').should('be.visible');
});
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't find a test case when there is server error.


function saveButton() {
return cy.findByRole('button', { name: /Save/i });
}
Expand Down
91 changes: 74 additions & 17 deletions src/components/slotMeasurement/SlotMeasurements.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import React from 'react';
import { CommentFieldsFragment, SlotMeasurementRequest } from '../../types/sdk';
import { CommentFieldsFragment, SampleFieldsFragment, SlotMeasurementRequest } from '../../types/sdk';
import { Row } from 'react-table';
import DataTable from '../DataTable';
import FormikInput from '../forms/Input';
import { selectOptionValues } from '../forms';
import CustomReactSelect, { OptionType } from '../forms/CustomReactSelect';
import { Dictionary, groupBy } from 'lodash';
import { TableCell } from '../Table';

export type MeasurementConfigProps = {
name: string;
stepIncrement: string;
validateFunction?: (value: string) => void;
initialMeasurementVal: string;
};

export interface SlotMeasurement extends SlotMeasurementRequest {
samples?: SampleFieldsFragment[];
}
export type SlotMeasurementProps = {
slotMeasurements: SlotMeasurementRequest[];
slotMeasurements: SlotMeasurement[];
measurementConfig: MeasurementConfigProps[];
comments?: CommentFieldsFragment[];
onChangeField: (fieldName: string, value: string) => void;
Expand All @@ -23,6 +28,7 @@ export type SlotMeasurementProps = {
type MeasurementRow = {
address: string;
measurements: SlotMeasurementRequest[];
samples?: SampleFieldsFragment[];
};
const setMeasurementNameTableTitle = (measurementName: string): string => {
return measurementName === 'cDNA concentration' || measurementName === 'Library concentration'
Expand All @@ -40,28 +46,42 @@ const setMeasurementNameTableTitle = (measurementName: string): string => {
*/

const SlotMeasurements = ({ slotMeasurements, measurementConfig, onChangeField, comments }: SlotMeasurementProps) => {
const [measurementRows, setMeasurementRows] = React.useState<MeasurementRow[]>([]);
const [measurementConfigOptions, setMeasurementConfigOptions] = React.useState<MeasurementConfigProps[]>([]);

/**concatenate all mesaurements if there are multiple measurements */
const isWithSampleInfo = React.useMemo(
() => slotMeasurements.some((measurement) => measurement.samples),
[slotMeasurements]
);

/**concatenate all mesaurements if there are multiple measurements */
React.useEffect(() => {
if (measurementConfigOptions.length === measurementConfig.length) return;
setMeasurementConfigOptions(measurementConfig);
}, [measurementConfig, measurementConfigOptions, setMeasurementConfigOptions]);

React.useEffect(() => {
const groupedMeasurements: Dictionary<SlotMeasurementRequest[]> = groupBy(slotMeasurements, 'address');
if (Object.keys(groupedMeasurements).length === measurementRows?.length) return;
setMeasurementRows(
Object.keys(groupedMeasurements).map((address) => {
return {
const measurementRowValues: MeasurementRow[] = React.useMemo(() => {
const groupedMeasurements: Dictionary<SlotMeasurement[]> = groupBy(slotMeasurements, 'address');
const values: MeasurementRow[] = [];
if (isWithSampleInfo) {
for (const address in groupedMeasurements) {
groupedMeasurements[address].forEach((measurement) => {
values.push({
address,
measurements: groupedMeasurements[address],
samples: measurement.samples
});
});
}
} else {
for (const address in groupedMeasurements) {
values.push({
address,
measurements: groupedMeasurements[address]
};
})
);
}, [slotMeasurements, measurementRows, setMeasurementRows]);
});
}
}
return values;
}, [slotMeasurements, isWithSampleInfo]);

const columns = React.useMemo(() => {
return [
Expand All @@ -70,7 +90,44 @@ const SlotMeasurements = ({ slotMeasurements, measurementConfig, onChangeField,
id: 'address',
accessor: (measurement: MeasurementRow) => measurement.address
},

...(isWithSampleInfo
? [
{
Header: 'External ID',
id: 'externalId',
Cell: ({ row }: { row: Row<MeasurementRow> }) => {
return (
<TableCell>
{row.original.samples?.map((sample) => {
return (
<div className="flex px-6">
<label>{sample.tissue.externalName}</label>
</div>
);
})}
</TableCell>
);
}
},
{
Header: 'Section Number',
id: 'sectionNumber',
Cell: ({ row }: { row: Row<MeasurementRow> }) => {
return (
<TableCell>
{row.original.samples?.map((sample) => {
return (
<div className="flex items-right px-6">
<label>{sample.section}</label>
</div>
);
})}
</TableCell>
);
}
}
]
: []),
...measurementConfigOptions.map((measurementProp, mesaurementIndex) => {
return {
Header: setMeasurementNameTableTitle(measurementProp.name),
Expand Down Expand Up @@ -129,13 +186,13 @@ const SlotMeasurements = ({ slotMeasurements, measurementConfig, onChangeField,
]
: [])
];
}, [comments, onChangeField, measurementConfigOptions]);
}, [comments, onChangeField, measurementConfigOptions, isWithSampleInfo]);

return (
<>
{slotMeasurements && slotMeasurements.length > 0 && (
<>
<DataTable columns={columns} data={measurementRows ?? []} />
<DataTable columns={columns} data={measurementRowValues ?? []} />
</>
)}
</>
Expand Down
4 changes: 2 additions & 2 deletions src/components/visiumQC/Amplification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import SlotMeasurements, { MeasurementConfigProps } from '../slotMeasurement/Slo
import { useFormikContext } from 'formik';
import { VisiumQCFormData } from '../../pages/VisiumQC';

export type AmplificationProps = {
export type CDNAProps = {
labware: LabwareFlaggedFieldsFragment;
slotMeasurements: SlotMeasurementRequest[] | undefined;
removeLabware: (barcode: string) => void;
};

const Amplification = ({ labware, slotMeasurements, removeLabware }: AmplificationProps) => {
const Amplification = ({ labware, slotMeasurements, removeLabware }: CDNAProps) => {
const { values, setErrors, setTouched, setFieldValue } = useFormikContext<VisiumQCFormData>();

const memoMeasurementConfig: MeasurementConfigProps[] = React.useMemo(
Expand Down
Loading
Loading