Skip to content

Commit

Permalink
feat(protocol-designer): initial absorbance reader stepform UI (#17280)
Browse files Browse the repository at this point in the history
This PR adds the initial UI for absorbance reader steps. It creates the
new AbsorbanceReaderTools component in line with our other step form
toolbox components, and modularizes its children components for
cleanliness. The content of both pages 1 and 2 of the form are
determined by:
1) module initialization state, and
2) labware presence or lack thereof.

According to designs, the UI will funnel the user into the correct step
form type (lid, initializaton, or reading) based on those 2 inputs. Also
implicated here are creation and wiring up of the following methods:
- patch to pre-select an absorbance reader module if only one is
available
- form change handlers for absorbance reader dependent fields
- absorbance reader module option getter

Closes AUTH-1264
  • Loading branch information
ncdiehl11 authored Jan 15, 2025
1 parent 9ab7ba9 commit 88408a2
Show file tree
Hide file tree
Showing 16 changed files with 427 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"protocol_name": "Protocol Name",
"save": "save",
"stepType": {
"absorbanceReader": "absorbance plate reader",
"comment": "comment",
"ending_hold": "ending hold",
"heaterShaker": "heater-shaker",
Expand All @@ -39,7 +40,6 @@
"moveLabware": "move",
"moveLiquid": "transfer",
"pause": "pause",
"plateReader": "absorbance reader",
"profile_settings": "profile settings",
"profile_steps": "profile steps",
"temperature": "temperature",
Expand All @@ -57,6 +57,7 @@
"microliter": "μL",
"microliterPerSec": "μL/s",
"millimeter": "mm",
"nanometer": "nm",
"minutes": "m",
"rpm": "rpm",
"seconds": "s",
Expand Down
9 changes: 6 additions & 3 deletions protocol-designer/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ export const STAGING_AREA_CUTOUTS_ORDERED: CutoutId[] = [
]
export const ABSORBANCE_READER_INITIALIZE_MODE_SINGLE = 'single'
export const ABSORBANCE_READER_INITIALIZE_MODE_MULTI = 'multi'
export const ABSORBANCE_READER_INITIALIZE: 'initialize' = 'initialize'
export const ABSORBANCE_READER_READ: 'read' = 'read'
export const ABSORBANCE_READER_LID: 'lid' = 'lid'
export const ABSORBANCE_READER_INITIALIZE: 'absorbanceReaderInitialize' =
'absorbanceReaderInitialize'
export const ABSORBANCE_READER_READ: 'absorbanceReaderRead' =
'absorbanceReaderRead'
export const ABSORBANCE_READER_LID: 'absorbanceReaderLid' =
'absorbanceReaderLid'
11 changes: 7 additions & 4 deletions protocol-designer/src/form-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,11 +379,14 @@ export interface HydratedHeaterShakerFormData {
targetHeaterShakerTemperature: string | null
targetSpeed: string | null
}

export type AbsorbanceReaderFormType =
| typeof ABSORBANCE_READER_INITIALIZE
| typeof ABSORBANCE_READER_READ
| typeof ABSORBANCE_READER_LID

export interface HydratedAbsorbanceReaderFormData {
absorbanceReaderFormType:
| typeof ABSORBANCE_READER_INITIALIZE
| typeof ABSORBANCE_READER_READ
| typeof ABSORBANCE_READER_LID
absorbanceReaderFormType: AbsorbanceReaderFormType | null
filePath: string | null
lidOpen: boolean | null
mode:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
}

const isMultiStepToolbox =
formData.stepType === 'absorbanceReader' ||
formData.stepType === 'moveLiquid' ||
formData.stepType === 'mix' ||
formData.stepType === 'thermocycler'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DIRECTION_COLUMN, Flex, SPACING } from '@opentrons/components'
import type { FormData } from '../../../../../../form-types'
import type { FieldPropsByName } from '../../types'

interface InitializationEditorProps {
formData: FormData
propsForFields: FieldPropsByName
}

