From 0526f555dd102ec4ba6f9193feab3707d9ac8a46 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 3 May 2024 14:58:01 -0400 Subject: [PATCH 1/2] fix(app): disable module calibration if heating or cooling (#15054) closes [RQA-2630](https://opentrons.atlassian.net/browse/RQA-2630) --- .../localization/en/module_wizard_flows.json | 1 + .../ModuleCard/ModuleOverflowMenu.tsx | 42 +++++++++++++++++-- .../__tests__/ModuleOverflowMenu.test.tsx | 40 ++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/app/src/assets/localization/en/module_wizard_flows.json b/app/src/assets/localization/en/module_wizard_flows.json index a502b0ef7976..940367641c21 100644 --- a/app/src/assets/localization/en/module_wizard_flows.json +++ b/app/src/assets/localization/en/module_wizard_flows.json @@ -26,6 +26,7 @@ "location_occupied": "A {{fixture}} is currently specified here on the deck configuration", "module_calibrating": "Stand back, {{moduleName}} is calibrating", "module_calibration": "Module calibration", + "module_heating_or_cooling": "Module calibration cannot proceed while heating or cooling", "module_secured": "The module must be fully secured in its caddy and secured in the deck slot.", "module_too_hot": "Module is too hot to proceed to module calibration", "move_gantry_to_front": "Move gantry to front", diff --git a/app/src/organisms/ModuleCard/ModuleOverflowMenu.tsx b/app/src/organisms/ModuleCard/ModuleOverflowMenu.tsx index d7ece8d4c0ab..28744a218cf7 100644 --- a/app/src/organisms/ModuleCard/ModuleOverflowMenu.tsx +++ b/app/src/organisms/ModuleCard/ModuleOverflowMenu.tsx @@ -3,7 +3,12 @@ import { useTranslation } from 'react-i18next' import { Flex, POSITION_RELATIVE, useHoverTooltip } from '@opentrons/components' -import { MODULE_MODELS_OT2_ONLY } from '@opentrons/shared-data' +import { + HEATERSHAKER_MODULE_TYPE, + MODULE_MODELS_OT2_ONLY, + TEMPERATURE_MODULE_TYPE, + THERMOCYCLER_MODULE_TYPE, +} from '@opentrons/shared-data' import { MenuList } from '../../atoms/MenuList' import { Tooltip } from '../../atoms/Tooltip' import { MenuItem } from '../../atoms/MenuList/MenuItem' @@ -69,6 +74,23 @@ export const ModuleOverflowMenu = ( isDisabled = true } + let isHeatingOrCooling + switch (module.moduleType) { + case TEMPERATURE_MODULE_TYPE: + isHeatingOrCooling = module.data.status !== 'idle' + break + case HEATERSHAKER_MODULE_TYPE: + isHeatingOrCooling = module.data.temperatureStatus !== 'idle' + break + case THERMOCYCLER_MODULE_TYPE: + isHeatingOrCooling = + module.data.lidTemperatureStatus !== 'idle' || + module.data.status !== 'idle' + break + default: + isHeatingOrCooling = false + } + const { menuOverflowItemsByModuleType } = useModuleOverflowMenu( module, handleAboutClick, @@ -79,6 +101,18 @@ export const ModuleOverflowMenu = ( isIncompatibleWithOT3 ) + const isCalibrateDisabled = !isPipetteReady || isTooHot || isHeatingOrCooling + let calibrateDisabledReason + if (!isPipetteReady) { + calibrateDisabledReason = t('calibrate_pipette') + } else if (isTooHot) { + calibrateDisabledReason = t('module_too_hot') + } else if (isHeatingOrCooling) { + calibrateDisabledReason = t('module_heating_or_cooling') + } else { + calibrateDisabledReason = null + } + return ( @@ -89,7 +123,7 @@ export const ModuleOverflowMenu = ( <> {i18n.format( @@ -99,9 +133,9 @@ export const ModuleOverflowMenu = ( 'capitalize' )} - {!isPipetteReady || isTooHot ? ( + {isCalibrateDisabled ? ( - {t(!isPipetteReady ? 'calibrate_pipette' : 'module_too_hot')} + {calibrateDisabledReason} ) : null} diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx index f749237e6e06..57c51f4d2f98 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx @@ -19,6 +19,8 @@ import { import { useCurrentRunId } from '../../ProtocolUpload/hooks' import { ModuleOverflowMenu } from '../ModuleOverflowMenu' +import type { TemperatureStatus } from '@opentrons/api-client' + vi.mock('../../Devices/hooks') vi.mock('../../RunTimeControl/hooks') vi.mock('../../ProtocolUpload/hooks') @@ -535,6 +537,44 @@ describe('ModuleOverflowMenu', () => { expect(calibrate).toBeDisabled() }) + it('renders a disabled calibrate button if module is heating or cooling', () => { + vi.mocked(useIsFlex).mockReturnValue(true) + const mockHeatingModule = { + ...mockHeaterShaker, + data: { + ...mockHeaterShaker.data, + temperatureStatus: 'heating' as TemperatureStatus, + }, + } + props = { + ...props, + module: mockHeatingModule, + } + render(props) + + const calibrate = screen.getByRole('button', { name: 'Calibrate' }) + expect(calibrate).toBeDisabled() + }) + + it('renders a disabled calibrate button if module temperature status errors', () => { + vi.mocked(useIsFlex).mockReturnValue(true) + const mockHeatingModule = { + ...mockHeaterShaker, + data: { + ...mockHeaterShaker.data, + temperatureStatus: 'error' as TemperatureStatus, + }, + } + props = { + ...props, + module: mockHeatingModule, + } + render(props) + + const calibrate = screen.getByRole('button', { name: 'Calibrate' }) + expect(calibrate).toBeDisabled() + }) + it('a mock function should be called when clicking Calibrate if pipette is ready', () => { vi.mocked(useIsFlex).mockReturnValue(true) props = { From d6472ae66cf04e80ed23d1af75cacecf0f92d89e Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 3 May 2024 15:29:21 -0400 Subject: [PATCH 2/2] fix(app): fix input field behavior on blur (#15035) * fix(app): fix input field behavior on blur --- app/src/atoms/InputField/index.tsx | 10 ++++++++-- app/src/organisms/ChooseProtocolSlideout/index.tsx | 5 ++++- app/src/organisms/ChooseRobotSlideout/index.tsx | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/atoms/InputField/index.tsx b/app/src/atoms/InputField/index.tsx index 09579d8bb2bb..f891b580c6ca 100644 --- a/app/src/atoms/InputField/index.tsx +++ b/app/src/atoms/InputField/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { css } from 'styled-components' +import styled, { css } from 'styled-components' import { ALIGN_CENTER, @@ -276,7 +276,7 @@ function Input(props: InputFieldProps): JSX.Element { } }} > - ) } + +const StyledInput = styled.input` + &::placeholder { + color: ${COLORS.grey40}; + } +` diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index fd0f2178013e..98dc980c1138 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -93,6 +93,7 @@ export function ChooseProtocolSlideoutComponent( ] = React.useState([]) const [currentPage, setCurrentPage] = React.useState(1) const [hasParamError, setHasParamError] = React.useState(false) + const [isInputFocused, setIsInputFocused] = React.useState(false) React.useEffect(() => { setRunTimeParametersOverrides( @@ -229,7 +230,7 @@ export function ChooseProtocolSlideoutComponent( const value = runtimeParam.value as number const id = `InputField_${runtimeParam.variableName}_${index.toString()}` const error = - Number.isNaN(value) || + (Number.isNaN(value) && !isInputFocused) || value < runtimeParam.min || value > runtimeParam.max ? t(`protocol_details:value_out_of_range`, { @@ -258,6 +259,8 @@ export function ChooseProtocolSlideoutComponent( caption={`${runtimeParam.min}-${runtimeParam.max}`} id={id} error={error} + onBlur={() => setIsInputFocused(false)} + onFocus={() => setIsInputFocused(true)} onChange={e => { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) { diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 72c07ab90a96..520b73a3633d 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -146,6 +146,7 @@ export function ChooseRobotSlideout( const dispatch = useDispatch() const isScanning = useSelector((state: State) => getScanning(state)) const [targetProps, tooltipProps] = useHoverTooltip() + const [isInputFocused, setIsInputFocused] = React.useState(false) const unhealthyReachableRobots = useSelector((state: State) => getReachableRobots(state) @@ -383,7 +384,7 @@ export function ChooseRobotSlideout( const value = runtimeParam.value as number const id = `InputField_${runtimeParam.variableName}_${index.toString()}` const error = - Number.isNaN(value) || + (Number.isNaN(value) && !isInputFocused) || value < runtimeParam.min || value > runtimeParam.max ? t(`value_out_of_range`, { @@ -418,6 +419,8 @@ export function ChooseRobotSlideout( } id={id} error={error} + onBlur={() => setIsInputFocused(false)} + onFocus={() => setIsInputFocused(true)} onChange={e => { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) {