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

feat(app): add input screen for ODD numerical runtime parameters #14858

Merged
merged 12 commits into from
Apr 11, 2024
5 changes: 4 additions & 1 deletion app/src/assets/localization/en/protocol_setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@
"resolve": "Resolve",
"restart_setup_and_try": "Restart setup and try using different parameter values.",
"restart_setup": "Restart setup",
"restore_default": "Restore default values",
"restore_defaults": "Restore default values",
"restore_default": "Restore default value",
"robot_cal_description": "Robot calibration establishes how the robot knows where it is in relation to the deck. Accurate Robot calibration is essential to run protocols successfully. Robot calibration has 3 parts: Deck calibration, Tip Length calibration and Pipette Offset calibration.",
"robot_cal_help_title": "How Robot Calibration Works",
"robot_calibration_step_description_pipettes_only": "Review required instruments and calibrations for this protocol.",
Expand Down Expand Up @@ -265,6 +266,8 @@
"usb_port_connected": "USB Port {{port}}",
"value": "Value",
"values_are_view_only": "Values are view-only",
"value_out_of_range_generic": "Value must be in range",
"value_out_of_range": "Value must be between {{min}}-{{max}}",
"view_current_offsets": "View current offsets",
"view_moam": "View setup instructions for placing modules of the same type to the robot.",
"view_setup_instructions": "View setup instructions",
Expand Down
34 changes: 18 additions & 16 deletions app/src/atoms/InputField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface InputFieldProps {
| typeof TYPOGRAPHY.textAlignCenter
/** small or medium input field height, relevant only */
size?: 'medium' | 'small'
ref?: React.MutableRefObject<null>
}