export function InitializationEditor(
props: InitializationEditorProps
): JSX.Element {
return (
<Flex gridGap={SPACING.spacing4} flexDirection={DIRECTION_COLUMN}>
<>TODO add wavelength component </>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useTranslation } from 'react-i18next'
import {
Flex,
DIRECTION_COLUMN,
SPACING,
ListItem,
StyledText,
InfoScreen,
} from '@opentrons/components'
import type { Initialization } from '../../../../../../step-forms/types'

interface InitializationSettingsProps {
initialization: Initialization | null
}

export function InitializationSettings(
props: InitializationSettingsProps
): JSX.Element {
const { initialization } = props
const { t } = useTranslation('form')
const content =
initialization == null ? (
<InfoScreen height="12.75rem" content={t('no_settings_defined')} />
) : (
initialization.wavelengths.map(wavelength => (
<ListItem
type="noActive"
key={`listItem_${wavelength}`}
padding={SPACING.spacing12}
>
<StyledText desktopStyle="bodyDefaultRegular">{`${wavelength} ${t(
'application:units.nanometer'
)}`}</StyledText>
</ListItem>
))
)

return (
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
width="100%"
>
<StyledText desktopStyle="bodyDefaultSemiBold">
{t('current_initialization_settings')}
</StyledText>
{content}
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useTranslation } from 'react-i18next'
import {
DIRECTION_COLUMN,
Flex,
SPACING,
StyledText,
} from '@opentrons/components'
import { ToggleStepFormField } from '../../../../../../molecules'

import type { FieldProps } from '../../types'

interface LidControlsProps {
fieldProps: FieldProps
label?: string
paddingX?: string
}

export function LidControls(props: LidControlsProps): JSX.Element {
const { fieldProps, label, paddingX = '0' } = props
const { t } = useTranslation('form')
return (
<Flex
width="100%"
paddingX={paddingX}
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
>
{label != null ? (
<StyledText desktopStyle="bodyDefaultSemiBold">{label}</StyledText>
) : null}
<ToggleStepFormField
title={t('lid_position')}
isSelected={fieldProps.value === true}
onLabel={t('open')}
offLabel={t('closed')}
toggleUpdateValue={() => {
fieldProps.updateValue(!fieldProps.value)
}}
toggleValue={fieldProps.value}
isDisabled={false}
tooltipContent={null}
/>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useTranslation } from 'react-i18next'
import {
DIRECTION_COLUMN,
Flex,
SPACING,
StyledText,
} from '@opentrons/components'
import { InputStepFormField } from '../../../../../../molecules'
import type { FieldPropsByName } from '../../types'

interface ReadSettingsProps {
propsForFields: FieldPropsByName
}

export function ReadSettings(props: ReadSettingsProps): JSX.Element {
const { propsForFields } = props
const { t } = useTranslation('form')
return (
<Flex
flexDirection={DIRECTION_COLUMN}
paddingX={SPACING.spacing16}
gridGap={SPACING.spacing12}
width="100%"
>
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText desktopStyle="bodyDefaultSemiBold">
{t('export_settings')}
</StyledText>
<StyledText desktopStyle="bodyDefaultRegular">
{t('export_detail')}
</StyledText>
</Flex>
<InputStepFormField
padding="0"
{...propsForFields.filePath}
title={t('exported_file_name')}
/>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,154 @@
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import {
DIRECTION_COLUMN,
Divider,
Flex,
RadioButton,
SPACING,
StyledText,
} from '@opentrons/components'
import {
ABSORBANCE_READER_INITIALIZE,
ABSORBANCE_READER_LID,
ABSORBANCE_READER_READ,
} from '../../../../../../constants'
import { DropdownStepFormField } from '../../../../../../molecules'
import { getRobotStateAtActiveItem } from '../../../../../../top-selectors/labware-locations'
import { getAbsorbanceReaderLabwareOptions } from '../../../../../../ui/modules/selectors'
import { hoverSelection } from '../../../../../../ui/steps/actions/actions'
import { InitializationSettings } from './InitializationSettings'
import { InitializationEditor } from './InitializationEditor'
import { LidControls } from './LidControls'
import { ReadSettings } from './ReadSettings'

import type { AbsorbanceReaderState } from '@opentrons/step-generation'
import type { AbsorbanceReaderFormType } from '../../../../../../form-types'
import type { StepFormProps } from '../../types'

export function AbsorbanceReaderTools(props: StepFormProps): JSX.Element {
return <div>TODO: ADD PLATE READER TOOLS</div>
const { formData, propsForFields, toolboxStep } = props
const { moduleId } = formData
const dispatch = useDispatch()
const { t } = useTranslation('form')
const robotState = useSelector(getRobotStateAtActiveItem)
const absorbanceReaderOptions = useSelector(getAbsorbanceReaderLabwareOptions)
const { labware = {}, modules = {} } = robotState ?? {}
const isLabwareOnAbsorbanceReader = Object.values(labware).some(
lw => lw.slot === propsForFields.moduleId.value
)
const absorbanceReaderFormType = formData.absorbanceReaderFormType as AbsorbanceReaderFormType
const absorbanceReaderState = modules[moduleId]
?.moduleState as AbsorbanceReaderState | null
const initialization = absorbanceReaderState?.initialization ?? null

const enableReadOrInitialization =
!isLabwareOnAbsorbanceReader || initialization != null
const compoundCommandType = isLabwareOnAbsorbanceReader
? ABSORBANCE_READER_READ
: ABSORBANCE_READER_INITIALIZE

// pre-select radio button on mount and module change if not previously set
useEffect(() => {
if (formData.absorbanceReaderFormType == null) {
if (enableReadOrInitialization) {
propsForFields.absorbanceReaderFormType.updateValue(compoundCommandType)
return
}
propsForFields.absorbanceReaderFormType.updateValue(ABSORBANCE_READER_LID)
}
}, [formData.moduleId])

const lidRadioButton = (
<RadioButton
onChange={() => {
propsForFields.absorbanceReaderFormType.updateValue(
ABSORBANCE_READER_LID
)
}}
isSelected={absorbanceReaderFormType === ABSORBANCE_READER_LID}
buttonLabel={t('change_lid_position')}
buttonValue={ABSORBANCE_READER_LID}
largeDesktopBorderRadius
/>
)
const compoundCommandButton = (
<RadioButton
onChange={() => {
propsForFields.absorbanceReaderFormType.updateValue(compoundCommandType)
}}
isSelected={absorbanceReaderFormType === compoundCommandType}
buttonLabel={t(compoundCommandType)}
buttonValue={compoundCommandType}
largeDesktopBorderRadius
/>
)

const page1Content = (
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing12}>
<DropdownStepFormField
options={absorbanceReaderOptions}
title={t('module')}
{...propsForFields.moduleId}
tooltipContent={null}
onEnter={(id: string) => {
dispatch(hoverSelection({ id, text: t('select') }))
}}
onExit={() => {
dispatch(hoverSelection({ id: null, text: null }))
}}
/>
{moduleId != null ? (
<>
<Divider marginY="0" />
<Flex paddingX={SPACING.spacing16}>
<InitializationSettings initialization={initialization} />
</Flex>
<Divider marginY="0" />
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
paddingX={SPACING.spacing16}
>
<StyledText desktopStyle="bodyDefaultSemiBold">
{t('module_controls')}
</StyledText>
{enableReadOrInitialization ? (
<>
{compoundCommandButton}
{lidRadioButton}
</>
) : (
<LidControls fieldProps={propsForFields.lidOpen} />
)}
</Flex>
</>
) : null}
</Flex>
)

const page2ContentMap = {
[ABSORBANCE_READER_READ]: <ReadSettings propsForFields={propsForFields} />,
[ABSORBANCE_READER_INITIALIZE]: (
<InitializationEditor
formData={formData}
propsForFields={propsForFields}
/>
),
[ABSORBANCE_READER_LID]: (
<LidControls
fieldProps={propsForFields.lidOpen}
label={t('change_lid_position')}
paddingX={SPACING.spacing16}
/>
),
}

const contentByPage: JSX.Element[] = [
page1Content,
page2ContentMap[absorbanceReaderFormType],
]

return <Flex paddingY={SPACING.spacing16}>{contentByPage[toolboxStep]}</Flex>
}
Loading

0 comments on commit 88408a2

Please sign in to comment.