From ebdfc849421639ab07bc342aaf78ed689b668b14 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:14:50 -0500 Subject: [PATCH 1/4] fix(protocol-designer): editing labware on top of a module and adapter fixes (#17161) address RQA-3777 RQA-3804 --- .../localization/en/protocol_steps.json | 3 +- .../Designer/DeckSetup/DeckSetupTools.tsx | 56 ++++++++++++++++--- .../__tests__/DeckSetupTools.test.tsx | 4 +- .../Designer/ProtocolSteps/StepSummary.tsx | 22 ++++---- .../src/step-forms/actions/thunks.ts | 55 +++++++++++++++++- 5 files changed, 118 insertions(+), 22 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 2f498904c61..0ed3f34caf8 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -143,5 +143,6 @@ "volume_per_well": "Volume per well", "well_name": "Well {{wellName}}", "well_order_title": "{{prefix}} well order", - "well_position": "Well position: X {{x}} Y {{y}} Z {{z}} (mm)" + "well_position": "Well position: X {{x}} Y {{y}} Z {{z}} (mm)", + "unknown_module": "Unknown module" } diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 48b0ef87bba..d9c2a84a95b 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -33,7 +33,8 @@ import { createDeckFixture, deleteDeckFixture, } from '../../../step-forms/actions/additionalItems' -import { createModule, deleteModule } from '../../../step-forms/actions' +import { deleteModule } from '../../../step-forms/actions' +import { getSavedStepForms } from '../../../step-forms/selectors' import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { createContainer, @@ -50,9 +51,12 @@ import { useBlockingHint } from '../../../organisms/BlockingHintModal/useBlockin import { selectors } from '../../../labware-ingred/selectors' import { useKitchen } from '../../../organisms/Kitchen/hooks' import { getDismissedHints } from '../../../tutorial/selectors' -import { createContainerAboveModule } from '../../../step-forms/actions/thunks' -import { ConfirmDeleteStagingAreaModal } from '../../../organisms' +import { + createContainerAboveModule, + createModuleEntityAndChangeForm, +} from '../../../step-forms/actions/thunks' import { BUTTON_LINK_STYLE } from '../../../atoms' +import { ConfirmDeleteStagingAreaModal } from '../../../organisms' import { getSlotInformation } from '../utils' import { ALL_ORDERED_CATEGORIES, FIXTURES, MOAM_MODELS } from './constants' import { LabwareTools } from './LabwareTools' @@ -63,6 +67,13 @@ import type { AddressableAreaName, ModuleModel } from '@opentrons/shared-data' import type { ThunkDispatch } from '../../../types' import type { Fixture } from './constants' +const mapModTypeToStepType: Record = { + heaterShakerModuleType: 'heaterShaker', + magneticModuleType: 'magnet', + temperatureModuleType: 'temperature', + thermocyclerModuleType: 'thermocycler', +} + interface DeckSetupToolsProps { onCloseClick: () => void setHoveredLabware: (defUri: string | null) => void @@ -80,6 +91,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const { makeSnackbar } = useKitchen() const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) const robotType = useSelector(getRobotType) + const savedSteps = useSelector(getSavedStepForms) const [showDeleteLabwareModal, setShowDeleteLabwareModal] = useState< ModuleModel | 'clear' | null >(null) @@ -255,7 +267,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { if ( createdLabwareForSlot != null && (!keepExistingLabware || - createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri) + createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri || + // if nested labware changes but labware doesn't, still delete both + (createdLabwareForSlot.labwareDefURI === selectedLabwareDefUri && + createdNestedLabwareForSlot?.labwareDefURI !== + selectedNestedLabwareDefUri)) ) { dispatch(deleteContainer({ labwareId: createdLabwareForSlot.id })) } @@ -309,11 +325,33 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } if (selectedModuleModel != null) { // create module + const moduleType = getModuleType(selectedModuleModel) + + const moduleSteps = Object.values(savedSteps).filter(step => { + return ( + step.stepType === mapModTypeToStepType[moduleType] && + // only update module steps that match the old moduleId + // to accommodate instances of MoaM + step.moduleId === createdModuleForSlot?.id + ) + }) + + const pauseSteps = Object.values(savedSteps).filter(step => { + return ( + step.stepType === 'pause' && + // only update pause steps that match the old moduleId + // to accommodate instances of MoaM + step.moduleId === createdModuleForSlot?.id + ) + }) + dispatch( - createModule({ + createModuleEntityAndChangeForm({ slot, - type: getModuleType(selectedModuleModel), + type: moduleType, model: selectedModuleModel, + moduleSteps, + pauseSteps, }) ) } @@ -344,7 +382,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { if ( selectedModuleModel != null && selectedLabwareDefUri != null && - createdLabwareForSlot?.labwareDefURI !== selectedLabwareDefUri + (createdLabwareForSlot?.labwareDefURI !== selectedLabwareDefUri || + // if nested labware changes but labware doesn't, still create both both + (createdLabwareForSlot.labwareDefURI === selectedLabwareDefUri && + createdNestedLabwareForSlot?.labwareDefURI !== + selectedNestedLabwareDefUri)) ) { // create adapter + labware on module dispatch( diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index 5eab480710e..1c3a7c6a472 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -10,6 +10,7 @@ import { i18n } from '../../../../assets/localization' import { renderWithProviders } from '../../../../__testing-utils__' import { deleteContainer } from '../../../../labware-ingred/actions' import { deleteModule } from '../../../../step-forms/actions' +import { getSavedStepForms } from '../../../../step-forms/selectors' import { getRobotType } from '../../../../file-data/selectors' import { getEnableAbsorbanceReader } from '../../../../feature-flags/selectors' import { deleteDeckFixture } from '../../../../step-forms/actions/additionalItems' @@ -19,7 +20,6 @@ import { getDeckSetupForActiveItem } from '../../../../top-selectors/labware-loc import { DeckSetupTools } from '../DeckSetupTools' import { LabwareTools } from '../LabwareTools' -import type * as React from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' vi.mock('../LabwareTools') @@ -31,6 +31,7 @@ vi.mock('../../../../step-forms/actions') vi.mock('../../../../step-forms/actions/additionalItems') vi.mock('../../../../labware-ingred/selectors') vi.mock('../../../../tutorial/selectors') +vi.mock('../../../../step-forms/selectors') const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -65,6 +66,7 @@ describe('DeckSetupTools', () => { additionalEquipmentOnDeck: {}, pipettes: {}, }) + vi.mocked(getSavedStepForms).mockReturnValue({}) vi.mocked(getDismissedHints).mockReturnValue([]) }) afterEach(() => { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx index f97d73e469f..19a648e11b0 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx @@ -79,6 +79,7 @@ interface StepSummaryProps { export function StepSummary(props: StepSummaryProps): JSX.Element | null { const { currentStep, stepDetails } = props const { t } = useTranslation(['protocol_steps', 'application']) + const unknownModule = t('unkonwn_module') const labwareNicknamesById = useSelector(getLabwareNicknamesById) const additionalEquipmentEntities = useSelector( getAdditionalEquipmentEntities @@ -124,9 +125,8 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { engageHeight, magnetAction, } = currentStep - const magneticModuleDisplayName = getModuleDisplayName( - modules[magneticModuleId].model - ) + const magneticModuleDisplayName = + getModuleDisplayName(modules[magneticModuleId]?.model) ?? unknownModule stepSummaryContent = magnetAction === 'engage' ? ( diff --git a/protocol-designer/src/step-forms/actions/thunks.ts b/protocol-designer/src/step-forms/actions/thunks.ts index 0a0f5f17227..65079b0fe2d 100644 --- a/protocol-designer/src/step-forms/actions/thunks.ts +++ b/protocol-designer/src/step-forms/actions/thunks.ts @@ -1,12 +1,21 @@ import { createContainer } from '../../labware-ingred/actions' import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' +import { uuid } from '../../utils' +import { changeSavedStepForm } from '../../steplist/actions' -import type { DeckSlotId } from '@opentrons/shared-data' +import type { + DeckSlotId, + ModuleModel, + ModuleType, +} from '@opentrons/shared-data' import type { ThunkAction } from '../../types' import type { CreateContainerAction, RenameLabwareAction, } from '../../labware-ingred/actions' +import type { CreateModuleAction } from './modules' +import type { ChangeSavedStepFormAction } from '../../steplist/actions' +import type { FormData } from '../../form-types' export interface CreateContainerAboveModuleArgs { slot: DeckSlotId @@ -37,3 +46,47 @@ export const createContainerAboveModule: ( }) ) } + +interface ModuleAndChangeFormArgs { + slot: DeckSlotId + type: ModuleType + model: ModuleModel + moduleSteps: FormData[] + pauseSteps: FormData[] +} +export const createModuleEntityAndChangeForm: ( + args: ModuleAndChangeFormArgs +) => ThunkAction = args => ( + dispatch, + getState +) => { + const { slot, model, type, moduleSteps, pauseSteps } = args + const moduleId = `${uuid()}:${type}` + + dispatch({ + type: 'CREATE_MODULE', + payload: { slot, model, type, id: moduleId }, + }) + + // if steps are created with the module that has been regenerated, migrate them to use the correct moduleId + moduleSteps.forEach(step => { + dispatch( + changeSavedStepForm({ + stepId: step.id, + update: { + moduleId, + }, + }) + ) + }) + pauseSteps.forEach(step => { + dispatch( + changeSavedStepForm({ + stepId: step.id, + update: { + moduleId, + }, + }) + ) + }) +} From 20e1dd3810c6eef8386a30a36d69c33c15636d67 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:29:13 -0500 Subject: [PATCH 2/4] =?UTF-8?q?fix(protocol-designer):=20allow=20minutesSe?= =?UTF-8?q?conds=20timer=20fields=20to=20add=20more=E2=80=A6=20(#17185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … than 99 minutes closes RQA-3817 --- protocol-designer/src/steplist/fieldLevel/errors.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/steplist/fieldLevel/errors.ts b/protocol-designer/src/steplist/fieldLevel/errors.ts index 7405f097643..892e7f0a11a 100644 --- a/protocol-designer/src/steplist/fieldLevel/errors.ts +++ b/protocol-designer/src/steplist/fieldLevel/errors.ts @@ -34,7 +34,7 @@ export type ErrorChecker = (value: unknown) => string | null export const requiredField: ErrorChecker = (value: unknown) => !value ? FIELD_ERRORS.REQUIRED : null export const isTimeFormat: ErrorChecker = (value: unknown): string | null => { - const timeRegex = new RegExp(/^\d{1,2}:\d{1,2}:\d{1,2}$/g) + const timeRegex = new RegExp(/^\d{1,2}:(?:[0-5]?\d):(?:[0-5]?\d)$/g) return (typeof value === 'string' && timeRegex.test(value)) || !value ? null : FIELD_ERRORS.BAD_TIME_HMS @@ -42,7 +42,7 @@ export const isTimeFormat: ErrorChecker = (value: unknown): string | null => { export const isTimeFormatMinutesSeconds: ErrorChecker = ( value: unknown ): string | null => { - const timeRegex = new RegExp(/^\d{1,2}:\d{1,2}$/g) + const timeRegex = new RegExp(/^\d+:[0-5]?\d$/g) return (typeof value === 'string' && timeRegex.test(value)) || !value ? null : FIELD_ERRORS.BAD_TIME_MS From 7fa13097016205c72efe6b5d011e7624706fda33 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:33:55 -0500 Subject: [PATCH 3/4] =?UTF-8?q?fix(step-generation):=20configureForVolume?= =?UTF-8?q?=20volume=20correctly=20set=20to=20am=E2=80=A6=20(#17189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ount closes RESC-366 Previously, the configure for volume volume was set to the transfer's total volume and not the chunk's volume. But this pr changed it so the volume was set to the chunk number! This pr fixes it so the volume is set to the actual volume --- .../fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json | 2 +- step-generation/src/commandCreators/compound/transfer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json b/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json index 9c6d24abaca..8b8d129418f 100644 --- a/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json +++ b/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json @@ -3624,7 +3624,7 @@ "key": "dd1b37d6-eb8c-4616-bc8a-e1f17556234a", "params": { "pipetteId": "21087f15-4c03-4587-8a2b-1ba0b5a501a0", - "volume": 1 + "volume": 10 } }, { diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index e892dc94b3d..18f99b7e036 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -263,7 +263,7 @@ export const transfer: CommandCreator = ( ? [ curryCommandCreator(configureForVolume, { pipetteId: args.pipette, - volume: chunksPerSubTransfer, + volume: subTransferVol, }), ] : [] From 9bced85d6c314df91cf22c0ad2479facd4e48d5d Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:50:24 -0500 Subject: [PATCH 4/4] fix(protocol-designer): allow custom labware on adapters (#17217) closes RQA-3779 --- .../src/pages/Designer/DeckSetup/LabwareTools.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx index be4f457429e..099416f17c3 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx @@ -430,10 +430,15 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { ) } ) - : getLabwareCompatibleWithAdapter( - loadName - ).map(nestedDefUri => { - const nestedDef = defs[nestedDefUri] + : [ + ...getLabwareCompatibleWithAdapter( + loadName + ), + ...Object.keys(customLabwareDefs), + ].map(nestedDefUri => { + const nestedDef = + defs[nestedDefUri] ?? + customLabwareDefs[nestedDefUri] return (