export function InputField(props: InputFieldProps): JSX.Element {
Expand Down Expand Up @@ -108,6 +109,7 @@ function Input(props: InputFieldProps): JSX.Element {

const OUTER_CSS = css`
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
grid-gap: ${SPACING.spacing8};
&:focus-within {
filter: ${error
? 'none'
Expand Down Expand Up @@ -191,18 +193,16 @@ function Input(props: InputFieldProps): JSX.Element {
`

const FORM_BOTTOM_SPACE_STYLE = css`
padding: ${SPACING.spacing4} 0rem;
padding-top: ${SPACING.spacing4};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
padding: ${SPACING.spacing8} 0rem;
padding-bottom: 0;
}
`

const TITLE_STYLE = css`
color: ${error ? COLORS.red50 : COLORS.black90};
padding-bottom: ${SPACING.spacing8};
font-size: ${TYPOGRAPHY.fontSizeLabel};
font-weight: ${TYPOGRAPHY.fontWeightSemiBold};
line-height: ${TYPOGRAPHY.lineHeight12};
align-text: ${textAlign};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
font-size: ${TYPOGRAPHY.fontSize22};
Expand All @@ -214,9 +214,11 @@ function Input(props: InputFieldProps): JSX.Element {

const ERROR_TEXT_STYLE = css`
color: ${COLORS.red50};
padding-top: ${SPACING.spacing4};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
font-size: ${TYPOGRAPHY.fontSize22};
color: ${COLORS.red50};
padding-top: ${SPACING.spacing8};
}
`

Expand All @@ -239,9 +241,14 @@ function Input(props: InputFieldProps): JSX.Element {
<Flex flexDirection={DIRECTION_COLUMN} width="100%">
{title != null ? (
<Flex gridGap={SPACING.spacing8}>
<Flex as="label" htmlFor={props.id} css={TITLE_STYLE}>
<StyledText
as="label"
fontWeight={TYPOGRAPHY.fontWeightSemiBold}
htmlFor={props.id}
css={TITLE_STYLE}
>
{title}
</Flex>
</StyledText>
{tooltipText != null ? (
<>
<Flex {...targetProps}>
Expand Down Expand Up @@ -277,16 +284,6 @@ function Input(props: InputFieldProps): JSX.Element {
<Flex css={UNITS_STYLE}>{props.units}</Flex>
) : null}
</Flex>
{props.error != null ? (
<Flex
color={COLORS.grey60}
fontSize={TYPOGRAPHY.fontSizeLabel}
paddingTop={SPACING.spacing4}
flexDirection={DIRECTION_COLUMN}
>
<Flex css={ERROR_TEXT_STYLE}>{props.error}</Flex>
</Flex>
) : null}
</Flex>
{props.caption != null ? (
<StyledText
Expand All @@ -306,6 +303,11 @@ function Input(props: InputFieldProps): JSX.Element {
{props.secondaryCaption}
</StyledText>
) : null}
{props.error != null ? (
<StyledText as="label" css={ERROR_TEXT_STYLE}>
{props.error}
</StyledText>
) : null}
</Flex>
)
}
2 changes: 1 addition & 1 deletion app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import { KeyboardReact as Keyboard } from 'react-simple-keyboard'
import Keyboard from 'react-simple-keyboard'
import { numericalKeyboardLayout, numericalCustom } from '../constants'
import '../index.css'
import './index.css'
Expand Down
2 changes: 1 addition & 1 deletion app/src/organisms/ChooseProtocolSlideout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export function ChooseProtocolSlideoutComponent(
key={runtimeParam.variableName}
type="number"
units={runtimeParam.suffix}
placeholder={value.toString()}
placeholder={runtimeParam.default.toString()}
value={value}
title={runtimeParam.displayName}
tooltipText={runtimeParam.description}
Expand Down
2 changes: 1 addition & 1 deletion app/src/organisms/ChooseRobotSlideout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ export function ChooseRobotSlideout(
key={runtimeParam.variableName}
type="number"
units={runtimeParam.suffix}
placeholder={value.toString()}
placeholder={runtimeParam.default.toString()}
value={value}
title={runtimeParam.displayName}
tooltipText={runtimeParam.description}
Expand Down
14 changes: 14 additions & 0 deletions app/src/organisms/Devices/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import type {
Instruments,
PipetteData,
PipetteOffsetCalibration,
RunTimeParameterCreateData,
} from '@opentrons/api-client'
import type { RunTimeParameter } from '@opentrons/shared-data'

/**
* formats a string if it is in ISO 8601 date format
Expand Down Expand Up @@ -89,3 +91,15 @@ export function getShowPipetteCalibrationWarning(
}) ?? false
)
}

export function getRunTimeParameterValuesForRun(
runTimeParameters: RunTimeParameter[]
): RunTimeParameterCreateData {
return runTimeParameters.reduce(
(acc, param) =>
param.value !== param.default
? { ...acc, [param.variableName]: param.value }
: acc,
{}
)
}
7 changes: 1 addition & 6 deletions app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ export function ChooseEnum({
const { makeSnackbar } = useToaster()

const { t } = useTranslation(['protocol_setup', 'shared'])
if (parameter.type !== 'str') {
console.error(
`parameter type is expected to be a string for parameter ${parameter.displayName}`
)
}
const options = parameter.type === 'str' ? parameter.choices : undefined
const options = 'choices' in parameter ? parameter.choices : undefined
const handleOnClick = (newValue: string | number | boolean): void => {
setParameter(newValue, parameter.variableName)
}
Expand Down
166 changes: 166 additions & 0 deletions app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import {
ALIGN_CENTER,
DIRECTION_COLUMN,
Flex,
JUSTIFY_CENTER,
SPACING,
StyledText,
TYPOGRAPHY,
} from '@opentrons/components'
import { InputField } from '../../atoms/InputField'
import { useToaster } from '../ToasterOven'
import { ChildNavigation } from '../ChildNavigation'
import type { NumberParameter } from '@opentrons/shared-data'
import { NumericalKeyboard } from '../../atoms/SoftwareKeyboard'

interface ChooseNumberProps {
handleGoBack: () => void
parameter: NumberParameter
setParameter: (value: number, variableName: string) => void
}

export function ChooseNumber({
handleGoBack,
parameter,
setParameter,
}: ChooseNumberProps): JSX.Element | null {
const { makeSnackbar } = useToaster()

const { i18n, t } = useTranslation(['protocol_setup', 'shared'])
const keyboardRef = React.useRef(null)
const [paramValue, setParamValue] = React.useState<string>(
String(parameter.value)
)

// We need to arbitrarily set the value of the keyboard to a string the
// same length as the initial parameter value (as string) when the component mounts
// so that the delete button operates properly on the exisiting input field value.
const [prevKeyboardValue, setPrevKeyboardValue] = React.useState<string>('')
React.useEffect(() => {
const arbitraryInput = new Array(paramValue).join('*')
// @ts-expect-error keyboard should expose for `setInput` method
keyboardRef.current?.setInput(arbitraryInput)
setPrevKeyboardValue(arbitraryInput)
}, [])

if (parameter.type !== 'int' && parameter.type !== 'float') {
console.log(`Incorrect parameter type: ${parameter.type}`)
return null
}
const handleClickGoBack = (newValue: number): void => {
if (error != null) {
makeSnackbar(t('value_out_of_range_generic'))
} else {
setParameter(newValue, parameter.variableName)
handleGoBack()
}
}

const handleKeyboardInput = (e: string): void => {
if (prevKeyboardValue.length < e.length) {
const lastDigit = e.slice(-1)
if (
!'.-'.includes(lastDigit) ||
(lastDigit === '.' && !paramValue.includes('.')) ||
(lastDigit === '-' && paramValue.length === 0)
) {
setParamValue(paramValue + lastDigit)
}
} else {
setParamValue(paramValue.slice(0, paramValue.length - 1))
}
setPrevKeyboardValue(e)
}

const paramValueAsNumber = Number(paramValue)
const resetValueDisabled = parameter.default === paramValueAsNumber
const { min, max } = parameter
const error =
paramValue === '' ||
Number.isNaN(paramValueAsNumber) ||
paramValueAsNumber < min ||
paramValueAsNumber > max
? t(`value_out_of_range`, {
min: parameter.type === 'int' ? min : min.toFixed(1),
max: parameter.type === 'int' ? max : max.toFixed(1),
})
: null

return (
<>
<ChildNavigation
header={i18n.format(parameter.displayName, 'sentenceCase')}
onClickBack={() => {
handleClickGoBack(paramValueAsNumber)
}}
buttonType="tertiaryLowLight"
buttonText={t('restore_default')}
onClickButton={() =>
resetValueDisabled
? makeSnackbar(t('no_custom_values'))
: setParamValue(String(parameter.default))
}
/>
<Flex
alignSelf={ALIGN_CENTER}
gridGap={SPACING.spacing48}
paddingX={SPACING.spacing40}
paddingBottom={SPACING.spacing40}
marginTop="7.75rem"
height="22rem"
justifyContent={JUSTIFY_CENTER}
alignItems={ALIGN_CENTER}
>
<Flex
width="30.5rem"
height="100%"
gridGap={SPACING.spacing24}
flexDirection={DIRECTION_COLUMN}
marginTop="7.75rem"
>
<StyledText as="h4" textAlign={TYPOGRAPHY.textAlignLeft}>
{parameter.description}
</StyledText>
<InputField
type="text"
units={parameter.suffix}
placeholder={parameter.default.toString()}
value={paramValue}
title={parameter.displayName}
caption={
parameter.type === 'int'
? `${parameter.min}-${parameter.max}`
: `${parameter.min.toFixed(1)}-${parameter.max.toFixed(1)}`
}
error={error}
onChange={e => {
const updatedValue =
parameter.type === 'int'
? Math.round(e.target.valueAsNumber)
: e.target.valueAsNumber
setParamValue(
Number.isNaN(updatedValue) ? '' : String(updatedValue)
)
}}
/>
</Flex>
<Flex
paddingX={SPACING.spacing24}
height="21.25rem"
marginTop="7.75rem"
>
<NumericalKeyboard
keyboardRef={keyboardRef}
isDecimal={parameter.type === 'float'}
hasHyphen={true}
onChange={e => {
handleKeyboardInput(e)
}}
/>
</Flex>
</Flex>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { ChildNavigation } from '../ChildNavigation'
import { useToaster } from '../ToasterOven'
import { mockData } from './index'

import type { SetupScreens } from '../../pages/ProtocolSetup'

Expand All @@ -37,7 +36,7 @@ export function ViewOnlyParameters({
}

// TODO(jr, 3/18/24): remove mockData
const parameters = mostRecentAnalysis?.runTimeParameters ?? mockData
const parameters = mostRecentAnalysis?.runTimeParameters ?? []

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('ChooseEnum', () => {
})
it('calls the prop if reset default is clicked when the default has changed', () => {
render(props)
fireEvent.click(screen.getByText('Restore default values'))
fireEvent.click(screen.getByText('Restore default value'))
expect(props.setParameter).toHaveBeenCalled()
})
it('calls does not call prop if reset default is clicked when the default has not changed', () => {
Expand All @@ -61,7 +61,7 @@ describe('ChooseEnum', () => {
rawValue: 'none',
}
render(props)
fireEvent.click(screen.getByText('Restore default values'))
fireEvent.click(screen.getByText('Restore default value'))
expect(props.setParameter).not.toHaveBeenCalled()
})
it('should render the text and buttons for choice param', () => {
Expand Down
Loading
Loading